Refactoring: extract a class TextBodyBuilder

This commit is contained in:
Koji Arai 2013-09-22 12:50:01 +09:00
parent 9a99c77653
commit 6155a65f65
5 changed files with 639 additions and 150 deletions

View File

@ -12,7 +12,7 @@ import java.io.Serializable;
*
* TODO: This container should also have a text part, along with its insertion point. Or maybe a generic InsertableContent and maintain one each for Html and Text?
*/
class InsertableHtmlContent implements Serializable {
public class InsertableHtmlContent implements Serializable {
private static final long serialVersionUID = 2397327034L;
// Default to a headerInsertionPoint at the beginning of the message.
private int headerInsertionPoint = 0;

View File

@ -88,6 +88,7 @@ import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.TextBodyBuilder;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
@ -111,6 +112,7 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -1350,136 +1352,51 @@ public class MessageCompose extends K9Activity implements OnClickListener,
* original message.
*/
private TextBody buildText(boolean isDraft, SimpleMessageFormat messageFormat) {
// The length of the formatted version of the user-supplied text/reply
int composedMessageLength;
// The offset of the user-supplied text/reply in the final text body
int composedMessageOffset;
TextBodyBuilder textBodyBuilder =
new TextBodyBuilder(
mMessageContentView.getText().toString(),
mIdentity.getSignatureUse() ? mSignatureView.getCharacters() : null
);
/*
* Find out if we need to include the original message as quoted text.
*
* We include the quoted text in the body if the user didn't choose to hide it. We always
* include the quoted text when we're saving a draft. That's so the user is able to
* "un-hide" the quoted text if (s)he opens a saved draft.
* We include the quoted text in the body if the user didn't choose to
* hide it. We always include the quoted text when we're saving a draft.
* That's so the user is able to "un-hide" the quoted text if (s)he
* opens a saved draft.
*/
boolean includeQuotedText = (mQuotedTextMode.equals(QuotedTextMode.SHOW) || isDraft);
if (isDraft || mQuotedTextMode == QuotedTextMode.SHOW) {
textBodyBuilder.setIncludeQuotedText(true);
}
else {
textBodyBuilder.setIncludeQuotedText(false);
}
if (!isDraft) {
textBodyBuilder.setAppendSignature(true);
}
else {
textBodyBuilder.setAppendSignature(false);
}
textBodyBuilder.setDraft(isDraft);
textBodyBuilder.setSignatureBeforeQuotedText(mAccount.isSignatureBeforeQuotedText());
// Reply after quote makes no sense for HEADER style replies
boolean replyAfterQuote = (mQuoteStyle == QuoteStyle.HEADER) ?
false : mAccount.isReplyAfterQuote();
boolean signatureBeforeQuotedText = mAccount.isSignatureBeforeQuotedText();
// Get the user-supplied text
String text = mMessageContentView.getCharacters();
// Handle HTML separate from the rest of the text content
if (messageFormat == SimpleMessageFormat.HTML) {
// Do we have to modify an existing message to include our reply?
if (includeQuotedText && mQuotedHtmlContent != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "insertable: " + mQuotedHtmlContent.toDebugString());
}
if (!isDraft) {
// Append signature to the reply
if (replyAfterQuote || signatureBeforeQuotedText) {
text = appendSignature(text);
}
}
// Convert the text to HTML
text = HtmlConverter.textToHtmlFragment(text);
/*
* Set the insertion location based upon our reply after quote setting.
* Additionally, add some extra separators between the composed message and quoted
* message depending on the quote location. We only add the extra separators when
* we're sending, that way when we load a draft, we don't have to know the length
* of the separators to remove them before editing.
*/
if (replyAfterQuote) {
mQuotedHtmlContent.setInsertionLocation(
InsertableHtmlContent.InsertionLocation.AFTER_QUOTE);
if (!isDraft) {
text = "<br clear=\"all\">" + text;
}
} else {
mQuotedHtmlContent.setInsertionLocation(
InsertableHtmlContent.InsertionLocation.BEFORE_QUOTE);
if (!isDraft) {
text += "<br><br>";
}
}
if (!isDraft) {
// Place signature immediately after the quoted text
if (!(replyAfterQuote || signatureBeforeQuotedText)) {
mQuotedHtmlContent.insertIntoQuotedFooter(getSignatureHtml());
}
}
mQuotedHtmlContent.setUserContent(text);
// Save length of the body and its offset. This is used when thawing drafts.
composedMessageLength = text.length();
composedMessageOffset = mQuotedHtmlContent.getInsertionPoint();
text = mQuotedHtmlContent.toString();
} else {
// There is no text to quote so simply append the signature if available
if (!isDraft) {
text = appendSignature(text);
}
// Convert the text to HTML
text = HtmlConverter.textToHtmlFragment(text);
//TODO: Wrap this in proper HTML tags
composedMessageLength = text.length();
composedMessageOffset = 0;
}
} else {
// Capture composed message length before we start attaching quoted parts and signatures.
composedMessageLength = text.length();
composedMessageOffset = 0;
if (!isDraft) {
// Append signature to the text/reply
if (replyAfterQuote || signatureBeforeQuotedText) {
text = appendSignature(text);
}
}
String quotedText = mQuotedText.getCharacters();
if (includeQuotedText && quotedText.length() > 0) {
if (replyAfterQuote) {
composedMessageOffset = quotedText.length() + "\r\n".length();
text = quotedText + "\r\n" + text;
} else {
text += "\r\n\r\n" + quotedText.toString();
}
}
if (!isDraft) {
// Place signature immediately after the quoted text
if (!(replyAfterQuote || signatureBeforeQuotedText)) {
text = appendSignature(text);
}
}
if (mQuoteStyle == QuoteStyle.PREFIX && mAccount.isReplyAfterQuote()) {
textBodyBuilder.setReplyAfterQuote(true);
}
else {
textBodyBuilder.setReplyAfterQuote(false);
}
TextBody body = new TextBody(text);
body.setComposedMessageLength(composedMessageLength);
body.setComposedMessageOffset(composedMessageOffset);
TextBody body;
if (messageFormat == SimpleMessageFormat.HTML) {
body = textBodyBuilder.buildTextHtml(mQuotedHtmlContent);
}
else {
body = textBodyBuilder.buildTextPlain(mQuotedText.getText().toString());
}
return body;
}
/**
* Build the final message to be sent (or saved). If there is another message quoted in this one, it will be baked
* into the final message here.
@ -1827,35 +1744,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
return identity;
}
private String appendSignature(String originalText) {
String text = originalText;
if (mIdentity.getSignatureUse()) {
String signature = mSignatureView.getCharacters();
if (signature != null && !signature.contentEquals("")) {
text += "\r\n" + signature;
}
}
return text;
}
/**
* Get an HTML version of the signature in the #mSignatureView, if any.
* @return HTML version of signature.
*/
private String getSignatureHtml() {
String signature = "";
if (mIdentity.getSignatureUse()) {
signature = mSignatureView.getCharacters();
if(!StringUtils.isNullOrEmpty(signature)) {
signature = HtmlConverter.textToHtmlFragment("\r\n" + signature);
}
}
return signature;
}
private void sendMessage() {
new SendMessageTask().execute();
}

View File

@ -0,0 +1,261 @@
package com.fsck.k9.mail.internet;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.activity.InsertableHtmlContent;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.helper.StringUtils;
import com.fsck.k9.mail.Body;
public class TextBodyBuilder {
// option
private boolean mIncludeQuotedText = true;
private boolean mReplyAfterQuote = false;
private boolean mSignatureBeforeQuotedText = false;
private boolean mDraft = false;
private boolean mAppendSignature = true;
// message parts
private String mMessageContent;
private String mSignature;
public TextBodyBuilder(
String messageContent,
String signature
) {
mMessageContent = messageContent;
mSignature = signature;
}
/**
* Build the {@link Body} that will contain the text of the message.
*
* <p>
* Draft messages are treated somewhat differently in that signatures are
* not appended and HTML separators between composed text and quoted text
* are not added.
* </p>
*
* @return {@link TextBody} instance that contains the entered text and
* possibly the quoted original message.
*/
public TextBody buildTextHtml(InsertableHtmlContent quotedHtmlContent) {
// The length of the formatted version of the user-supplied text/reply
int composedMessageLength;
// The offset of the user-supplied text/reply in the final text body
int composedMessageOffset;
// Get the user-supplied text
String text = mMessageContent;
// Do we have to modify an existing message to include our reply?
if (mIncludeQuotedText && quotedHtmlContent != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "insertable: " + quotedHtmlContent.toDebugString());
}
if (mAppendSignature) {
// Append signature to the reply
if (mReplyAfterQuote || mSignatureBeforeQuotedText) {
text += getSignature();
}
}
// Convert the text to HTML
text = textToHtmlFragment(text);
/*
* Set the insertion location based upon our reply after quote
* setting. Additionally, add some extra separators between the
* composed message and quoted message depending on the quote
* location. We only add the extra separators when we're
* sending, that way when we load a draft, we don't have to know
* the length of the separators to remove them before editing.
*/
if (mReplyAfterQuote) {
quotedHtmlContent.setInsertionLocation(
InsertableHtmlContent.InsertionLocation.AFTER_QUOTE);
if (!mDraft) {
text = "<br clear=\"all\">" + text;
}
}
else {
quotedHtmlContent.setInsertionLocation(
InsertableHtmlContent.InsertionLocation.BEFORE_QUOTE);
if (!mDraft) {
text += "<br><br>";
}
}
if (mAppendSignature) {
// Place signature immediately after the quoted text
if (!(mReplyAfterQuote || mSignatureBeforeQuotedText)) {
quotedHtmlContent.insertIntoQuotedFooter(getSignatureHtml());
}
}
quotedHtmlContent.setUserContent(text);
// Save length of the body and its offset. This is used when thawing drafts.
composedMessageLength = text.length();
composedMessageOffset = quotedHtmlContent.getInsertionPoint();
text = quotedHtmlContent.toString();
}
else {
// There is no text to quote so simply append the signature if available
if (mAppendSignature) {
text += getSignature();
}
// Convert the text to HTML
text = textToHtmlFragment(text);
//TODO: Wrap this in proper HTML tags
composedMessageLength = text.length();
composedMessageOffset = 0;
}
TextBody body = new TextBody(text);
body.setComposedMessageLength(composedMessageLength);
body.setComposedMessageOffset(composedMessageOffset);
return body;
}
/**
* Build the {@link Body} that will contain the text of the message.
*
* <p>
* Draft messages are treated somewhat differently in that signatures are
* not appended and HTML separators between composed text and quoted text
* are not added.
* </p>
*
* @return {@link TextBody} instance that contains the entered text and
* possibly the quoted original message.
*/
public TextBody buildTextPlain(String quotedText) {
// The length of the formatted version of the user-supplied text/reply
int composedMessageLength;
// The offset of the user-supplied text/reply in the final text body
int composedMessageOffset;
// Get the user-supplied text
String text = mMessageContent;
// Capture composed message length before we start attaching quoted parts and signatures.
composedMessageLength = text.length();
composedMessageOffset = 0;
// Do we have to modify an existing message to include our reply?
if (mIncludeQuotedText && !StringUtils.isNullOrEmpty(quotedText)) {
if (mAppendSignature) {
// Append signature to the text/reply
if (mReplyAfterQuote || mSignatureBeforeQuotedText) {
text += getSignature();
}
}
if (mReplyAfterQuote) {
composedMessageOffset = quotedText.length() + "\r\n".length();
text = quotedText + "\r\n" + text;
} else {
text += "\r\n\r\n" + quotedText;
}
if (mAppendSignature) {
// Place signature immediately after the quoted text
if (!(mReplyAfterQuote || mSignatureBeforeQuotedText)) {
text += getSignature();
}
}
}
else {
// There is no text to quote so simply append the signature if available
if (mAppendSignature) {
// Append signature to the text/reply
text += getSignature();
}
}
TextBody body = new TextBody(text);
body.setComposedMessageLength(composedMessageLength);
body.setComposedMessageOffset(composedMessageOffset);
return body;
}
private String getSignature() {
String signature = "";
if (!StringUtils.isNullOrEmpty(mSignature)) {
signature = "\r\n" + mSignature;
}
return signature;
}
/**
* Get an HTML version of the signature in the #mSignatureView, if any.
*
* @return HTML version of signature.
*/
private String getSignatureHtml() {
String signature = "";
if (!StringUtils.isNullOrEmpty(mSignature)) {
signature = textToHtmlFragment("\r\n" + mSignature);
}
return signature;
}
public String textToHtmlFragment(String text) {
return HtmlConverter.textToHtmlFragment(text);
}
// getter and setter
public boolean isIncludeQuotedText() {
return mIncludeQuotedText;
}
public void setIncludeQuotedText(boolean includeQuotedText) {
mIncludeQuotedText = includeQuotedText;
}
public boolean isDraft() {
return mDraft;
}
public void setDraft(boolean draft) {
mDraft = draft;
}
public boolean isSignatureBeforeQuotedText() {
return mSignatureBeforeQuotedText;
}
public void setSignatureBeforeQuotedText(boolean signatureBeforeQuotedText) {
mSignatureBeforeQuotedText = signatureBeforeQuotedText;
}
public boolean isReplyAfterQuote() {
return mReplyAfterQuote;
}
public void setReplyAfterQuote(boolean replyAfterQuote) {
mReplyAfterQuote = replyAfterQuote;
}
public boolean isAppendSignature() {
return mAppendSignature;
}
public void setAppendSignature(boolean appendSignature) {
mAppendSignature = appendSignature;
}
}

View File

@ -0,0 +1,5 @@
package com.fsck.k9;
public class K9 {
public static boolean DEBUG = false;
}

View File

@ -0,0 +1,335 @@
package com.fsck.k9.mail.internet;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.experimental.theories.*;
import org.junit.runner.RunWith;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.activity.InsertableHtmlContent;
class TestingTextBodyBuilder extends TextBodyBuilder {
public TestingTextBodyBuilder(
boolean includeQuotedText,
boolean isDraft,
QuoteStyle quoteStyle,
boolean replyAfterQuote,
boolean signatureBeforeQuotedText,
boolean signatureUse,
String messageContent,
String signature) {
super(messageContent,
signatureUse ? signature : null);
if (isDraft || includeQuotedText) {
this.setIncludeQuotedText(true);
}
else {
this.setIncludeQuotedText(false);
}
if (!isDraft) {
this.setAppendSignature(true);
}
else {
this.setAppendSignature(false);
}
this.setDraft(isDraft);
this.setSignatureBeforeQuotedText(signatureBeforeQuotedText);
if (quoteStyle == QuoteStyle.PREFIX && replyAfterQuote) {
this.setReplyAfterQuote(true);
}
else {
this.setReplyAfterQuote(false);
}
}
// HtmlConverter depends on Android.
// So we use dummy method for tests.
@Override
public String textToHtmlFragment(String text) {
return "<html>" + text + "</html>";
}
}
@RunWith(Theories.class)
public class TextBodyBuilderTest {
@DataPoints
public static boolean[] BOOLEANS = { true, false };
@DataPoints
public static QuoteStyle[] QUOTESTYLES = {QuoteStyle.PREFIX, QuoteStyle.HEADER};
@Theory
public void testBuildTextPlain(
boolean includeQuotedText,
QuoteStyle quoteStyle,
boolean isReplyAfterQuote,
boolean isSignatureUse,
boolean isSignatureBeforeQuotedText,
boolean isDraft
) {
String expectedText;
int expectedMessageLength;
int expectedMessagePosition;
// 1.quoted text
// 2.message content
// 3.signature
if (quoteStyle == QuoteStyle.PREFIX && isReplyAfterQuote) {
String expectedQuotedText = "";
if (isDraft || includeQuotedText) {
expectedQuotedText = "quoted text" + "\r\n";
}
expectedText = expectedQuotedText;
expectedText += "message content";
if (!isDraft && isSignatureUse) {
expectedText += "\r\n" + "signature";
}
expectedMessageLength = "message content".length();
expectedMessagePosition = expectedQuotedText.length();
}
// 1.message content
// 2.signature
// 3.quoted text
else if (isSignatureBeforeQuotedText) {
expectedText = "message content";
if (!isDraft && isSignatureUse) {
expectedText += "\r\n" + "signature";
}
if (isDraft || includeQuotedText) {
expectedText += "\r\n\r\nquoted text";
}
expectedMessageLength = "message content".length();
expectedMessagePosition = 0;
}
// 1.message content
// 2.quoted text
// 3.signature
else {
expectedText = "message content";
if (isDraft || includeQuotedText) {
expectedText += "\r\n\r\nquoted text";
}
if (!isDraft && isSignatureUse) {
expectedText += "\r\n" + "signature";
}
expectedMessageLength = "message content".length();
expectedMessagePosition = 0;
}
String quotedText = "quoted text";
String messageContent = "message content";
String signature = "signature";
TextBody textBody = new TestingTextBodyBuilder(
includeQuotedText,
isDraft,
quoteStyle,
isReplyAfterQuote,
isSignatureBeforeQuotedText,
isSignatureUse,
messageContent,
signature
).buildTextPlain(quotedText);
assertThat(textBody, instanceOf(TextBody.class));
assertThat(textBody.getText(), is(expectedText));
assertThat(textBody.getComposedMessageLength(), is(expectedMessageLength));
assertThat(textBody.getComposedMessageOffset(), is(expectedMessagePosition));
assertThat(textBody.getText().substring(expectedMessagePosition, expectedMessagePosition + expectedMessageLength),
is("message content"));
}
/**
* generate expected HtmlContent debug string
*
* @param expectedText
* @param quotedContent
* @param footerInsertionPoint
* @param isBefore
* @param userContent
* @param compiledResult
* @return expected string
*
* @see InsertableHtmlContent#toDebugString()
*/
public String makeExpectedHtmlContent(String expectedText, String quotedContent, int footerInsertionPoint, boolean isBefore,
String userContent, String compiledResult) {
String expectedHtmlContent = "InsertableHtmlContent{"
+ "headerInsertionPoint=0,"
+ " footerInsertionPoint=" + footerInsertionPoint + ","
+ " insertionLocation=" + (isBefore ? "BEFORE_QUOTE" : "AFTER_QUOTE") + ","
+ " quotedContent=" + quotedContent + ","
+ " userContent=" + userContent + ","
+ " compiledResult=" + compiledResult
+ "}";
return expectedHtmlContent;
}
@Theory
public void testBuildTextHtml(
boolean includeQuotedText,
QuoteStyle quoteStyle,
boolean isReplyAfterQuote,
boolean isSignatureUse,
boolean isSignatureBeforeQuotedText,
boolean isDraft
) {
String expectedText;
int expectedMessageLength;
int expectedMessagePosition = 0;
String expectedHtmlContent;
String expectedPrefix = "";
if (includeQuotedText && quoteStyle == QuoteStyle.PREFIX && isReplyAfterQuote && !isDraft) {
expectedPrefix = "<br clear=\"all\">";
}
String expectedPostfix = "";
if (!isDraft && includeQuotedText) {
expectedPostfix = "<br><br>";
}
// 1.quoted text
// 2.message content
// 3.signature
if (quoteStyle == QuoteStyle.PREFIX && isReplyAfterQuote) {
expectedText = expectedPrefix
+ "<html>message content";
if (!isDraft && isSignatureUse) {
expectedText += "\r\n" + "signature";
}
expectedText += "</html>";
expectedMessageLength = expectedText.length();
String quotedContent = "quoted text";
if (isDraft || includeQuotedText) {
expectedHtmlContent = makeExpectedHtmlContent(expectedText, quotedContent,
0,
false,
expectedText,
expectedText + quotedContent);
expectedText += quotedContent;
}
else {
expectedHtmlContent = makeExpectedHtmlContent(expectedText, quotedContent,
0,
true,
"",
quotedContent);
// expectedText += quotedContent;
}
}
// 1.message content
// 2.signature
// 3.quoted text
else if (isSignatureBeforeQuotedText) {
expectedText = expectedPrefix
+ "<html>message content";
if (!isDraft && isSignatureUse) {
expectedText += "\r\n" + "signature";
}
expectedText += "</html>";
expectedText += expectedPostfix;
expectedMessageLength = expectedText.length();
String quotedContent = "quoted text";
if (isDraft || includeQuotedText) {
expectedHtmlContent = makeExpectedHtmlContent(expectedText, quotedContent,
0,
true,
expectedText,
expectedText + quotedContent);
expectedText += quotedContent;
}
else {
expectedHtmlContent = makeExpectedHtmlContent(expectedText, quotedContent,
0,
true,
"",
quotedContent);
// expectedText += quotedContent;
}
}
// 1.message content
// 2.quoted text
// 3.signature
else {
String expectedSignature = "";
expectedText = expectedPrefix
+ "<html>message content";
if (!isDraft && isSignatureUse) {
if (!includeQuotedText) {
expectedText += "\r\n" + "signature";
}
else {
expectedSignature = "<html>\r\nsignature</html>";
}
}
expectedText += "</html>";
expectedText += expectedPostfix;
expectedMessageLength = expectedText.length();
String quotedContent = "quoted text";
if (isDraft || includeQuotedText) {
expectedHtmlContent = makeExpectedHtmlContent(expectedText, expectedSignature + quotedContent,
expectedSignature.length(),
true,
expectedText,
expectedText + expectedSignature + quotedContent);
expectedText += expectedSignature + quotedContent;
}
else {
expectedHtmlContent = makeExpectedHtmlContent(expectedText, quotedContent,
0,
true,
"",
quotedContent);
// expectedText += quotedContent;
}
}
InsertableHtmlContent insertableHtmlContent = new InsertableHtmlContent();
String quotedText = "quoted text";
insertableHtmlContent.setQuotedContent(new StringBuilder(quotedText));
String messageContent = "message content";
String signature = "signature";
TextBody textBody = new TestingTextBodyBuilder(
includeQuotedText,
isDraft,
quoteStyle,
isReplyAfterQuote,
isSignatureBeforeQuotedText,
isSignatureUse,
messageContent,
signature
).buildTextHtml(insertableHtmlContent);
assertThat(textBody, instanceOf(TextBody.class));
assertThat(textBody.getText(), is(expectedText));
assertThat(textBody.getComposedMessageLength(), is(expectedMessageLength));
assertThat(textBody.getComposedMessageOffset(), is(expectedMessagePosition));
assertThat(insertableHtmlContent.toDebugString(), is(expectedHtmlContent));
}
}