Implement an ExchangeSession pool (needed with LdapServer)

git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@196 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
mguessan 2008-12-04 11:50:31 +00:00
parent 9b5feac91f
commit 24362b8632
5 changed files with 181 additions and 45 deletions

View File

@ -1,6 +1,7 @@
package davmail; package davmail;
import davmail.exchange.ExchangeSession; import davmail.exchange.ExchangeSession;
import davmail.exchange.ExchangeSessionFactory;
import davmail.smtp.SmtpConnection; import davmail.smtp.SmtpConnection;
import davmail.tray.DavGatewayTray; import davmail.tray.DavGatewayTray;
import org.apache.commons.httpclient.util.Base64; import org.apache.commons.httpclient.util.Base64;
@ -142,13 +143,7 @@ public class AbstractConnection extends Thread {
} catch (IOException e2) { } catch (IOException e2) {
DavGatewayTray.warn("Exception closing client socket", e2); DavGatewayTray.warn("Exception closing client socket", e2);
} }
try { ExchangeSessionFactory.close(session);
if (session != null) {
session.close();
}
} catch (IOException e3) {
DavGatewayTray.warn("Exception closing gateway", e3);
}
} }
protected String base64Encode(String value) { protected String base64Encode(String value) {

View File

@ -64,11 +64,20 @@ public class ExchangeSession {
private String currentFolderUrl; private String currentFolderUrl;
private WebdavResource wdr = null; private WebdavResource wdr = null;
private ExchangeSessionFactory.PoolKey poolKey;
ExchangeSessionFactory.PoolKey getPoolKey() {
return poolKey;
}
/** /**
* Create an exchange session for the given URL. * Create an exchange session for the given URL.
* The session is not actually established until a call to login() * The session is not actually established until a call to login()
*
* @param poolKey session pool key
*/ */
ExchangeSession() { ExchangeSession(ExchangeSessionFactory.PoolKey poolKey) {
this.poolKey = poolKey;
// SimpleDateFormat are not thread safe, need to create one instance for // SimpleDateFormat are not thread safe, need to create one instance for
// each session // each session
dateParser = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); dateParser = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
@ -76,6 +85,31 @@ public class ExchangeSession {
LOGGER.debug("Session " + this + " created"); LOGGER.debug("Session " + this + " created");
} }
public boolean isExpired() {
boolean isExpired = false;
HttpMethod testMethod = null;
try {
testMethod = DavGatewayHttpClientFacade.executeFollowRedirects(wdr.retrieveSessionInstance(),
URIUtil.encodePath(currentFolderUrl));
String queryString = testMethod.getQueryString();
if (testMethod.getStatusCode() != HttpStatus.SC_OK) {
isExpired = true;
} else if (queryString != null && queryString.contains("reason=")) {
isExpired = true;
}
} catch (IOException e) {
isExpired = true;
} finally {
if (testMethod != null) {
testMethod.releaseConnection();
}
}
return isExpired;
}
/** /**
* Test authentication mode : form based or basic. * Test authentication mode : form based or basic.
* *
@ -197,25 +231,23 @@ public class ExchangeSession {
return result; return result;
} }
void login(String userName, String password) throws IOException { void login() throws IOException {
LOGGER.debug("Session " + this + " login"); LOGGER.debug("Session " + this + " login");
try { try {
String baseUrl = Settings.getProperty("davmail.url"); boolean isBasicAuthentication = isBasicAuthentication(poolKey.url);
boolean isBasicAuthentication = isBasicAuthentication(baseUrl);
// get proxy configuration from setttings properties // get proxy configuration from setttings properties
URL urlObject = new URL(baseUrl); URL urlObject = new URL(poolKey.url);
// webdavresource is unable to create the correct url type // webdavresource is unable to create the correct url type
HttpURL httpURL; HttpURL httpURL;
if (baseUrl.startsWith("http://")) { if (poolKey.url.startsWith("http://")) {
httpURL = new HttpURL(userName, password, httpURL = new HttpURL(poolKey.userName, poolKey.password,
urlObject.getHost(), urlObject.getPort()); urlObject.getHost(), urlObject.getPort());
} else if (baseUrl.startsWith("https://")) { } else if (poolKey.url.startsWith("https://")) {
httpURL = new HttpsURL(userName, password, httpURL = new HttpsURL(poolKey.userName, poolKey.password,
urlObject.getHost(), urlObject.getPort()); urlObject.getHost(), urlObject.getPort());
} else { } else {
throw new IllegalArgumentException("Invalid URL: " + baseUrl); throw new IllegalArgumentException("Invalid URL: " + poolKey.url);
} }
wdr = new WebdavResource(httpURL, WebdavResource.NOACTION, 0); wdr = new WebdavResource(httpURL, WebdavResource.NOACTION, 0);
@ -230,10 +262,10 @@ public class ExchangeSession {
// get webmail root url // get webmail root url
// providing credentials // providing credentials
// manually follow redirect // manually follow redirect
HttpMethod method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, baseUrl); HttpMethod method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, poolKey.url);
if (!isBasicAuthentication) { if (!isBasicAuthentication) {
method = formLogin(httpClient, method, userName, password); method = formLogin(httpClient, method, poolKey.userName, poolKey.password);
// reexecute method with new base URL // reexecute method with new base URL
// method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, baseUrl); // method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, baseUrl);
} }
@ -250,7 +282,7 @@ public class ExchangeSession {
String queryString = method.getQueryString(); String queryString = method.getQueryString();
if (queryString != null && queryString.contains("reason=2")) { if (queryString != null && queryString.contains("reason=2")) {
method.releaseConnection(); method.releaseConnection();
if (userName != null && userName.contains("\\")) { if (poolKey.userName != null && poolKey.userName.contains("\\")) {
throw new HttpException("Authentication failed: invalid user or password"); throw new HttpException("Authentication failed: invalid user or password");
} else { } else {
throw new HttpException("Authentication failed: invalid user or password, " + throw new HttpException("Authentication failed: invalid user or password, " +
@ -261,7 +293,7 @@ public class ExchangeSession {
mailPath = getMailPath(method); mailPath = getMailPath(method);
if (mailPath == null) { if (mailPath == null) {
throw new HttpException(baseUrl + " not found in body, authentication failed: password expired ?"); throw new HttpException(poolKey.url + " not found in body, authentication failed: password expired ?");
} }
// got base http mailbox http url // got base http mailbox http url
@ -343,12 +375,14 @@ public class ExchangeSession {
/** /**
* Close session. * Close session.
* This will only close http client, not the actual Exchange session * This will only close http client, not the actual Exchange session
*
* @throws IOException if unable to close Webdav context
*/ */
public void close() throws IOException { void close() {
LOGGER.debug("Session " + this + " closed"); try {
wdr.close(); wdr.close();
LOGGER.debug("Session " + this + " closed");
} catch (IOException e) {
LOGGER.warn("Exception closing session", e);
}
} }
/** /**
@ -422,14 +456,9 @@ public class ExchangeSession {
public Message getMessage(String messageUrl) throws IOException { public Message getMessage(String messageUrl) throws IOException {
// TODO switch according to Log4J log level
//wdr.setDebug(4);
//wdr.propfindMethod(messageUrl, 0);
Enumeration messageEnum = wdr.propfindMethod(messageUrl, 0, MESSAGE_REQUEST_PROPERTIES); Enumeration messageEnum = wdr.propfindMethod(messageUrl, 0, MESSAGE_REQUEST_PROPERTIES);
//wdr.setDebug(0);
// 201 created in some cases ?!? if ((wdr.getStatusCode() != HttpURLConnection.HTTP_OK)
if ((wdr.getStatusCode() != HttpURLConnection.HTTP_OK && wdr.getStatusCode() != HttpURLConnection.HTTP_CREATED)
|| !messageEnum.hasMoreElements()) { || !messageEnum.hasMoreElements()) {
throw new IOException("Unable to get message: " + wdr.getStatusCode() throw new IOException("Unable to get message: " + wdr.getStatusCode()
+ " " + wdr.getStatusMessage()); + " " + wdr.getStatusMessage());
@ -771,7 +800,7 @@ public class ExchangeSession {
" FROM Scope('SHALLOW TRAVERSAL OF \"" + calendarUrl + "\"')\n" + " FROM Scope('SHALLOW TRAVERSAL OF \"" + calendarUrl + "\"')\n" +
" WHERE NOT \"urn:schemas:calendar:instancetype\" = 1\n" + " WHERE NOT \"urn:schemas:calendar:instancetype\" = 1\n" +
" AND \"DAV:contentclass\" = 'urn:content-classes:appointment'\n" + " AND \"DAV:contentclass\" = 'urn:content-classes:appointment'\n" +
" AND \"urn:schemas:calendar:dtstart\" > '2008/11/01 00:00:00'\n" + // " AND \"urn:schemas:calendar:dtstart\" > '2008/11/01 00:00:00'\n" +
" ORDER BY \"urn:schemas:calendar:dtstart\" ASC\n" + " ORDER BY \"urn:schemas:calendar:dtstart\" ASC\n" +
" </d:sql>\n" + " </d:sql>\n" +
"</d:searchrequest>"; "</d:searchrequest>";
@ -965,10 +994,10 @@ public class ExchangeSession {
} }
results = XMLStreamUtil.getElementContentsAsMap(getMethod.getResponseBodyAsStream(), "item", "AN"); results = XMLStreamUtil.getElementContentsAsMap(getMethod.getResponseBodyAsStream(), "item", "AN");
// add detailed information, only if few results // add detailed information, only if few results
if (results.size() <=10) { if (results.size() <= 10) {
for (Map<String, String> person : results.values()) { for (Map<String, String> person : results.values()) {
galLookup(person); galLookup(person);
} }
} }
} finally { } finally {
getMethod.releaseConnection(); getMethod.releaseConnection();

View File

@ -11,12 +11,69 @@ import java.io.IOException;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.SocketException; import java.net.SocketException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Enumeration; import java.util.*;
/** /**
* Create ExchangeSession instances. * Create ExchangeSession instances.
*/ */
public class ExchangeSessionFactory { public class ExchangeSessionFactory {
private static final Object LOCK = new Object();
private static final Map<PoolKey, ExchangeSessionStack> poolMap = new HashMap<PoolKey, ExchangeSessionStack>();
static class PoolKey {
public String url;
public String userName;
public String password;
public PoolKey(String url, String userName, String password) {
this.url = url;
this.userName = userName;
this.password = password;
}
@Override
public boolean equals(Object object) {
return object == this ||
object instanceof PoolKey &&
((PoolKey) object).url.equals(this.url) &&
((PoolKey) object).userName.equals(this.userName) &&
((PoolKey) object).password.equals(this.password);
}
@Override
public int hashCode() {
return url.hashCode() + userName.hashCode() + password.hashCode();
}
}
static class ExchangeSessionStack extends Stack<ExchangeSession> {
// 15 minutes expire delay
protected static final long EXPIRE_DELAY = 1000 * 60 * 15;
protected long timestamp = System.currentTimeMillis();
@Override
public ExchangeSession pop() throws EmptyStackException {
timestamp = System.currentTimeMillis();
return super.pop();
}
@Override
public ExchangeSession push(ExchangeSession session) {
timestamp = System.currentTimeMillis();
return super.push(session);
}
public boolean isExpired() {
return (System.currentTimeMillis() - timestamp) > EXPIRE_DELAY;
}
public void clean() {
while (!isEmpty()) {
pop().close();
}
}
}
/** /**
* Create authenticated Exchange session * Create authenticated Exchange session
* *
@ -27,8 +84,29 @@ public class ExchangeSessionFactory {
*/ */
public static ExchangeSession getInstance(String userName, String password) throws IOException { public static ExchangeSession getInstance(String userName, String password) throws IOException {
try { try {
ExchangeSession session = new ExchangeSession(); String baseUrl = Settings.getProperty("davmail.url");
session.login(userName, password); PoolKey poolKey = new PoolKey(baseUrl, userName, password);
ExchangeSession session = null;
synchronized (LOCK) {
Stack<ExchangeSession> sessionStack = poolMap.get(poolKey);
if (sessionStack != null && !sessionStack.isEmpty()) {
session = sessionStack.pop();
ExchangeSession.LOGGER.debug("Got session " + session + " from pool");
}
}
if (session != null && session.isExpired()) {
ExchangeSession.LOGGER.debug("Session " + session + " expired");
session.close();
session = null;
}
if (session == null) {
session = new ExchangeSession(poolKey);
session.login();
ExchangeSession.LOGGER.debug("Created new session: " + session);
}
return session; return session;
} catch (IOException e) { } catch (IOException e) {
if (checkNetwork()) { if (checkNetwork()) {
@ -39,6 +117,41 @@ public class ExchangeSessionFactory {
} }
} }
/**
* Close (or pool) session.
*
* @param session exchange session
*/
public static void close(ExchangeSession session) {
synchronized (LOCK) {
if (session != null) {
PoolKey poolKey = session.getPoolKey();
ExchangeSessionStack sessionStack = poolMap.get(poolKey);
if (sessionStack == null) {
sessionStack = new ExchangeSessionStack();
poolMap.put(poolKey, sessionStack);
}
// keep httpClient, but close HTTP connection
session.close();
sessionStack.push(session);
ExchangeSession.LOGGER.debug("Pooled session: " + session);
}
// clean pool
List<PoolKey> toDeleteKeys = new ArrayList<PoolKey>();
for (Map.Entry<PoolKey, ExchangeSessionStack> entry : poolMap.entrySet()) {
if (entry.getValue().isExpired()) {
ExchangeSession.LOGGER.debug("Session pool for " + entry.getKey().userName + " expired");
entry.getValue().clean();
toDeleteKeys.add(entry.getKey());
}
}
for (PoolKey toDeleteKey : toDeleteKeys) {
poolMap.remove(toDeleteKey);
}
}
}
public static void checkConfig() throws IOException { public static void checkConfig() throws IOException {
String url = Settings.getProperty("davmail.url"); String url = Settings.getProperty("davmail.url");
@ -49,7 +162,6 @@ public class ExchangeSessionFactory {
testMethod.setFollowRedirects(false); testMethod.setFollowRedirects(false);
testMethod.setDoAuthentication(false); testMethod.setDoAuthentication(false);
int status = httpClient.executeMethod(testMethod); int status = httpClient.executeMethod(testMethod);
testMethod.releaseConnection();
ExchangeSession.LOGGER.debug("Test configuration status: " + status); ExchangeSession.LOGGER.debug("Test configuration status: " + status);
if (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED if (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED
&& status != HttpStatus.SC_MOVED_TEMPORARILY) { && status != HttpStatus.SC_MOVED_TEMPORARILY) {

View File

@ -178,7 +178,7 @@ public class LdapConnection extends AbstractConnection {
} else if (operation == LDAP_REQ_UNBIND) { } else if (operation == LDAP_REQ_UNBIND) {
if (session != null) { if (session != null) {
session.close(); ExchangeSessionFactory.close(session);
session = null; session = null;
} }
} else if (operation == LDAP_REQ_SEARCH) { } else if (operation == LDAP_REQ_SEARCH) {

View File

@ -16,11 +16,11 @@ public class TestExchangeSession {
Settings.setConfigFilePath(argv[currentArg++]); Settings.setConfigFilePath(argv[currentArg++]);
Settings.load(); Settings.load();
ExchangeSession session = new ExchangeSession(); ExchangeSession session;
// test auth // test auth
try { try {
ExchangeSessionFactory.checkConfig(); ExchangeSessionFactory.checkConfig();
session.login(argv[currentArg++], argv[currentArg++]); session = ExchangeSessionFactory.getInstance(argv[currentArg++], argv[currentArg++]);
ExchangeSession.Folder folder = session.selectFolder(argv[currentArg++]); ExchangeSession.Folder folder = session.selectFolder(argv[currentArg++]);
String messageName; String messageName;