001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.mailrepository.jdbc;
021
022import java.io.ByteArrayInputStream;
023import java.io.ByteArrayOutputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.ObjectInputStream;
027import java.io.ObjectOutputStream;
028import java.io.Serializable;
029import java.sql.Blob;
030import java.sql.Connection;
031import java.sql.DatabaseMetaData;
032import java.sql.PreparedStatement;
033import java.sql.ResultSet;
034import java.sql.SQLException;
035import java.sql.Statement;
036import java.util.ArrayList;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.Iterator;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043import java.util.StringTokenizer;
044
045import javax.annotation.PostConstruct;
046import javax.inject.Inject;
047import javax.mail.MessagingException;
048import javax.mail.internet.MimeMessage;
049import javax.sql.DataSource;
050
051import org.apache.commons.configuration.ConfigurationException;
052import org.apache.commons.configuration.DefaultConfigurationBuilder;
053import org.apache.commons.configuration.HierarchicalConfiguration;
054import org.apache.james.core.MailImpl;
055import org.apache.james.core.MimeMessageCopyOnWriteProxy;
056import org.apache.james.core.MimeMessageWrapper;
057import org.apache.james.filesystem.api.FileSystem;
058import org.apache.james.mailrepository.lib.AbstractMailRepository;
059import org.apache.james.repository.file.FilePersistentStreamRepository;
060import org.apache.james.util.sql.JDBCUtil;
061import org.apache.james.util.sql.SqlResources;
062import org.apache.mailet.Mail;
063import org.apache.mailet.MailAddress;
064
065/**
066 * Implementation of a MailRepository on a database.
067 * 
068 * <p>
069 * Requires a configuration element in the .conf.xml file of the form:
070 * 
071 * <pre>
072 *  &lt;repository destinationURL="db://&lt;datasource&gt;/&lt;table_name&gt;/&lt;repository_name&gt;"
073 *              type="MAIL"
074 *              model="SYNCHRONOUS"/&gt;
075 *  &lt;/repository&gt;
076 * </pre>
077 * 
078 * </p>
079 * <p>
080 * destinationURL specifies..(Serge??) <br>
081 * Type can be SPOOL or MAIL <br>
082 * Model is currently not used and may be dropped
083 * </p>
084 * 
085 * <p>
086 * Requires a logger called MailRepository.
087 * </p>
088 * 
089 * @version CVS $Revision$ $Date: 2010-12-29 21:47:46 +0100 (Wed, 29
090 *          Dec 2010) $
091 */
092public class JDBCMailRepository extends AbstractMailRepository {
093
094    /**
095     * The table name parsed from the destination URL
096     */
097    protected String tableName;
098
099    /**
100     * The repository name parsed from the destination URL
101     */
102    protected String repositoryName;
103
104    /**
105     * The name of the SQL configuration file to be used to configure this
106     * repository.
107     */
108    private String sqlFileName;
109
110    /**
111     * The stream repository used in dbfile mode
112     */
113    private FilePersistentStreamRepository sr = null;
114
115    /**
116     * The JDBC datasource that provides the JDBC connection
117     */
118    protected DataSource datasource;
119
120    /**
121     * The name of the datasource used by this repository
122     */
123    protected String datasourceName;
124
125    /**
126     * Contains all of the sql strings for this component.
127     */
128    protected SqlResources sqlQueries;
129
130    /**
131     * The JDBCUtil helper class
132     */
133    protected JDBCUtil theJDBCUtil;
134
135    /**
136     * "Support for Mail Attributes under JDBC repositories is ready" indicator.
137     */
138    protected boolean jdbcMailAttributesReady = false;
139
140    /**
141     * The size threshold for in memory handling of storing operations
142     */
143    private int inMemorySizeLimit;
144
145    private FileSystem fileSystem;
146
147    private String filestore;
148
149    private String destination;
150
151    @Inject
152    public void setDatasource(DataSource datasource) {
153        this.datasource = datasource;
154    }
155
156    @Inject
157    public void setFileSystem(FileSystem fileSystem) {
158        this.fileSystem = fileSystem;
159    }
160
161    protected void doConfigure(HierarchicalConfiguration configuration) throws ConfigurationException {
162        super.doConfigure(configuration);
163        if (getLogger().isDebugEnabled()) {
164            getLogger().debug(this.getClass().getName() + ".configure()");
165        }
166        destination = configuration.getString("[@destinationURL]");
167
168        // normalize the destination, to simplify processing.
169        if (!destination.endsWith("/")) {
170            destination += "/";
171        }
172        // Parse the DestinationURL for the name of the datasource,
173        // the table to use, and the (optional) repository Key.
174        // Split on "/", starting after "db://"
175        List<String> urlParams = new ArrayList<String>();
176        int start = 5;
177        if (destination.startsWith("dbfile")) {
178            // this is dbfile:// instead of db://
179            start += 4;
180        }
181        int end = destination.indexOf('/', start);
182        while (end > -1) {
183            urlParams.add(destination.substring(start, end));
184            start = end + 1;
185            end = destination.indexOf('/', start);
186        }
187
188        // Build SqlParameters and get datasource name from URL parameters
189        if (urlParams.size() == 0) {
190            String exceptionBuffer = "Malformed destinationURL - Must be of the format '" + "db://<data-source>[/<table>[/<repositoryName>]]'.  Was passed " + configuration.getString("[@destinationURL]");
191            throw new ConfigurationException(exceptionBuffer);
192        }
193        if (urlParams.size() >= 1) {
194            datasourceName = urlParams.get(0);
195        }
196        if (urlParams.size() >= 2) {
197            tableName = urlParams.get(1);
198        }
199        if (urlParams.size() >= 3) {
200            repositoryName = "";
201            for (int i = 2; i < urlParams.size(); i++) {
202                if (i >= 3) {
203                    repositoryName += '/';
204                }
205                repositoryName += urlParams.get(i);
206            }
207        }
208
209        if (getLogger().isDebugEnabled()) {
210            String logBuffer = "Parsed URL: table = '" + tableName + "', repositoryName = '" + repositoryName + "'";
211            getLogger().debug(logBuffer);
212        }
213
214        inMemorySizeLimit = configuration.getInt("inMemorySizeLimit", 409600000);
215
216        filestore = configuration.getString("filestore", null);
217        sqlFileName = configuration.getString("sqlFile");
218
219    }
220
221    /**
222     * Initialises the JDBC repository.
223     * <ol>
224     * <li>Tests the connection to the database.</li>
225     * <li>Loads SQL strings from the SQL definition file, choosing the
226     * appropriate SQL for this connection, and performing paramter
227     * substitution,</li>
228     * <li>Initialises the database with the required tables, if necessary.</li>
229     * </ol>
230     * 
231     * @throws Exception
232     *             if an error occurs
233     */
234    @Override
235    @PostConstruct
236    public void init() throws Exception {
237        StringBuffer logBuffer;
238        if (getLogger().isDebugEnabled()) {
239            getLogger().debug(this.getClass().getName() + ".initialize()");
240        }
241
242        try {
243            if (filestore != null) {
244
245                // prepare Configurations for stream repositories
246                DefaultConfigurationBuilder streamConfiguration = new DefaultConfigurationBuilder();
247
248                streamConfiguration.addProperty("[@destinationURL]", filestore);
249
250                sr = new FilePersistentStreamRepository();
251                sr.setLog(getLogger());
252                sr.setFileSystem(fileSystem);
253                sr.configure(streamConfiguration);
254                sr.init();
255
256                if (getLogger().isDebugEnabled()) {
257                    getLogger().debug("Got filestore for JdbcMailRepository: " + filestore);
258                }
259            }
260
261            if (getLogger().isDebugEnabled()) {
262                String logBuf = this.getClass().getName() + " created according to " + destination;
263                getLogger().debug(logBuf);
264            }
265        } catch (Exception e) {
266            final String message = "Failed to retrieve Store component:" + e.getMessage();
267            getLogger().error(message, e);
268            throw new ConfigurationException(message, e);
269        }
270
271        theJDBCUtil = new JDBCUtil() {
272            protected void delegatedLog(String logString) {
273                JDBCMailRepository.this.getLogger().warn("JDBCMailRepository: " + logString);
274            }
275        };
276
277        // Test the connection to the database, by getting the DatabaseMetaData.
278        Connection conn = datasource.getConnection();
279        PreparedStatement createStatement = null;
280
281        try {
282            // Initialise the sql strings.
283
284            InputStream sqlFile;
285            try {
286                sqlFile = fileSystem.getResource(sqlFileName);
287            } catch (Exception e) {
288                getLogger().error(e.getMessage(), e);
289                throw e;
290            }
291
292            if (getLogger().isDebugEnabled()) {
293                logBuffer = new StringBuffer(128).append("Reading SQL resources from file: ").append(sqlFileName).append(", section ").append(this.getClass().getName()).append(".");
294                getLogger().debug(logBuffer.toString());
295            }
296
297            // Build the statement parameters
298            Map<String, String> sqlParameters = new HashMap<String, String>();
299            if (tableName != null) {
300                sqlParameters.put("table", tableName);
301            }
302            if (repositoryName != null) {
303                sqlParameters.put("repository", repositoryName);
304            }
305
306            sqlQueries = new SqlResources();
307            sqlQueries.init(sqlFile, this.getClass().getName(), conn, sqlParameters);
308
309            // Check if the required table exists. If not, create it.
310            DatabaseMetaData dbMetaData = conn.getMetaData();
311            // Need to ask in the case that identifiers are stored, ask the
312            // DatabaseMetaInfo.
313            // Try UPPER, lower, and MixedCase, to see if the table is there.
314            if (!(theJDBCUtil.tableExists(dbMetaData, tableName))) {
315                // Users table doesn't exist - create it.
316                createStatement = conn.prepareStatement(sqlQueries.getSqlString("createTable", true));
317                createStatement.execute();
318
319                if (getLogger().isInfoEnabled()) {
320                    logBuffer = new StringBuffer(64).append("JdbcMailRepository: Created table '").append(tableName).append("'.");
321                    getLogger().info(logBuffer.toString());
322                }
323            }
324
325            checkJdbcAttributesSupport(dbMetaData);
326
327        } finally {
328            theJDBCUtil.closeJDBCStatement(createStatement);
329            theJDBCUtil.closeJDBCConnection(conn);
330        }
331    }
332
333    /**
334     * Checks whether support for JDBC Mail atributes is activated for this
335     * repository and if everything is consistent.<br>
336     * Looks for both the "updateMessageAttributesSQL" and
337     * "retrieveMessageAttributesSQL" statements in sqlResources and for a table
338     * column named "message_attributes".
339     * 
340     * @param dbMetaData
341     *            the database metadata to be used to look up the column
342     * @throws SQLException
343     *             if a fatal situation is met
344     */
345    protected void checkJdbcAttributesSupport(DatabaseMetaData dbMetaData) throws SQLException {
346        String attributesColumnName = "message_attributes";
347        boolean hasUpdateMessageAttributesSQL = false;
348        boolean hasRetrieveMessageAttributesSQL = false;
349
350        boolean hasMessageAttributesColumn = theJDBCUtil.columnExists(dbMetaData, tableName, attributesColumnName);
351
352        StringBuilder logBuffer = new StringBuilder(64).append("JdbcMailRepository '").append(repositoryName).append(", table '").append(tableName).append("': ");
353
354        // Determine whether attributes are used and available for storing
355        // Do we have updateMessageAttributesSQL?
356        String updateMessageAttrSql = sqlQueries.getSqlString("updateMessageAttributesSQL", false);
357        if (updateMessageAttrSql != null) {
358            hasUpdateMessageAttributesSQL = true;
359        }
360
361        // Determine whether attributes are used and retrieve them
362        // Do we have retrieveAttributesSQL?
363        String retrieveMessageAttrSql = sqlQueries.getSqlString("retrieveMessageAttributesSQL", false);
364        if (retrieveMessageAttrSql != null) {
365            hasRetrieveMessageAttributesSQL = true;
366        }
367
368        if (hasUpdateMessageAttributesSQL && !hasRetrieveMessageAttributesSQL) {
369            logBuffer.append("JDBC Mail Attributes support was activated for update but not for retrieval" + "(found 'updateMessageAttributesSQL' but not 'retrieveMessageAttributesSQL'" + "in table '").append(tableName).append("').");
370            getLogger().error(logBuffer.toString());
371            throw new SQLException(logBuffer.toString());
372        }
373        if (!hasUpdateMessageAttributesSQL && hasRetrieveMessageAttributesSQL) {
374            logBuffer.append("JDBC Mail Attributes support was activated for retrieval but not for update" + "(found 'retrieveMessageAttributesSQL' but not 'updateMessageAttributesSQL'" + "in table '").append(tableName).append("'.");
375            getLogger().error(logBuffer.toString());
376            throw new SQLException(logBuffer.toString());
377        }
378        if (!hasMessageAttributesColumn && (hasUpdateMessageAttributesSQL || hasRetrieveMessageAttributesSQL)) {
379            logBuffer.append("JDBC Mail Attributes support was activated but column '").append(attributesColumnName).append("' is missing in table '").append(tableName).append("'.");
380            getLogger().error(logBuffer.toString());
381            throw new SQLException(logBuffer.toString());
382        }
383        if (hasUpdateMessageAttributesSQL && hasRetrieveMessageAttributesSQL) {
384            jdbcMailAttributesReady = true;
385            if (getLogger().isInfoEnabled()) {
386                logBuffer.append("JDBC Mail Attributes support ready.");
387                getLogger().info(logBuffer.toString());
388            }
389        } else {
390            jdbcMailAttributesReady = false;
391            logBuffer.append("JDBC Mail Attributes support not activated. " + "Missing both 'updateMessageAttributesSQL' " + "and 'retrieveMessageAttributesSQL' " + "statements for table '").append(tableName).append("' in sqlResources.xml. ").append("Will not persist in the repository '").append(repositoryName).append("'.");
392            getLogger().warn(logBuffer.toString());
393        }
394    }
395
396    /**
397     * @see org.apache.james.mailrepository.lib.AbstractMailRepository#internalStore(Mail)
398     */
399    protected void internalStore(Mail mc) throws IOException, MessagingException {
400        Connection conn = null;
401        try {
402            conn = datasource.getConnection();
403            // Need to determine whether need to insert this record, or update
404            // it.
405
406            // Determine whether the message body has changed, and possibly
407            // avoid
408            // updating the database.
409            boolean saveBody;
410
411            MimeMessage messageBody = mc.getMessage();
412            // if the message is a CopyOnWrite proxy we check the modified
413            // wrapped object.
414            if (messageBody instanceof MimeMessageCopyOnWriteProxy) {
415                MimeMessageCopyOnWriteProxy messageCow = (MimeMessageCopyOnWriteProxy) messageBody;
416                messageBody = messageCow.getWrappedMessage();
417            }
418            if (messageBody instanceof MimeMessageWrapper) {
419                MimeMessageWrapper message = (MimeMessageWrapper) messageBody;
420                saveBody = message.isModified();
421                if (saveBody) {
422                    message.loadMessage();
423                }
424            } else {
425                saveBody = true;
426            }
427            MessageInputStream is = new MessageInputStream(mc, sr, inMemorySizeLimit, true);
428
429            // Begin a transaction
430            conn.setAutoCommit(false);
431
432            PreparedStatement checkMessageExists = null;
433            ResultSet rsExists = null;
434            boolean exists = false;
435            try {
436                checkMessageExists = conn.prepareStatement(sqlQueries.getSqlString("checkMessageExistsSQL", true));
437                checkMessageExists.setString(1, mc.getName());
438                checkMessageExists.setString(2, repositoryName);
439                rsExists = checkMessageExists.executeQuery();
440                exists = rsExists.next() && rsExists.getInt(1) > 0;
441            } finally {
442                theJDBCUtil.closeJDBCResultSet(rsExists);
443                theJDBCUtil.closeJDBCStatement(checkMessageExists);
444            }
445
446            if (exists) {
447                // MessageInputStream is = new
448                // MessageInputStream(mc,sr,inMemorySizeLimit, true);
449
450                // Update the existing record
451                PreparedStatement updateMessage = null;
452
453                try {
454                    updateMessage = conn.prepareStatement(sqlQueries.getSqlString("updateMessageSQL", true));
455                    updateMessage.setString(1, mc.getState());
456                    updateMessage.setString(2, mc.getErrorMessage());
457                    if (mc.getSender() == null) {
458                        updateMessage.setNull(3, java.sql.Types.VARCHAR);
459                    } else {
460                        updateMessage.setString(3, mc.getSender().toString());
461                    }
462                    StringBuilder recipients = new StringBuilder();
463                    for (Iterator<MailAddress> i = mc.getRecipients().iterator(); i.hasNext();) {
464                        recipients.append(i.next().toString());
465                        if (i.hasNext()) {
466                            recipients.append("\r\n");
467                        }
468                    }
469                    updateMessage.setString(4, recipients.toString());
470                    updateMessage.setString(5, mc.getRemoteHost());
471                    updateMessage.setString(6, mc.getRemoteAddr());
472                    updateMessage.setTimestamp(7, new java.sql.Timestamp(mc.getLastUpdated().getTime()));
473                    updateMessage.setString(8, mc.getName());
474                    updateMessage.setString(9, repositoryName);
475                    updateMessage.execute();
476                } finally {
477                    Statement localUpdateMessage = updateMessage;
478                    // Clear reference to statement
479                    updateMessage = null;
480                    theJDBCUtil.closeJDBCStatement(localUpdateMessage);
481                }
482
483                // Determine whether attributes are used and available for
484                // storing
485                if (jdbcMailAttributesReady && mc.hasAttributes()) {
486                    String updateMessageAttrSql = sqlQueries.getSqlString("updateMessageAttributesSQL", false);
487                    PreparedStatement updateMessageAttr = null;
488                    try {
489                        updateMessageAttr = conn.prepareStatement(updateMessageAttrSql);
490                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
491                        ObjectOutputStream oos = new ObjectOutputStream(baos);
492                        try {
493                            if (mc instanceof MailImpl) {
494                                oos.writeObject(((MailImpl) mc).getAttributesRaw());
495                            } else {
496                                HashMap<String, Serializable> temp = new HashMap<String, Serializable>();
497                                for (Iterator<String> i = mc.getAttributeNames(); i.hasNext();) {
498                                    String hashKey = i.next();
499                                    temp.put(hashKey, mc.getAttribute(hashKey));
500                                }
501                                oos.writeObject(temp);
502                            }
503                            oos.flush();
504                            ByteArrayInputStream attrInputStream = new ByteArrayInputStream(baos.toByteArray());
505                            updateMessageAttr.setBinaryStream(1, attrInputStream, baos.size());
506                        } finally {
507                            try {
508                                if (oos != null) {
509                                    oos.close();
510                                }
511                            } catch (IOException ioe) {
512                                getLogger().debug("JDBCMailRepository: Unexpected exception while closing output stream.", ioe);
513                            }
514                        }
515                        updateMessageAttr.setString(2, mc.getName());
516                        updateMessageAttr.setString(3, repositoryName);
517                        updateMessageAttr.execute();
518                    } catch (SQLException sqle) {
519                        getLogger().info("JDBCMailRepository: Trying to update mail attributes failed.", sqle);
520
521                    } finally {
522                        theJDBCUtil.closeJDBCStatement(updateMessageAttr);
523                    }
524                }
525
526                if (saveBody) {
527
528                    PreparedStatement updateMessageBody = conn.prepareStatement(sqlQueries.getSqlString("updateMessageBodySQL", true));
529                    try {
530                        updateMessageBody.setBinaryStream(1, is, (int) is.getSize());
531                        updateMessageBody.setString(2, mc.getName());
532                        updateMessageBody.setString(3, repositoryName);
533                        updateMessageBody.execute();
534
535                    } finally {
536                        theJDBCUtil.closeJDBCStatement(updateMessageBody);
537                    }
538                }
539
540            } else {
541                // Insert the record into the database
542                PreparedStatement insertMessage = null;
543                try {
544                    String insertMessageSQL = sqlQueries.getSqlString("insertMessageSQL", true);
545                    int number_of_parameters = getNumberOfParameters(insertMessageSQL);
546                    insertMessage = conn.prepareStatement(insertMessageSQL);
547                    insertMessage.setString(1, mc.getName());
548                    insertMessage.setString(2, repositoryName);
549                    insertMessage.setString(3, mc.getState());
550                    insertMessage.setString(4, mc.getErrorMessage());
551                    if (mc.getSender() == null) {
552                        insertMessage.setNull(5, java.sql.Types.VARCHAR);
553                    } else {
554                        insertMessage.setString(5, mc.getSender().toString());
555                    }
556                    StringBuilder recipients = new StringBuilder();
557                    for (Iterator<MailAddress> i = mc.getRecipients().iterator(); i.hasNext();) {
558                        recipients.append(i.next().toString());
559                        if (i.hasNext()) {
560                            recipients.append("\r\n");
561                        }
562                    }
563                    insertMessage.setString(6, recipients.toString());
564                    insertMessage.setString(7, mc.getRemoteHost());
565                    insertMessage.setString(8, mc.getRemoteAddr());
566                    insertMessage.setTimestamp(9, new java.sql.Timestamp(mc.getLastUpdated().getTime()));
567
568                    insertMessage.setBinaryStream(10, is, (int) is.getSize());
569
570                    // Store attributes
571                    if (number_of_parameters > 10) {
572                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
573                        ObjectOutputStream oos = new ObjectOutputStream(baos);
574                        try {
575                            if (mc instanceof MailImpl) {
576                                oos.writeObject(((MailImpl) mc).getAttributesRaw());
577                            } else {
578                                HashMap<String, Serializable> temp = new HashMap<String, Serializable>();
579                                for (Iterator<String> i = mc.getAttributeNames(); i.hasNext();) {
580                                    String hashKey = i.next();
581                                    temp.put(hashKey, mc.getAttribute(hashKey));
582                                }
583                                oos.writeObject(temp);
584                            }
585                            oos.flush();
586                            ByteArrayInputStream attrInputStream = new ByteArrayInputStream(baos.toByteArray());
587                            insertMessage.setBinaryStream(11, attrInputStream, baos.size());
588                        } finally {
589                            try {
590                                if (oos != null) {
591                                    oos.close();
592                                }
593                            } catch (IOException ioe) {
594                                getLogger().debug("JDBCMailRepository: Unexpected exception while closing output stream.", ioe);
595                            }
596                        }
597                    }
598
599                    insertMessage.execute();
600                } finally {
601                    theJDBCUtil.closeJDBCStatement(insertMessage);
602                }
603            }
604
605            conn.commit();
606            conn.setAutoCommit(true);
607        } catch (SQLException e) {
608            getLogger().debug("Failed to store internal mail", e);
609            throw new IOException(e.getMessage());
610        } finally {
611            theJDBCUtil.closeJDBCConnection(conn);
612        }
613    }
614
615    /**
616     * @see org.apache.james.mailrepository.api.MailRepository#retrieve(String)
617     */
618    @SuppressWarnings("unchecked")
619    public Mail retrieve(String key) throws MessagingException {
620        if (DEEP_DEBUG) {
621            System.err.println("retrieving " + key);
622        }
623        Connection conn = null;
624        PreparedStatement retrieveMessage = null;
625        ResultSet rsMessage = null;
626        try {
627            conn = datasource.getConnection();
628            if (DEEP_DEBUG) {
629                System.err.println("got a conn " + key);
630            }
631
632            retrieveMessage = conn.prepareStatement(sqlQueries.getSqlString("retrieveMessageSQL", true));
633            retrieveMessage.setString(1, key);
634            retrieveMessage.setString(2, repositoryName);
635            rsMessage = retrieveMessage.executeQuery();
636            if (DEEP_DEBUG) {
637                System.err.println("ran the query " + key);
638            }
639            if (!rsMessage.next()) {
640                if (getLogger().isDebugEnabled()) {
641                    String debugBuffer = "Did not find a record " + key + " in " + repositoryName;
642                    getLogger().debug(debugBuffer);
643                }
644                return null;
645            }
646            // Determine whether attributes are used and retrieve them
647            PreparedStatement retrieveMessageAttr = null;
648            HashMap<String, Object> attributes = null;
649            if (jdbcMailAttributesReady) {
650                String retrieveMessageAttrSql = sqlQueries.getSqlString("retrieveMessageAttributesSQL", false);
651                ResultSet rsMessageAttr = null;
652                try {
653                    retrieveMessageAttr = conn.prepareStatement(retrieveMessageAttrSql);
654
655                    retrieveMessageAttr.setString(1, key);
656                    retrieveMessageAttr.setString(2, repositoryName);
657                    rsMessageAttr = retrieveMessageAttr.executeQuery();
658
659                    if (rsMessageAttr.next()) {
660                        try {
661                            byte[] serialized_attr;
662                            String getAttributesOption = sqlQueries.getDbOption("getAttributes");
663                            if (getAttributesOption != null && (getAttributesOption.equalsIgnoreCase("useBlob") || getAttributesOption.equalsIgnoreCase("useBinaryStream"))) {
664                                Blob b = rsMessageAttr.getBlob(1);
665                                serialized_attr = b.getBytes(1, (int) b.length());
666                            } else {
667                                serialized_attr = rsMessageAttr.getBytes(1);
668                            }
669                            // this check is for better backwards compatibility
670                            if (serialized_attr != null) {
671                                ByteArrayInputStream bais = new ByteArrayInputStream(serialized_attr);
672                                ObjectInputStream ois = new ObjectInputStream(bais);
673                                attributes = (HashMap<String, Object>) ois.readObject();
674                                ois.close();
675                            }
676                        } catch (IOException ioe) {
677                            if (getLogger().isDebugEnabled()) {
678                                String debugBuffer = "Exception reading attributes " + key + " in " + repositoryName;
679                                getLogger().debug(debugBuffer, ioe);
680                            }
681                        }
682                    } else {
683                        if (getLogger().isDebugEnabled()) {
684                            String debugBuffer = "Did not find a record (attributes) " + key + " in " + repositoryName;
685                            getLogger().debug(debugBuffer);
686                        }
687                    }
688                } catch (SQLException sqle) {
689                    String errorBuffer = "Error retrieving message" + sqle.getMessage() + sqle.getErrorCode() + sqle.getSQLState() + sqle.getNextException();
690                    getLogger().error(errorBuffer);
691                } finally {
692                    theJDBCUtil.closeJDBCResultSet(rsMessageAttr);
693                    theJDBCUtil.closeJDBCStatement(retrieveMessageAttr);
694                }
695            }
696
697            MailImpl mc = new MailImpl();
698            mc.setAttributesRaw(attributes);
699            mc.setName(key);
700            mc.setState(rsMessage.getString(1));
701            mc.setErrorMessage(rsMessage.getString(2));
702            String sender = rsMessage.getString(3);
703            if (sender == null) {
704                mc.setSender(null);
705            } else {
706                mc.setSender(new MailAddress(sender));
707            }
708            StringTokenizer st = new StringTokenizer(rsMessage.getString(4), "\r\n", false);
709            Set<MailAddress> recipients = new HashSet<MailAddress>();
710            while (st.hasMoreTokens()) {
711                recipients.add(new MailAddress(st.nextToken()));
712            }
713            mc.setRecipients(recipients);
714            mc.setRemoteHost(rsMessage.getString(5));
715            mc.setRemoteAddr(rsMessage.getString(6));
716            mc.setLastUpdated(rsMessage.getTimestamp(7));
717
718            MimeMessageJDBCSource source = new MimeMessageJDBCSource(this, key, sr);
719            MimeMessageCopyOnWriteProxy message = new MimeMessageCopyOnWriteProxy(source);
720            mc.setMessage(message);
721            return mc;
722        } catch (SQLException sqle) {
723            String errorBuffer = "Error retrieving message" + sqle.getMessage() + sqle.getErrorCode() + sqle.getSQLState() + sqle.getNextException();
724            getLogger().error(errorBuffer);
725            getLogger().debug("Failed to retrieve mail", sqle);
726            throw new MessagingException("Exception while retrieving mail: " + sqle.getMessage(), sqle);
727        } catch (Exception me) {
728            throw new MessagingException("Exception while retrieving mail: " + me.getMessage(), me);
729        } finally {
730            theJDBCUtil.closeJDBCResultSet(rsMessage);
731            theJDBCUtil.closeJDBCStatement(retrieveMessage);
732            theJDBCUtil.closeJDBCConnection(conn);
733        }
734    }
735
736    /**
737     * @see org.apache.james.mailrepository.lib.AbstractMailRepository#internalRemove(String)
738     */
739    protected void internalRemove(String key) throws MessagingException {
740        Connection conn = null;
741        PreparedStatement removeMessage = null;
742        try {
743            conn = datasource.getConnection();
744            removeMessage = conn.prepareStatement(sqlQueries.getSqlString("removeMessageSQL", true));
745            removeMessage.setString(1, key);
746            removeMessage.setString(2, repositoryName);
747            removeMessage.execute();
748
749            if (sr != null) {
750                sr.remove(key);
751            }
752        } catch (Exception me) {
753            throw new MessagingException("Exception while removing mail: " + me.getMessage(), me);
754        } finally {
755            theJDBCUtil.closeJDBCStatement(removeMessage);
756            theJDBCUtil.closeJDBCConnection(conn);
757        }
758    }
759
760    /**
761     * @see org.apache.james.mailrepository.api.MailRepository#list()
762     */
763    public Iterator<String> list() throws MessagingException {
764        // System.err.println("listing messages");
765        Connection conn = null;
766        PreparedStatement listMessages = null;
767        ResultSet rsListMessages = null;
768        try {
769            conn = datasource.getConnection();
770            listMessages = conn.prepareStatement(sqlQueries.getSqlString("listMessagesSQL", true));
771            listMessages.setString(1, repositoryName);
772            rsListMessages = listMessages.executeQuery();
773
774            List<String> messageList = new ArrayList<String>();
775            while (rsListMessages.next() && !Thread.currentThread().isInterrupted()) {
776                messageList.add(rsListMessages.getString(1));
777            }
778            return messageList.iterator();
779        } catch (Exception me) {
780            throw new MessagingException("Exception while listing mail: " + me.getMessage(), me);
781        } finally {
782            theJDBCUtil.closeJDBCResultSet(rsListMessages);
783            theJDBCUtil.closeJDBCStatement(listMessages);
784            theJDBCUtil.closeJDBCConnection(conn);
785        }
786    }
787
788    /**
789     * Gets the SQL connection to be used by this JDBCMailRepository
790     * 
791     * @return the connection
792     * @throws SQLException
793     *             if there is an issue with getting the connection
794     */
795    protected Connection getConnection() throws SQLException {
796        return datasource.getConnection();
797    }
798
799    /**
800     * @see java.lang.Object#equals(Object)
801     */
802    public boolean equals(Object obj) {
803        if (!(obj instanceof JDBCMailRepository)) {
804            return false;
805        }
806        // TODO: Figure out whether other instance variables should be part of
807        // the equals equation
808        JDBCMailRepository repository = (JDBCMailRepository) obj;
809        return ((repository.tableName.equals(tableName)) || ((repository.tableName != null) && repository.tableName.equals(tableName))) && ((repository.repositoryName.equals(repositoryName)) || ((repository.repositoryName != null) && repository.repositoryName.equals(repositoryName)));
810    }
811
812    /**
813     * Provide a hash code that is consistent with equals for this class
814     * 
815     * @return the hash code
816     */
817    public int hashCode() {
818        int result = 17;
819        if (tableName != null) {
820            result = 37 * tableName.hashCode();
821        }
822        if (repositoryName != null) {
823            result = 37 * repositoryName.hashCode();
824        }
825        return result;
826    }
827
828    /**
829     * This method calculates number of parameters in a prepared statement SQL
830     * String. It does so by counting the number of '?' in the string
831     * 
832     * @param sqlstring
833     *            to return parameter count for
834     * @return number of parameters
835     **/
836    private int getNumberOfParameters(String sqlstring) {
837        // it is alas a java 1.4 feature to be able to call
838        // getParameterMetaData which could provide us with the parameterCount
839        char[] chars = sqlstring.toCharArray();
840        int count = 0;
841        for (char aChar : chars) {
842            count += aChar == '?' ? 1 : 0;
843        }
844        return count;
845    }
846}