Merge "DO NOT MERGE: Fix handling IOException in ImapStore" into gingerbread

This commit is contained in:
Makoto Onuki 2010-09-21 10:24:48 -07:00 committed by Android (Google) Code Review
commit 0be6c7c5cf
3 changed files with 181 additions and 31 deletions

View File

@ -510,10 +510,15 @@ public class ImapStore extends Store {
mName = name; mName = name;
} }
private void destroyResponses() {
if (mConnection != null) {
mConnection.destroyResponses();
}
}
@Override @Override
public void open(OpenMode mode, PersistentDataCallbacks callbacks) public void open(OpenMode mode, PersistentDataCallbacks callbacks)
throws MessagingException { throws MessagingException {
try { try {
if (isOpen()) { if (isOpen()) {
if (mMode == mode) { if (mMode == mode) {
@ -526,7 +531,7 @@ public class ImapStore extends Store {
} catch (IOException ioe) { } catch (IOException ioe) {
ioExceptionHandler(mConnection, ioe); ioExceptionHandler(mConnection, ioe);
} finally { } finally {
mConnection.destroyResponses(); destroyResponses();
} }
} else { } else {
// Return the connection to the pool, if exists. // Return the connection to the pool, if exists.
@ -582,7 +587,7 @@ public class ImapStore extends Store {
} catch (IOException ioe) { } catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe); throw ioExceptionHandler(mConnection, ioe);
} finally { } finally {
mConnection.destroyResponses(); destroyResponses();
} }
} catch (MessagingException e) { } catch (MessagingException e) {
mExists = false; mExists = false;
@ -606,6 +611,7 @@ public class ImapStore extends Store {
// TODO implement expunge // TODO implement expunge
mMessageCount = -1; mMessageCount = -1;
synchronized (this) { synchronized (this) {
destroyResponses();
mStore.poolConnection(mConnection); mStore.poolConnection(mConnection);
mConnection = null; mConnection = null;
} }
@ -707,7 +713,7 @@ public class ImapStore extends Store {
} catch (IOException ioe) { } catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe); throw ioExceptionHandler(mConnection, ioe);
} finally { } finally {
mConnection.destroyResponses(); destroyResponses();
} }
} }
@ -735,7 +741,7 @@ public class ImapStore extends Store {
} catch (IOException ioe) { } catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe); throw ioExceptionHandler(mConnection, ioe);
} finally { } finally {
mConnection.destroyResponses(); destroyResponses();
} }
} }
@ -773,7 +779,7 @@ public class ImapStore extends Store {
} }
return uids.toArray(Utility.EMPTY_STRINGS); return uids.toArray(Utility.EMPTY_STRINGS);
} finally { } finally {
mConnection.destroyResponses(); destroyResponses();
} }
} }
@ -835,16 +841,16 @@ public class ImapStore extends Store {
fetchInternal(messages, fp, listener); fetchInternal(messages, fp, listener);
} catch (RuntimeException e) { // Probably a parser error. } catch (RuntimeException e) { // Probably a parser error.
Log.w(Email.LOG_TAG, "Exception detected: " + e.getMessage()); Log.w(Email.LOG_TAG, "Exception detected: " + e.getMessage());
mConnection.logLastDiscourse(); if (mConnection != null) {
mConnection.logLastDiscourse();
}
throw e; throw e;
} finally {
mConnection.destroyResponses();
} }
} }
public void fetchInternal(Message[] messages, FetchProfile fp, public void fetchInternal(Message[] messages, FetchProfile fp,
MessageRetrievalListener listener) throws MessagingException { MessageRetrievalListener listener) throws MessagingException {
if (messages == null || messages.length == 0) { if (messages.length == 0) {
return; return;
} }
checkOpen(); checkOpen();
@ -897,7 +903,7 @@ public class ImapStore extends Store {
} }
try { try {
String tag = mConnection.sendCommand(String.format( mConnection.sendCommand(String.format(
ImapConstants.UID_FETCH + " %s (%s)", joinMessageUids(messages), ImapConstants.UID_FETCH + " %s (%s)", joinMessageUids(messages),
Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
), false); ), false);
@ -991,7 +997,7 @@ public class ImapStore extends Store {
listener.messageRetrieved(message); listener.messageRetrieved(message);
} }
} finally { } finally {
mConnection.destroyResponses(); destroyResponses();
} }
} while (!response.isTagged()); } while (!response.isTagged());
} catch (IOException ioe) { } catch (IOException ioe) {
@ -1285,7 +1291,7 @@ public class ImapStore extends Store {
} catch (IOException ioe) { } catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe); throw ioExceptionHandler(mConnection, ioe);
} finally { } finally {
mConnection.destroyResponses(); destroyResponses();
} }
} }
@ -1297,7 +1303,7 @@ public class ImapStore extends Store {
} catch (IOException ioe) { } catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe); throw ioExceptionHandler(mConnection, ioe);
} finally { } finally {
mConnection.destroyResponses(); destroyResponses();
} }
return null; return null;
} }
@ -1332,7 +1338,7 @@ public class ImapStore extends Store {
} catch (IOException ioe) { } catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe); throw ioExceptionHandler(mConnection, ioe);
} finally { } finally {
mConnection.destroyResponses(); destroyResponses();
} }
} }
@ -1344,8 +1350,15 @@ public class ImapStore extends Store {
private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe)
throws MessagingException { throws MessagingException {
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, "IO Exception detected: ", ioe);
}
connection.destroyResponses();
connection.close(); connection.close();
close(false); if (connection == mConnection) {
mConnection = null; // To prevent close() from returning the connection to the pool.
close(false);
}
return new MessagingException("IO Error", ioe); return new MessagingException("IO Error", ioe);
} }

View File

@ -50,6 +50,7 @@ import android.test.AndroidTestCase;
import android.test.MoreAsserts; import android.test.MoreAsserts;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -1612,4 +1613,115 @@ public class ImapStoreUnitTests extends AndroidTestCase {
folders[1].open(OpenMode.READ_WRITE, null); folders[1].open(OpenMode.READ_WRITE, null);
folders[1].close(false); folders[1].close(false);
} }
/**
* Callback for {@link #runAndExpectMessagingException}.
*/
private interface RunAndExpectMessagingExceptionTarget {
public void run(MockTransport mockTransport) throws Exception;
}
/**
* Set up the usual mock transport, open the folder,
* run {@link RunAndExpectMessagingExceptionTarget} and make sure a {@link MessagingException}
* is thrown.
*/
private void runAndExpectMessagingException(RunAndExpectMessagingExceptionTarget target)
throws Exception {
try {
final MockTransport mockTransport = openAndInjectMockTransport();
setupOpenFolder(mockTransport);
mFolder.open(OpenMode.READ_WRITE, null);
target.run(mockTransport);
fail("MessagingException expected.");
} catch (MessagingException expected) {
}
}
/**
* Make sure that IOExceptions are always converted to MessagingException.
*/
public void testFetchIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
final Message message = mFolder.createMessage("1");
final FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.STRUCTURE);
mFolder.fetch(new Message[] { message }, fp, null);
}
});
}
/**
* Make sure that IOExceptions are always converted to MessagingException.
*/
public void testUnreadMessageCountIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
mFolder.getUnreadMessageCount();
}
});
}
/**
* Make sure that IOExceptions are always converted to MessagingException.
*/
public void testCopyMessagesIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
final Message message = mFolder.createMessage("1");
final Folder folder = mStore.getFolder("test");
mFolder.copyMessages(new Message[] { message }, folder, null);
}
});
}
/**
* Make sure that IOExceptions are always converted to MessagingException.
*/
public void testSearchForUidsIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
mFolder.getMessage("uid");
}
});
}
/**
* Make sure that IOExceptions are always converted to MessagingException.
*/
public void testExpungeIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
mFolder.expunge();
}
});
}
/**
* Make sure that IOExceptions are always converted to MessagingException.
*/
public void testOpenIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
final Folder folder = mStore.getFolder("test");
folder.open(OpenMode.READ_WRITE, null);
}
});
}
} }

View File

@ -42,6 +42,8 @@ public class MockTransport implements Transport {
private static String LOG_TAG = "MockTransport"; private static String LOG_TAG = "MockTransport";
private static final String SPECIAL_RESPONSE_IOEXCEPTION = "!!!IOEXCEPTION!!!";
private boolean mSslAllowed = false; private boolean mSslAllowed = false;
private boolean mTlsAllowed = false; private boolean mTlsAllowed = false;
@ -56,8 +58,8 @@ public class MockTransport implements Transport {
private static class Transaction { private static class Transaction {
public static final int ACTION_INJECT_TEXT = 0; public static final int ACTION_INJECT_TEXT = 0;
public static final int ACTION_SERVER_CLOSE = 1; public static final int ACTION_CLIENT_CLOSE = 1;
public static final int ACTION_CLIENT_CLOSE = 2; public static final int ACTION_IO_EXCEPTION = 2;
int mAction; int mAction;
String mPattern; String mPattern;
@ -80,10 +82,10 @@ public class MockTransport implements Transport {
switch (mAction) { switch (mAction) {
case ACTION_INJECT_TEXT: case ACTION_INJECT_TEXT:
return mPattern + ": " + Arrays.toString(mResponses); return mPattern + ": " + Arrays.toString(mResponses);
case ACTION_SERVER_CLOSE:
return "Close the server connection";
case ACTION_CLIENT_CLOSE: case ACTION_CLIENT_CLOSE:
return "Expect the client to close"; return "Expect the client to close";
case ACTION_IO_EXCEPTION:
return "Expect IOException";
default: default:
return "(Hmm. Unknown action.)"; return "(Hmm. Unknown action.)";
} }
@ -136,9 +138,22 @@ public class MockTransport implements Transport {
mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE)); mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE));
} }
private void sendResponse(String[] responses) { public void expectIOException() {
for (String s : responses) { mPairs.add(new Transaction(Transaction.ACTION_IO_EXCEPTION));
mQueuedInput.add(s); }
private void sendResponse(Transaction pair) {
switch (pair.mAction) {
case Transaction.ACTION_INJECT_TEXT:
for (String s : pair.mResponses) {
mQueuedInput.add(s);
}
break;
case Transaction.ACTION_IO_EXCEPTION:
mQueuedInput.add(SPECIAL_RESPONSE_IOEXCEPTION);
break;
default:
Assert.fail("Invalid action for sendResponse: " + pair.mAction);
} }
} }
@ -250,18 +265,25 @@ public class MockTransport implements Transport {
throw new IOException("Reading from MockTransport with closed input"); throw new IOException("Reading from MockTransport with closed input");
} }
// if there's nothing to read, see if we can find a null-pattern response // if there's nothing to read, see if we can find a null-pattern response
if (0 == mQueuedInput.size()) { if ((mQueuedInput.size() == 0) && (mPairs.size() > 0)) {
Transaction pair = mPairs.size() > 0 ? mPairs.get(0) : null; Transaction pair = mPairs.get(0);
if (pair != null && pair.mPattern == null) { if (pair.mPattern == null) {
mPairs.remove(0); mPairs.remove(0);
sendResponse(pair.mResponses); sendResponse(pair);
} }
} }
SmtpSenderUnitTests.assertTrue("Underflow reading from MockTransport", 0 != mQueuedInput.size()); if (mQueuedInput.size() == 0) {
// MailTransport returns "" at EOS.
Log.w(LOG_TAG, "Underflow reading from MockTransport");
return "";
}
String line = mQueuedInput.remove(0); String line = mQueuedInput.remove(0);
if (DEBUG_LOG_STREAMS) { if (DEBUG_LOG_STREAMS) {
Log.d(LOG_TAG, "<<< " + line); Log.d(LOG_TAG, "<<< " + line);
} }
if (SPECIAL_RESPONSE_IOEXCEPTION.equals(line)) {
throw new IOException("Expected IOException.");
}
return line; return line;
} }
@ -292,7 +314,7 @@ public class MockTransport implements Transport {
* *
* Logs the written text if DEBUG_LOG_STREAMS is true. * Logs the written text if DEBUG_LOG_STREAMS is true.
*/ */
public void writeLine(String s, String sensitiveReplacement) /* throws IOException */ { public void writeLine(String s, String sensitiveReplacement) throws IOException {
if (DEBUG_LOG_STREAMS) { if (DEBUG_LOG_STREAMS) {
Log.d(LOG_TAG, ">>> " + s); Log.d(LOG_TAG, ">>> " + s);
} }
@ -300,11 +322,14 @@ public class MockTransport implements Transport {
SmtpSenderUnitTests.assertTrue("Overflow writing to MockTransport: Getting " + s, SmtpSenderUnitTests.assertTrue("Overflow writing to MockTransport: Getting " + s,
0 != mPairs.size()); 0 != mPairs.size());
Transaction pair = mPairs.remove(0); Transaction pair = mPairs.remove(0);
if (pair.mAction == Transaction.ACTION_IO_EXCEPTION) {
throw new IOException("Expected IOException.");
}
SmtpSenderUnitTests.assertTrue("Unexpected string written to MockTransport: Actual=" + s SmtpSenderUnitTests.assertTrue("Unexpected string written to MockTransport: Actual=" + s
+ " Expected=" + pair.mPattern, + " Expected=" + pair.mPattern,
pair.mPattern != null && s.matches(pair.mPattern)); pair.mPattern != null && s.matches(pair.mPattern));
if (pair.mResponses != null) { if (pair.mResponses != null) {
sendResponse(pair.mResponses); sendResponse(pair);
} }
} }
@ -354,7 +379,7 @@ public class MockTransport implements Transport {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@Override @Override
public void write(int oneByte) { public void write(int oneByte) throws IOException {
// CR or CRLF will immediately dump previous line (w/o CRLF) // CR or CRLF will immediately dump previous line (w/o CRLF)
if (oneByte == '\r') { if (oneByte == '\r') {
writeLine(sb.toString(), null); writeLine(sb.toString(), null);