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 org.apache.james.core.MimeMessageSource;
023import org.apache.james.repository.api.StreamRepository;
024import org.apache.james.util.sql.JDBCUtil;
025
026import java.io.ByteArrayInputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.SequenceInputStream;
030import java.sql.Blob;
031import java.sql.Connection;
032import java.sql.PreparedStatement;
033import java.sql.ResultSet;
034import java.sql.SQLException;
035
036/**
037 * This class points to a specific message in a repository. This will return an
038 * InputStream to the JDBC field/record, possibly sequenced with the file
039 * stream.
040 */
041public class MimeMessageJDBCSource extends MimeMessageSource {
042
043    /**
044     * Whether 'deep debugging' is turned on.
045     */
046    private static final boolean DEEP_DEBUG = false;
047
048    // Define how to get to the data
049    JDBCMailRepository repository = null;
050    String key = null;
051    StreamRepository sr = null;
052
053    private long size = -1;
054
055    /**
056     * SQL used to retrieve the message body
057     */
058    String retrieveMessageBodySQL = null;
059
060    /**
061     * SQL used to retrieve the size of the message body
062     */
063    String retrieveMessageBodySizeSQL = null;
064
065    /**
066     * The JDBCUtil helper class
067     */
068    private static final JDBCUtil theJDBCUtil = new JDBCUtil() {
069        protected void delegatedLog(String logString) {
070            // No logging available at this point in the code.
071            // Therefore this is a noop method.
072        }
073    };
074
075    /**
076     * Construct a MimeMessageSource based on a JDBC repository, a key, and a
077     * stream repository (where we might store the message body)
078     * 
079     * @param repository
080     *            the JDBCMailRepository to use
081     * @param key
082     *            the key for the particular stream in the stream repository to
083     *            be used by this data source.
084     * @param sr
085     *            the stream repository used by this data source.
086     * @throws IOException
087     *             get thrown if an IO error detected
088     */
089    public MimeMessageJDBCSource(JDBCMailRepository repository, String key, StreamRepository sr) throws IOException {
090        super();
091
092        if (repository == null) {
093            throw new IOException("Repository is null");
094        }
095        if (key == null) {
096            throw new IOException("Message name (key) was not defined");
097        }
098        this.repository = repository;
099        this.key = key;
100        this.sr = sr;
101
102        retrieveMessageBodySQL = repository.sqlQueries.getSqlString("retrieveMessageBodySQL", true);
103        // this is optional
104        retrieveMessageBodySizeSQL = repository.sqlQueries.getSqlString("retrieveMessageBodySizeSQL");
105
106    }
107
108    /**
109     * Returns a unique String ID that represents the location from where this
110     * source is loaded. This will be used to identify where the data is,
111     * primarily to avoid situations where this data would get overwritten.
112     * 
113     * @return the String ID
114     */
115    public String getSourceId() {
116        return repository.repositoryName + "/" + key;
117    }
118
119    /**
120     * Return the input stream to the database field and then the file stream.
121     * This should be smart enough to work even if the file does not exist. This
122     * is to support a repository with the entire message in the database, which
123     * is how James 1.2 worked.
124     * 
125     * @see org.apache.james.core.MimeMessageSource#getInputStream()
126     */
127    public synchronized InputStream getInputStream() throws IOException {
128        Connection conn = null;
129        PreparedStatement retrieveMessageStream = null;
130        ResultSet rsRetrieveMessageStream = null;
131        try {
132            conn = repository.getConnection();
133
134            byte[] headers;
135
136            long start = 0;
137            if (DEEP_DEBUG) {
138                start = System.currentTimeMillis();
139                System.out.println("starting");
140            }
141            retrieveMessageStream = conn.prepareStatement(retrieveMessageBodySQL);
142            retrieveMessageStream.setString(1, key);
143            retrieveMessageStream.setString(2, repository.repositoryName);
144            rsRetrieveMessageStream = retrieveMessageStream.executeQuery();
145
146            if (!rsRetrieveMessageStream.next()) {
147                throw new IOException("Could not find message");
148            }
149
150            String getBodyOption = repository.sqlQueries.getDbOption("getBody");
151            if (getBodyOption != null && getBodyOption.equalsIgnoreCase("useBlob")) {
152                Blob b = rsRetrieveMessageStream.getBlob(1);
153                headers = b.getBytes(1, (int) b.length());
154            } else {
155                headers = rsRetrieveMessageStream.getBytes(1);
156            }
157            if (DEEP_DEBUG) {
158                System.err.println("stopping");
159                System.err.println(System.currentTimeMillis() - start);
160            }
161
162            InputStream in = new ByteArrayInputStream(headers);
163            try {
164                if (sr != null) {
165                    in = new SequenceInputStream(in, sr.get(key));
166                }
167            } catch (Exception e) {
168                // ignore this... either sr is null, or the file does not exist
169                // or something else
170            }
171            return in;
172        } catch (SQLException sqle) {
173            throw new IOException(sqle.toString());
174        } finally {
175            theJDBCUtil.closeJDBCResultSet(rsRetrieveMessageStream);
176            theJDBCUtil.closeJDBCStatement(retrieveMessageStream);
177            theJDBCUtil.closeJDBCConnection(conn);
178        }
179    }
180
181    /**
182     * Runs a custom SQL statement to check the size of the message body
183     * 
184     * @see org.apache.james.core.MimeMessageSource#getMessageSize()
185     */
186    public synchronized long getMessageSize() throws IOException {
187        if (size != -1)
188            return size;
189        if (retrieveMessageBodySizeSQL == null) {
190            // There was no SQL statement for this repository... figure it out
191            // the hard way
192            System.err.println("no SQL statement to find size");
193            return size = super.getMessageSize();
194        }
195        Connection conn = null;
196        PreparedStatement retrieveMessageSize = null;
197        ResultSet rsRetrieveMessageSize = null;
198        try {
199            conn = repository.getConnection();
200
201            retrieveMessageSize = conn.prepareStatement(retrieveMessageBodySizeSQL);
202            retrieveMessageSize.setString(1, key);
203            retrieveMessageSize.setString(2, repository.repositoryName);
204            rsRetrieveMessageSize = retrieveMessageSize.executeQuery();
205
206            if (!rsRetrieveMessageSize.next()) {
207                throw new IOException("Could not find message");
208            }
209
210            size = rsRetrieveMessageSize.getLong(1);
211
212            InputStream in = null;
213            try {
214                if (sr != null) {
215                    if (sr instanceof org.apache.james.repository.file.FilePersistentStreamRepository) {
216                        size += ((org.apache.james.repository.file.FilePersistentStreamRepository) sr).getSize(key);
217                    } else {
218                        in = sr.get(key);
219                        int len;
220                        byte[] block = new byte[1024];
221                        while ((len = in.read(block)) > -1) {
222                            size += len;
223                        }
224                    }
225                }
226            } catch (Exception e) {
227                // ignore this... either sr is null, or the file does not exist
228                // or something else
229            } finally {
230                try {
231                    if (in != null) {
232                        in.close();
233                    }
234                } catch (IOException ioe) {
235                    // Ignored - no access to logger at this point in the code
236                }
237            }
238
239            return size;
240        } catch (SQLException sqle) {
241            throw new IOException(sqle.toString());
242        } finally {
243            theJDBCUtil.closeJDBCResultSet(rsRetrieveMessageSize);
244            theJDBCUtil.closeJDBCStatement(retrieveMessageSize);
245            theJDBCUtil.closeJDBCConnection(conn);
246        }
247    }
248
249    /**
250     * Check to see whether this is the same repository and the same key
251     */
252    public boolean equals(Object obj) {
253        if (obj instanceof MimeMessageJDBCSource) {
254            // TODO: Figure out whether other instance variables should be part
255            // of
256            // the equals equation
257            MimeMessageJDBCSource source = (MimeMessageJDBCSource) obj;
258            return ((source.key.equals(key)) || ((source.key != null) && source.key.equals(key))) && ((source.repository == repository) || ((source.repository != null) && source.repository.equals(repository)));
259        }
260        return false;
261    }
262
263    /**
264     * Provide a hash code that is consistent with equals for this class
265     * 
266     * @return the hash code
267     */
268    public int hashCode() {
269        int result = 17;
270        if (key != null) {
271            result = 37 * key.hashCode();
272        }
273        if (repository != null) {
274            result = 37 * repository.hashCode();
275        }
276        return result;
277    }
278
279}