blob: f89619c070f0820ffe9266969cab028e1fe27595 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.geronimo.javamail.store.imap.connection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.StoreClosedException;
import org.apache.geronimo.javamail.store.imap.IMAPStore;
import org.apache.geronimo.javamail.util.ProtocolProperties;
public class IMAPConnectionPool {
protected static final String MAIL_PORT = "port";
protected static final String MAIL_POOL_SIZE = "connectionpoolsize";
protected static final String MAIL_POOL_TIMEOUT = "connectionpooltimeout";
protected static final String MAIL_SEPARATE_STORE_CONNECTION = "separatestoreconnection";
protected static final String MAIL_SASL_REALM = "sasl.realm";
protected static final String MAIL_AUTHORIZATIONID = "sasl.authorizationid";
// 45 seconds, by default.
protected static final int DEFAULT_POOL_TIMEOUT = 45000;
protected static final String DEFAULT_MAIL_HOST = "localhost";
protected static final int MAX_CONNECTION_RETRIES = 3;
protected static final int MAX_POOL_WAIT = 500;
// Our hosting Store instance
protected IMAPStore store;
// our Protocol abstraction
protected ProtocolProperties props;
// our list of created connections
protected List poolConnections = new ArrayList();
// our list of available connections
protected List availableConnections = new ArrayList();
// the dedicated Store connection (if we're configured that way)
protected IMAPConnection storeConnection = null;
// our dedicated Store connection attribute
protected boolean dedicatedStoreConnection;
// the size of our connection pool (by default, we only keep a single connection in the pool)
protected int poolSize = 1;
// the connection timeout property
protected long poolTimeout;
// our debug flag
protected boolean debug;
// the target host
protected String host;
// the target server port.
protected int port;
// the username we connect with
protected String username;
// the authentication password.
protected String password;
// the SASL realm name
protected String realm;
// the authorization id. With IMAP, it's possible to
// log on with another's authorization.
protected String authid;
// Turned on when the store is closed for business.
protected boolean closed = false;
// the connection capabilities map
protected Map capabilities;
/**
* Create a connection pool associated with a give IMAPStore instance. The
* connection pool manages handing out connections for both the Store and
* Folder and Message usage.
*
* Depending on the session properties, the Store may be given a dedicated
* connection, or will share connections with the Folders. Connections may
* be requested from either the Store or Folders. Messages must request
* their connections from their hosting Folder, and only one connection is
* allowed per folder.
*
* @param store The Store we're creating the pool for.
* @param props The property bundle that defines protocol properties
* that alter the connection behavior.
*/
public IMAPConnectionPool(IMAPStore store, ProtocolProperties props) {
this.store = store;
this.props = props;
// get the pool size. By default, we just use a single connection that's
// shared among Store and all of the Folders. Since most apps that use
// javamail tend to be single-threaded, this generally poses no great hardship.
poolSize = props.getIntProperty(MAIL_POOL_SIZE, 1);
// get the timeout property. Default is 45 seconds.
poolTimeout = props.getIntProperty(MAIL_POOL_TIMEOUT, DEFAULT_POOL_TIMEOUT);
// we can create a dedicated connection over and above the pool set that's
// reserved for the Store instance to use.
dedicatedStoreConnection = props.getBooleanProperty(MAIL_SEPARATE_STORE_CONNECTION, false);
// if we have a dedicated pool connection, we allocated that from the pool. Add this to
// the total pool size so we don't find ourselves stuck if the pool size is 1.
if (dedicatedStoreConnection) {
poolSize++;
}
}
/**
* Manage the initial connection to the IMAP server. This is the first
* point where we obtain the information needed to make an actual server
* connection. Like the Store protocolConnect method, we return false
* if there's any sort of authentication difficulties.
*
* @param host The host of the IMAP server.
* @param port The IMAP server connection port.
* @param user The connection user name.
* @param password The connection password.
*
* @return True if we were able to connect and authenticate correctly.
* @exception MessagingException
*/
public synchronized boolean protocolConnect(String host, int port, String username, String password) throws MessagingException {
// NOTE: We don't check for the username/password being null at this point. It's possible that
// the server will send back a PREAUTH response, which means we don't need to go through login
// processing. We'll need to check the capabilities response after we make the connection to decide
// if logging in is necesssary.
// save this for subsequent connections. All pool connections will use this info.
// if the port is defaulted, then see if we have something configured in the session.
// if not configured, we just use the default default.
if (port == -1) {
// check for a property and fall back on the default if it's not set.
port = props.getIntProperty(MAIL_PORT, props.getDefaultPort());
// it's possible that -1 might have been explicitly set, so one last check.
if (port == -1) {
port = props.getDefaultPort();
}
}
// Before we do anything, let's make sure that we succesfully received a host
if ( host == null ) {
host = DEFAULT_MAIL_HOST;
}
this.host = host;
this.port = port;
this.username = username;
this.password = password;
// make sure we have the realm information
realm = props.getProperty(MAIL_SASL_REALM);
// get an authzid value, if we have one. The default is to use the username.
authid = props.getProperty(MAIL_AUTHORIZATIONID, username);
// go create a connection and just add it to the pool. If there is an authenticaton error,
// return the connect failure, and we may end up trying again.
IMAPConnection connection = createPoolConnection();
if (connection == null) {
return false;
}
// save the capabilities map from the first connection.
capabilities = connection.getCapabilities();
// if we're using a dedicated store connection, remove this from the pool and
// reserve it for the store.
if (dedicatedStoreConnection)
{
storeConnection = connection;
// make sure this is hooked up to the store.
connection.addResponseHandler(store);
}
else {
// just put this back in the pool. It's ready for anybody to use now.
synchronized(this) {
availableConnections.add(connection);
}
}
// we're connection, authenticated, and ready to go.
return true;
}
/**
* Creates an authenticated pool connection and adds it to
* the connection pool. If there is an existing connection
* already in the pool, this returns without creating a new
* connection.
*
* @exception MessagingException
*/
protected IMAPConnection createPoolConnection() throws MessagingException {
IMAPConnection connection = new IMAPConnection(props, this);
if (!connection.protocolConnect(host, port, authid, realm, username, password)) {
// we only add live connections to the pool. Sever the connections and
// allow it to go free.
connection.closeServerConnection();
return null;
}
// add this to the master list. We do NOT add this to the
// available queue because we're handing this out.
synchronized(this) {
// uh oh, we closed up shop while we were doing this...clean it up a
// get out of here
if (closed) {
connection.close();
throw new StoreClosedException(store, "No Store connections available");
}
poolConnections.add(connection);
}
// return that connection
return connection;
}
/**
* Get a connection from the pool. We try to retrieve a live
* connection, but we test the connection's liveness before
* returning one. If we don't have a viable connection in
* the pool, we'll create a new one. The returned connection
* will be in the authenticated state already.
*
* @return An IMAPConnection object that is connected to the server.
*/
protected IMAPConnection getConnection() throws MessagingException {
int retryCount = 0;
// To keep us from falling into a futile failure loop, we'll only allow
// a set number of connection failures.
while (retryCount < MAX_CONNECTION_RETRIES) {
// first try for an already created one. If this returns
// null, then we'll probably have to make a new one.
IMAPConnection connection = getPoolConnection();
// cool, we got one, the hard part is done.
if (connection != null) {
return connection;
}
// ok, create a new one. This *should* work, but the server might
// have gone down, or other problem may occur. If we have a problem,
// retry the entire process...but only for a bit. No sense
// being stubborn about it.
connection = createPoolConnection();
if (connection != null) {
return connection;
}
// step the retry count
retryCount++;
}
throw new MessagingException("Unable to get connection to IMAP server");
}
/**
* Obtain a connection from the existing connection pool. If none are
* available, and we've reached the connection pool limit, we'll wait for
* some other thread to return one. It generally doesn't take too long, as
* they're usually only held for the time required to execute a single
* command. If we're not at the pool limit, return null, which will signal
* the caller to go ahead and create a new connection outside of the
* lock.
*
* @return Either an active connection instance, or null if the caller should go
* ahead and try to create a new connection.
* @exception MessagingException
*/
protected synchronized IMAPConnection getPoolConnection() throws MessagingException {
// if the pool is closed, we can't process this
if (closed) {
throw new StoreClosedException(store, "No Store connections available");
}
// we'll retry this a few times if the connection pool is full, but
// after that, we'll just create a new connection.
for (int i = 0; i < MAX_CONNECTION_RETRIES; i++) {
Iterator it = availableConnections.iterator();
while (it.hasNext()) {
IMAPConnection connection = (IMAPConnection)it.next();
// live or dead, we're going to remove this from the
// available list.
it.remove();
if (connection.isAlive(poolTimeout)) {
// return the connection to the requestor
return connection;
}
else {
// remove this from the pool...it's toast.
poolConnections.remove(connection);
// make sure this cleans up after itself.
connection.closeServerConnection();
}
}
// we've not found something usable in the pool. Now see if
// we're allowed to add another connection, or must just wait for
// someone else to return one.
if (poolConnections.size() >= poolSize) {
// check to see if we've been told to shutdown before waiting
if (closed) {
throw new StoreClosedException(store, "No Store connections available");
}
// we need to wait for somebody to return a connection
// once woken up, we'll spin around and try to snag one from
// the pool again.
try {
wait(MAX_POOL_WAIT);
} catch (InterruptedException e) {
}
// check to see if we've been told to shutdown while we waited
if (closed) {
throw new StoreClosedException(store, "No Store connections available");
}
}
else {
// exit out and create a new connection. Since
// we're going to be outside the synchronized block, it's possible
// we'll go over our pool limit. We'll take care of that when connections start
// getting returned.
return null;
}
}
// we've hit the maximum number of retries...just create a new connection.
return null;
}
/**
* Return a connection to the connection pool.
*
* @param connection The connection getting returned.
*
* @exception MessagingException
*/
protected void returnPoolConnection(IMAPConnection connection) throws MessagingException
{
synchronized(this) {
// If we're still within the bounds of our connection pool,
// just add this to the active list and send out a notification
// in case somebody else is waiting for the connection.
if (availableConnections.size() < poolSize) {
availableConnections.add(connection);
notify();
return;
}
// remove this from the connection pool...we have too many.
poolConnections.remove(connection);
}
// the additional cleanup occurs outside the synchronized block
connection.close();
}
/**
* Release a closed connection.
*
* @param connection The connection getting released.
*
* @exception MessagingException
*/
protected void releasePoolConnection(IMAPConnection connection) throws MessagingException
{
synchronized(this) {
// remove this from the connection pool...it's no longer usable.
poolConnections.remove(connection);
}
// the additional cleanup occurs outside the synchronized block
connection.close();
}
/**
* Get a connection for the Store. This will be either a
* dedicated connection object, or one from the pool, depending
* on the mail.imap.separatestoreconnection property.
*
* @return An authenticated connection object.
*/
public synchronized IMAPConnection getStoreConnection() throws MessagingException {
if (closed) {
throw new StoreClosedException(store, "No Store connections available");
}
// if we have a dedicated connection created, return it.
if (storeConnection != null) {
return storeConnection;
}
else {
IMAPConnection connection = getConnection();
// add the store as a response handler while it has it.
connection.addResponseHandler(store);
return connection;
}
}
/**
* Return the Store connection to the connection pool. If we have a dedicated
* store connection, this is simple. Otherwise, the connection goes back
* into the general connection pool.
*
* @param connection The connection getting returned.
*/
public synchronized void releaseStoreConnection(IMAPConnection connection) throws MessagingException {
// have a server disconnect situation?
if (connection.isClosed()) {
// we no longer have a dedicated store connection.
// we need to return to the pool from now on.
storeConnection = null;
// throw this away.
releasePoolConnection(connection);
}
else {
// if we have a dedicated connection, nothing to do really. Otherwise,
// return this connection to the pool.
if (storeConnection == null) {
// unhook the store from the connection.
connection.removeResponseHandler(store);
returnPoolConnection(connection);
}
}
}
/**
* Get a connection for Folder.
*
* @return An authenticated connection object.
*/
public IMAPConnection getFolderConnection() throws MessagingException {
// just get a connection from the pool
return getConnection();
}
/**
* Return a Folder connection to the connection pool.
*
* @param connection The connection getting returned.
*/
public void releaseFolderConnection(IMAPConnection connection) throws MessagingException {
// potentially, the server may have decided to shut us down.
// In that case, the connection is no longer usable, so we need
// to remove it from the list of available ones.
if (!connection.isClosed()) {
// back into the pool with yee, matey....arrggghhh
returnPoolConnection(connection);
}
else {
// can't return this one to the pool. It's been stomped on
releasePoolConnection(connection);
}
}
/**
* Close the entire connection pool.
*
* @exception MessagingException
*/
public synchronized void close() throws MessagingException {
// first close each of the connections. This also closes the
// store connection.
for (int i = 0; i < poolConnections.size(); i++) {
IMAPConnection connection = (IMAPConnection)poolConnections.get(i);
connection.close();
}
// clear the pool
poolConnections.clear();
availableConnections.clear();
storeConnection = null;
// turn out the lights, hang the closed sign on the wall.
closed = true;
}
/**
* Flush any connections from the pool that have not been used
* for at least the connection pool timeout interval.
*/
protected synchronized void closeStaleConnections() {
Iterator i = poolConnections.iterator();
while (i.hasNext()) {
IMAPConnection connection = (IMAPConnection)i.next();
// if this connection is a stale one, remove it from the pool
// and close it out.
if (connection.isStale(poolTimeout)) {
i.remove();
try {
connection.close();
} catch (MessagingException e) {
// ignored. we're just closing connections that are probably timed out anyway, so errors
// on those shouldn't have an effect on the real operation we're dealing with.
}
}
}
}
/**
* Return a connection back to the connection pool. If we're not
* over our limit, the connection is kept around. Otherwise, it's
* given a nice burial.
*
* @param connection The returned connection.
*/
protected synchronized void releaseConnection(IMAPConnection connection) {
// before adding this to the pool, close any stale connections we may
// have. The connection we're adding is quite likely to be a fresh one,
// so we should cache that one if we can.
closeStaleConnections();
// still over the limit?
if (poolConnections.size() + 1 > poolSize) {
try {
// close this out and forget we ever saw it.
connection.close();
} catch (MessagingException e) {
// ignore....this is a non-critical problem if this fails now.
}
}
else {
// listen to alerts on this connection, and put it back in the pool.
poolConnections.add(connection);
}
}
/**
* Cleanup time. Sever and cleanup all of the pool connection
* objects, including the special Store connection, if we have one.
*/
protected synchronized void freeAllConnections() {
for (int i = 0; i < poolConnections.size(); i++) {
IMAPConnection connection = (IMAPConnection)poolConnections.get(i);
try {
// close this out and forget we ever saw it.
connection.close();
} catch (MessagingException e) {
// ignore....this is a non-critical problem if this fails now.
}
}
// everybody, out of the pool!
poolConnections.clear();
// don't forget the special store connection, if we have one.
if (storeConnection != null) {
try {
// close this out and forget we ever saw it.
storeConnection.close();
} catch (MessagingException e) {
// ignore....this is a non-critical problem if this fails now.
}
storeConnection = null;
}
}
/**
* Test if this connection has a given capability.
*
* @param capability The capability name.
*
* @return true if this capability is in the list, false for a mismatch.
*/
public boolean hasCapability(String capability) {
if (capabilities == null) {
return false;
}
return capabilities.containsKey(capability);
}
}