View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.geronimo.javamail.store.imap.connection;
19  
20  import java.util.ArrayList;
21  import java.util.Iterator;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.StringTokenizer;
25  
26  import javax.mail.MessagingException; 
27  import javax.mail.Session;
28  import javax.mail.Store;
29  
30  import javax.mail.StoreClosedException;
31  
32  import org.apache.geronimo.javamail.store.imap.IMAPStore; 
33  import org.apache.geronimo.javamail.util.ProtocolProperties; 
34  
35  public class IMAPConnectionPool {
36  
37      protected static final String MAIL_PORT = "port";
38      protected static final String MAIL_POOL_SIZE = "connectionpoolsize";
39      protected static final String MAIL_POOL_TIMEOUT = "connectionpooltimeout";
40      protected static final String MAIL_SEPARATE_STORE_CONNECTION = "separatestoreconnection";
41      
42      protected static final String MAIL_SASL_REALM = "sasl.realm"; 
43      protected static final String MAIL_AUTHORIZATIONID = "sasl.authorizationid"; 
44  
45      // 45 seconds, by default.
46      protected static final int DEFAULT_POOL_TIMEOUT = 45000;
47      protected static final String DEFAULT_MAIL_HOST = "localhost";
48      
49      protected static final int MAX_CONNECTION_RETRIES = 3; 
50      protected static final int MAX_POOL_WAIT = 500; 
51  
52  
53      // Our hosting Store instance
54      protected IMAPStore store;
55      // our Protocol abstraction 
56      protected ProtocolProperties props; 
57      // our list of created connections
58      protected List poolConnections = new ArrayList();
59      // our list of available connections 
60      protected List availableConnections = new ArrayList();
61      
62      // the dedicated Store connection (if we're configured that way)
63      protected IMAPConnection storeConnection = null;
64      
65      // our dedicated Store connection attribute
66      protected boolean dedicatedStoreConnection;
67      // the size of our connection pool (by default, we only keep a single connection in the pool)
68      protected int poolSize = 1;
69      // the connection timeout property
70      protected long poolTimeout;
71      // our debug flag
72      protected boolean debug;
73  
74      // the target host
75      protected String host;
76      // the target server port.
77      protected int port;
78      // the username we connect with
79      protected String username;
80      // the authentication password.
81      protected String password;
82      // the SASL realm name 
83      protected String realm; 
84      // the authorization id.  With IMAP, it's possible to 
85      // log on with another's authorization. 
86      protected String authid; 
87      // Turned on when the store is closed for business. 
88      protected boolean closed = false; 
89      // the connection capabilities map
90      protected Map capabilities; 
91  
92      /**
93       * Create a connection pool associated with a give IMAPStore instance.  The
94       * connection pool manages handing out connections for both the Store and
95       * Folder and Message usage.
96       * 
97       * Depending on the session properties, the Store may be given a dedicated
98       * connection, or will share connections with the Folders.  Connections may
99       * be requested from either the Store or Folders.  Messages must request
100      * their connections from their hosting Folder, and only one connection is
101      * allowed per folder.
102      * 
103      * @param store  The Store we're creating the pool for.
104      * @param props  The property bundle that defines protocol properties
105      *               that alter the connection behavior.
106      */
107     public IMAPConnectionPool(IMAPStore store, ProtocolProperties props) {
108         this.store = store;
109         this.props = props; 
110 
111         // get the pool size.  By default, we just use a single connection that's 
112         // shared among Store and all of the Folders.  Since most apps that use 
113         // javamail tend to be single-threaded, this generally poses no great hardship. 
114         poolSize = props.getIntProperty(MAIL_POOL_SIZE, 1);
115         // get the timeout property.  Default is 45 seconds.
116         poolTimeout = props.getIntProperty(MAIL_POOL_TIMEOUT, DEFAULT_POOL_TIMEOUT);
117         // we can create a dedicated connection over and above the pool set that's 
118         // reserved for the Store instance to use. 
119         dedicatedStoreConnection = props.getBooleanProperty(MAIL_SEPARATE_STORE_CONNECTION, false);
120         // if we have a dedicated pool connection, we allocated that from the pool.  Add this to 
121         // the total pool size so we don't find ourselves stuck if the pool size is 1. 
122         if (dedicatedStoreConnection) {
123             poolSize++; 
124         }
125     }
126 
127 
128     /**
129      * Manage the initial connection to the IMAP server.  This is the first 
130      * point where we obtain the information needed to make an actual server 
131      * connection.  Like the Store protocolConnect method, we return false 
132      * if there's any sort of authentication difficulties. 
133      * 
134      * @param host     The host of the IMAP server.
135      * @param port     The IMAP server connection port.
136      * @param user     The connection user name.
137      * @param password The connection password.
138      * 
139      * @return True if we were able to connect and authenticate correctly. 
140      * @exception MessagingException
141      */
142     public synchronized boolean protocolConnect(String host, int port, String username, String password) throws MessagingException {
143         // NOTE:  We don't check for the username/password being null at this point.  It's possible that 
144         // the server will send back a PREAUTH response, which means we don't need to go through login 
145         // processing.  We'll need to check the capabilities response after we make the connection to decide 
146         // if logging in is necesssary. 
147         
148         // save this for subsequent connections.  All pool connections will use this info.
149         // if the port is defaulted, then see if we have something configured in the session.
150         // if not configured, we just use the default default.
151         if (port == -1) {
152             // check for a property and fall back on the default if it's not set.
153             port = props.getIntProperty(MAIL_PORT, props.getDefaultPort());
154             // it's possible that -1 might have been explicitly set, so one last check. 
155             if (port == -1) {
156                 port = props.getDefaultPort(); 
157             }
158         }
159     	
160     	// Before we do anything, let's make sure that we succesfully received a host
161     	if ( host == null ) {
162     		host = DEFAULT_MAIL_HOST;
163     	}
164         
165         this.host = host;
166         this.port = port;
167         this.username = username;
168         this.password = password;
169         
170         // make sure we have the realm information 
171         realm = props.getProperty(MAIL_SASL_REALM); 
172         // get an authzid value, if we have one.  The default is to use the username.
173         authid = props.getProperty(MAIL_AUTHORIZATIONID, username);
174 
175         // go create a connection and just add it to the pool.  If there is an authenticaton error, 
176         // return the connect failure, and we may end up trying again. 
177         IMAPConnection connection = createPoolConnection(); 
178         if (connection == null) {
179             return false; 
180         }
181         // save the capabilities map from the first connection. 
182         capabilities = connection.getCapabilities(); 
183         // if we're using a dedicated store connection, remove this from the pool and
184         // reserve it for the store.
185         if (dedicatedStoreConnection)  
186         {  
187             storeConnection = connection;
188             // make sure this is hooked up to the store. 
189             connection.addResponseHandler(store); 
190         }
191         else {
192             // just put this back in the pool.  It's ready for anybody to use now. 
193             synchronized(this) {
194                 availableConnections.add(connection); 
195             }
196         }
197         // we're connection, authenticated, and ready to go. 
198         return true; 
199     }
200 
201     /**
202      * Creates an authenticated pool connection and adds it to
203      * the connection pool.  If there is an existing connection
204      * already in the pool, this returns without creating a new
205      * connection.
206      *
207      * @exception MessagingException
208      */
209     protected IMAPConnection createPoolConnection() throws MessagingException {
210         IMAPConnection connection = new IMAPConnection(props, this);
211         if (!connection.protocolConnect(host, port, authid, realm, username, password)) {
212             // we only add live connections to the pool.  Sever the connections and 
213             // allow it to go free. 
214             connection.closeServerConnection(); 
215             return null; 
216         }
217         
218         // add this to the master list.  We do NOT add this to the 
219         // available queue because we're handing this out. 
220         synchronized(this) {
221             // uh oh, we closed up shop while we were doing this...clean it up a 
222             // get out of here 
223             if (closed) {
224                 connection.close(); 
225                 throw new StoreClosedException(store, "No Store connections available"); 
226             }
227             
228             poolConnections.add(connection);
229         }
230         // return that connection 
231         return connection; 
232     }
233 
234 
235     /**
236      * Get a connection from the pool.  We try to retrieve a live
237      * connection, but we test the connection's liveness before
238      * returning one.  If we don't have a viable connection in
239      * the pool, we'll create a new one.  The returned connection
240      * will be in the authenticated state already.
241      *
242      * @return An IMAPConnection object that is connected to the server.
243      */
244     protected IMAPConnection getConnection() throws MessagingException {
245         int retryCount = 0; 
246         
247         // To keep us from falling into a futile failure loop, we'll only allow 
248         // a set number of connection failures. 
249         while (retryCount < MAX_CONNECTION_RETRIES) {
250             // first try for an already created one.  If this returns 
251             // null, then we'll probably have to make a new one. 
252             IMAPConnection connection = getPoolConnection(); 
253             // cool, we got one, the hard part is done.  
254             if (connection != null) {
255                 return connection; 
256             }
257             // ok, create a new one.  This *should* work, but the server might 
258             // have gone down, or other problem may occur. If we have a problem, 
259             // retry the entire process...but only for a bit.  No sense 
260             // being stubborn about it. 
261             connection = createPoolConnection(); 
262             if (connection != null) {
263                 return connection; 
264             }
265             // step the retry count 
266             retryCount++; 
267         }
268         
269         throw new MessagingException("Unable to get connection to IMAP server"); 
270     }
271     
272     /**
273      * Obtain a connection from the existing connection pool.  If none are 
274      * available, and we've reached the connection pool limit, we'll wait for 
275      * some other thread to return one.  It generally doesn't take too long, as 
276      * they're usually only held for the time required to execute a single 
277      * command.   If we're not at the pool limit, return null, which will signal 
278      * the caller to go ahead and create a new connection outside of the 
279      * lock. 
280      * 
281      * @return Either an active connection instance, or null if the caller should go 
282      *         ahead and try to create a new connection.
283      * @exception MessagingException
284      */
285     protected synchronized IMAPConnection getPoolConnection() throws MessagingException {
286         // if the pool is closed, we can't process this 
287         if (closed) {
288             throw new StoreClosedException(store, "No Store connections available"); 
289         }
290         
291         // we'll retry this a few times if the connection pool is full, but 
292         // after that, we'll just create a new connection. 
293         for (int i = 0; i < MAX_CONNECTION_RETRIES; i++) {
294             Iterator it = availableConnections.iterator(); 
295             while (it.hasNext()) {
296                 IMAPConnection connection = (IMAPConnection)it.next(); 
297                 // live or dead, we're going to remove this from the 
298                 // available list. 
299                 it.remove(); 
300                 if (connection.isAlive(poolTimeout)) {
301                     // return the connection to the requestor 
302                     return connection; 
303                 }
304                 else {
305                     // remove this from the pool...it's toast. 
306                     poolConnections.remove(connection); 
307                     // make sure this cleans up after itself. 
308                     connection.closeServerConnection(); 
309                 }
310             }
311 
312             // we've not found something usable in the pool.  Now see if 
313             // we're allowed to add another connection, or must just wait for 
314             // someone else to return one. 
315 
316             if (poolConnections.size() >= poolSize) {
317                 // check to see if we've been told to shutdown before waiting
318                 if (closed) {
319                     throw new StoreClosedException(store, "No Store connections available"); 
320                 }
321                 // we need to wait for somebody to return a connection 
322                 // once woken up, we'll spin around and try to snag one from 
323                 // the pool again.
324                 try {
325                     wait(MAX_POOL_WAIT);
326                 } catch (InterruptedException e) {
327                 }
328                 
329                 // check to see if we've been told to shutdown while we waited
330                 if (closed) {
331                     throw new StoreClosedException(store, "No Store connections available"); 
332                 }
333             }
334             else {
335                 // exit out and create a new connection.  Since 
336                 // we're going to be outside the synchronized block, it's possible 
337                 // we'll go over our pool limit.  We'll take care of that when connections start 
338                 // getting returned. 
339                 return null; 
340             }
341         }
342         // we've hit the maximum number of retries...just create a new connection. 
343         return null; 
344     }
345     
346     /**
347      * Return a connection to the connection pool.
348      * 
349      * @param connection The connection getting returned.
350      * 
351      * @exception MessagingException
352      */
353     protected void returnPoolConnection(IMAPConnection connection) throws MessagingException
354     {
355         synchronized(this) {
356             // If we're still within the bounds of our connection pool, 
357             // just add this to the active list and send out a notification 
358             // in case somebody else is waiting for the connection. 
359             if (availableConnections.size() < poolSize) {
360                 availableConnections.add(connection); 
361                 notify(); 
362                 return; 
363             }
364             // remove this from the connection pool...we have too many. 
365             poolConnections.remove(connection); 
366         }
367         // the additional cleanup occurs outside the synchronized block 
368         connection.close(); 
369     }
370     
371     /**
372      * Release a closed connection.
373      * 
374      * @param connection The connection getting released.
375      * 
376      * @exception MessagingException
377      */
378     protected void releasePoolConnection(IMAPConnection connection) throws MessagingException
379     {
380         synchronized(this) {
381             // remove this from the connection pool...it's no longer usable. 
382             poolConnections.remove(connection); 
383         }
384         // the additional cleanup occurs outside the synchronized block 
385         connection.close(); 
386     }
387 
388 
389     /**
390      * Get a connection for the Store.  This will be either a
391      * dedicated connection object, or one from the pool, depending
392      * on the mail.imap.separatestoreconnection property.
393      *
394      * @return An authenticated connection object.
395      */
396     public synchronized IMAPConnection getStoreConnection() throws MessagingException {  
397         if (closed) {
398             throw new StoreClosedException(store, "No Store connections available"); 
399         }
400         // if we have a dedicated connection created, return it.
401         if (storeConnection != null) {
402             return storeConnection;
403         }
404         else {
405             IMAPConnection connection = getConnection();
406             // add the store as a response handler while it has it. 
407             connection.addResponseHandler(store); 
408             return connection; 
409         }
410     }
411 
412 
413     /**
414      * Return the Store connection to the connection pool.  If we have a dedicated
415      * store connection, this is simple.  Otherwise, the connection goes back 
416      * into the general connection pool.
417      * 
418      * @param connection The connection getting returned.
419      */
420     public synchronized void releaseStoreConnection(IMAPConnection connection) throws MessagingException {
421         // have a server disconnect situation?
422         if (connection.isClosed()) {
423             // we no longer have a dedicated store connection.  
424             // we need to return to the pool from now on. 
425             storeConnection = null; 
426             // throw this away. 
427             releasePoolConnection(connection); 
428         }
429         else {
430             // if we have a dedicated connection, nothing to do really.  Otherwise, 
431             // return this connection to the pool. 
432             if (storeConnection == null) {
433                 // unhook the store from the connection. 
434                 connection.removeResponseHandler(store); 
435                 returnPoolConnection(connection); 
436             }
437         }
438     }
439 
440 
441     /**
442      * Get a connection for Folder.  
443      *
444      * @return An authenticated connection object.
445      */
446     public IMAPConnection getFolderConnection() throws MessagingException {  
447         // just get a connection from the pool 
448         return getConnection(); 
449     }
450 
451 
452     /**
453      * Return a Folder connection to the connection pool.  
454      * 
455      * @param connection The connection getting returned.
456      */
457     public void releaseFolderConnection(IMAPConnection connection) throws MessagingException {
458         // potentially, the server may have decided to shut us down.  
459         // In that case, the connection is no longer usable, so we need 
460         // to remove it from the list of available ones. 
461         if (!connection.isClosed()) {
462             // back into the pool with yee, matey....arrggghhh
463             returnPoolConnection(connection); 
464         }
465         else {
466             // can't return this one to the pool.  It's been stomped on 
467             releasePoolConnection(connection); 
468         }
469     }
470     
471     
472     /**
473      * Close the entire connection pool. 
474      * 
475      * @exception MessagingException
476      */
477     public synchronized void close() throws MessagingException {
478         // first close each of the connections.  This also closes the 
479         // store connection. 
480         for (int i = 0; i < poolConnections.size(); i++) {
481             IMAPConnection connection = (IMAPConnection)poolConnections.get(i);
482             connection.close(); 
483         }
484         // clear the pool 
485         poolConnections.clear(); 
486         availableConnections.clear(); 
487         storeConnection = null; 
488         // turn out the lights, hang the closed sign on the wall. 
489         closed = true; 
490     }
491 
492 
493     /**
494      * Flush any connections from the pool that have not been used
495      * for at least the connection pool timeout interval.
496      */
497     protected synchronized void closeStaleConnections() {
498         Iterator i = poolConnections.iterator();
499 
500         while (i.hasNext()) {
501             IMAPConnection connection = (IMAPConnection)i.next();
502             // if this connection is a stale one, remove it from the pool
503             // and close it out.
504             if (connection.isStale(poolTimeout)) {
505                 i.remove();
506                 try {
507                     connection.close();
508                 } catch (MessagingException e) {
509                     // ignored.  we're just closing connections that are probably timed out anyway, so errors
510                     // on those shouldn't have an effect on the real operation we're dealing with.
511                 }
512             }
513         }
514     }
515 
516 
517     /**
518      * Return a connection back to the connection pool.  If we're not
519      * over our limit, the connection is kept around.  Otherwise, it's
520      * given a nice burial.
521      *
522      * @param connection The returned connection.
523      */
524     protected synchronized void releaseConnection(IMAPConnection connection) {
525         // before adding this to the pool, close any stale connections we may
526         // have.  The connection we're adding is quite likely to be a fresh one,
527         // so we should cache that one if we can.
528         closeStaleConnections();
529         // still over the limit?
530         if (poolConnections.size() + 1 > poolSize) {
531             try {
532                 // close this out and forget we ever saw it.
533                 connection.close();
534             } catch (MessagingException e) {
535                 // ignore....this is a non-critical problem if this fails now.
536             }
537         }
538         else {
539             // listen to alerts on this connection, and put it back in the pool.
540             poolConnections.add(connection);
541         }
542     }
543 
544     /**
545      * Cleanup time.  Sever and cleanup all of the pool connection
546      * objects, including the special Store connection, if we have one.
547      */
548     protected synchronized void freeAllConnections() {
549         for (int i = 0; i < poolConnections.size(); i++) {
550             IMAPConnection connection = (IMAPConnection)poolConnections.get(i);
551             try {
552                 // close this out and forget we ever saw it.
553                 connection.close();
554             } catch (MessagingException e) {
555                 // ignore....this is a non-critical problem if this fails now.
556             }
557         }
558         // everybody, out of the pool!
559         poolConnections.clear();
560 
561         // don't forget the special store connection, if we have one.
562         if (storeConnection != null) {
563             try {
564                 // close this out and forget we ever saw it.
565                 storeConnection.close();
566             } catch (MessagingException e) {
567                 // ignore....this is a non-critical problem if this fails now.
568             }
569             storeConnection = null;
570         }
571     }
572     
573     
574     /**
575      * Test if this connection has a given capability. 
576      * 
577      * @param capability The capability name.
578      * 
579      * @return true if this capability is in the list, false for a mismatch. 
580      */
581     public boolean hasCapability(String capability) {
582         if (capabilities == null) {
583             return false; 
584         }
585         return capabilities.containsKey(capability); 
586     }
587 }
588 
589