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