ניתוח של נתוני XML

שפת סימון ניתנת להרחבה (XML) היא קבוצה של כללים לקידוד מסמכים ב- שמחשב יכול לקרוא. XML הוא פורמט פופולרי לשיתוף נתונים באינטרנט.

אתרים שמעדכנים את התוכן שלהם לעיתים קרובות, כמו אתרי חדשות או בלוגים, מספקים פיד XML כדי שתוכנות חיצוניות יוכלו להתעדכן בתוכן שינויים. העלאה וניתוח של נתוני XML היא משימה נפוצה הקשורה לרשת באפליקציות. בנושא הזה נסביר איך לנתח מסמכי XML ולהשתמש בנתונים שלהם.

למידע נוסף על יצירת תוכן מבוסס-אינטרנט באפליקציה ל-Android, אפשר להיכנס לכתובת תוכן מבוסס-אינטרנט.

בחירת מנתח

אנחנו ממליצים על XmlPullParser, שהוא יעיל דרך קלה לנתח XML ב-Android. ב-Android יש הטמעות של הממשק הזה:

כל אחת מהאפשרויות בסדר. הדוגמה בקטע הזה משתמשת ב-ExpatPullParser וב Xml.newPullParser().

ניתוח הפיד

השלב הראשון בניתוח פיד הוא להחליט באילו שדות מעניינים. המנתח מחלץ נתונים מהשדות האלה ומתעלם מהשאר.

אפשר לעיין בקטע הבא מתוך פיד מנותח באפליקציה לדוגמה. כל אחד פוסט אל StackOverflow.com מופיע פיד בתור תג entry שמכיל מספר תגים מקוננים:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ...">
<title type="text">newest questions tagged android - Stack Overflow</title>
...
    <entry>
    ...
    </entry>
    <entry>
        <id>http://stackoverflow.com/q/9439999</id>
        <re:rank scheme="http://stackoverflow.com">0</re:rank>
        <title type="text">Where is my data file?</title>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/>
        <author>
            <name>cliff2310</name>
            <uri>http://stackoverflow.com/users/1128925</uri>
        </author>
        <link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file" />
        <published>2012-02-25T00:30:54Z</published>
        <updated>2012-02-25T00:30:54Z</updated>
        <summary type="html">
            <p>I have an Application that requires a data file...</p>

        </summary>
    </entry>
    <entry>
    ...
    </entry>
...
</feed>

האפליקציה לדוגמה מחלץ נתונים של התג entry והתגים הפנימיים שלו title, link וגם summary.

יצירת אובייקטים של המנתח

השלב הבא בניתוח פיד הוא ליצור מנתח ולהתחיל את תהליך הניתוח. קטע הקוד הזה מאתחל את המנתח כדי שלא לעבד מרחבי שמות ולהשתמש ב-InputStream שסופק כקלט שלו. הוא מתחיל את תהליך הניתוח בהפעלה של קריאה ל- nextTag() ומפעיל את שיטה readFeed(), שמחלצת ומעבדת את הנתונים שהאפליקציה תחומי עניין:

Kotlin

// We don't use namespaces.
private val ns: String? = null

class StackOverflowXmlParser {

    @Throws(XmlPullParserException::class, IOException::class)
    fun parse(inputStream: InputStream): List<*> {
        inputStream.use { inputStream ->
            val parser: XmlPullParser = Xml.newPullParser()
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
            parser.setInput(inputStream, null)
            parser.nextTag()
            return readFeed(parser)
        }
    }
 ...
}

Java

public class StackOverflowXmlParser {
    // We don't use namespaces.
    private static final String ns = null;

    public List parse(InputStream in) throws XmlPullParserException, IOException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
            parser.setInput(in, null);
            parser.nextTag();
            return readFeed(parser);
        } finally {
            in.close();
        }
    }
 ...
}

קריאת הפיד

השיטה readFeed() מבצעת בפועל את העיבוד של שבפיד. הוא מחפש רכיבים שתויגו כ'רשומה'. כנקודת התחלה של יצירה רקורסיבית עיבוד הפיד. אם תג אינו תג entry, הוא ידלג. אחרי שמשלמים מעובד באופן רקורסיבי, הפונקציה readFeed() מחזירה List שמכיל את הרשומות (כולל חברים בתוך נתונים) שחולץ מהפיד. לאחר מכן, הList הזה מוחזר על ידי ומנתח נתונים.

Kotlin

@Throws(XmlPullParserException::class, IOException::class)
private fun readFeed(parser: XmlPullParser): List<Entry> {
    val entries = mutableListOf<Entry>()

    parser.require(XmlPullParser.START_TAG, ns, "feed")
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.eventType != XmlPullParser.START_TAG) {
            continue
        }
        // Starts by looking for the entry tag.
        if (parser.name == "entry") {
            entries.add(readEntry(parser))
        } else {
            skip(parser)
        }
    }
    return entries
}

Java

private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException {
    List entries = new ArrayList();

    parser.require(XmlPullParser.START_TAG, ns, "feed");
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        // Starts by looking for the entry tag.
        if (name.equals("entry")) {
            entries.add(readEntry(parser));
        } else {
            skip(parser);
        }
    }
    return entries;
}

נתח XML

אלה השלבים לניתוח פיד בפורמט XML:

  1. כפי שמתואר במאמר ניתוח הפיד, מזהים את התגים שרוצים לכלול באפליקציה. הזה בדוגמה מחלצת נתונים עבור התג entry והתגים המקוננים שלו: title, link וגם summary.
  2. יוצרים את השיטות הבאות:

    • "קריאה" אחת לכל תג שרוצים לכלול, כמו readEntry() וגם readTitle() המנתח קורא תגים מזרם הקלט. כשהוא נתקל בתג בשם, בדוגמה הזו, entry, title link או summary, היא מפעילה את השיטה המתאימה לתג הזה. אחרת, הוא מדלג על התג.
    • שיטות לחילוץ נתונים לכל סוג תג שונה ולקידום לתג הבא. בדוגמה הזו, השיטות הרלוונטיות הן:
      • עבור התגים title ו-summary, המנתח מפעיל readText(). השיטה הזו מחלצת נתונים מהתגים האלה באמצעות קריאה parser.getText()
      • עבור התג link, המנתח מחלץ נתונים של קישורים לפי כדי לקבוע אם הקישור הוא מסוג במה הוא מתעניין. לאחר מכן נעשה בו שימוש ב-parser.getAttributeValue() כדי לחלץ את ערך הקישור.
      • עבור התג entry, המנתח קורא ל-readEntry(). השיטה הזו מנתחת את התגים המקוננים ברשומה ומחזירה ערך מסוג Entry אובייקט עם הנתונים title, link ו- summary
    • שיטה רקורסיבית מסוג skip(). לדיון נוסף בנושא הזה, ראו דילוג על תגים שלא חשובים לכם.

קטע הקוד הזה מראה איך המנתח מנתח רשומות, כותרות, קישורים וסיכומים.

Kotlin

data class Entry(val title: String?, val summary: String?, val link: String?)

// Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
// to their respective "read" methods for processing. Otherwise, skips the tag.
@Throws(XmlPullParserException::class, IOException::class)
private fun readEntry(parser: XmlPullParser): Entry {
    parser.require(XmlPullParser.START_TAG, ns, "entry")
    var title: String? = null
    var summary: String? = null
    var link: String? = null
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.eventType != XmlPullParser.START_TAG) {
            continue
        }
        when (parser.name) {
            "title" -> title = readTitle(parser)
            "summary" -> summary = readSummary(parser)
            "link" -> link = readLink(parser)
            else -> skip(parser)
        }
    }
    return Entry(title, summary, link)
}

// Processes title tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readTitle(parser: XmlPullParser): String {
    parser.require(XmlPullParser.START_TAG, ns, "title")
    val title = readText(parser)
    parser.require(XmlPullParser.END_TAG, ns, "title")
    return title
}

// Processes link tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readLink(parser: XmlPullParser): String {
    var link = ""
    parser.require(XmlPullParser.START_TAG, ns, "link")
    val tag = parser.name
    val relType = parser.getAttributeValue(null, "rel")
    if (tag == "link") {
        if (relType == "alternate") {
            link = parser.getAttributeValue(null, "href")
            parser.nextTag()
        }
    }
    parser.require(XmlPullParser.END_TAG, ns, "link")
    return link
}

// Processes summary tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readSummary(parser: XmlPullParser): String {
    parser.require(XmlPullParser.START_TAG, ns, "summary")
    val summary = readText(parser)
    parser.require(XmlPullParser.END_TAG, ns, "summary")
    return summary
}

// For the tags title and summary, extracts their text values.
@Throws(IOException::class, XmlPullParserException::class)
private fun readText(parser: XmlPullParser): String {
    var result = ""
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.text
        parser.nextTag()
    }
    return result
}
...

Java

public static class Entry {
    public final String title;
    public final String link;
    public final String summary;

    private Entry(String title, String summary, String link) {
        this.title = title;
        this.summary = summary;
        this.link = link;
    }
}

// Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
// to their respective "read" methods for processing. Otherwise, skips the tag.
private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, ns, "entry");
    String title = null;
    String summary = null;
    String link = null;
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        if (name.equals("title")) {
            title = readTitle(parser);
        } else if (name.equals("summary")) {
            summary = readSummary(parser);
        } else if (name.equals("link")) {
            link = readLink(parser);
        } else {
            skip(parser);
        }
    }
    return new Entry(title, summary, link);
}

// Processes title tags in the feed.
private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "title");
    String title = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "title");
    return title;
}

// Processes link tags in the feed.
private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException {
    String link = "";
    parser.require(XmlPullParser.START_TAG, ns, "link");
    String tag = parser.getName();
    String relType = parser.getAttributeValue(null, "rel");
    if (tag.equals("link")) {
        if (relType.equals("alternate")){
            link = parser.getAttributeValue(null, "href");
            parser.nextTag();
        }
    }
    parser.require(XmlPullParser.END_TAG, ns, "link");
    return link;
}

// Processes summary tags in the feed.
private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "summary");
    String summary = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "summary");
    return summary;
}

// For the tags title and summary, extracts their text values.
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
    String result = "";
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.getText();
        parser.nextTag();
    }
    return result;
}
  ...
}

דילוג על תגים שלא חשובים לך

המנתח צריך לדלג על תגים שהוא לא מעוניין בהם. זו שיטת ה-skip() של המנתח:

Kotlin

@Throws(XmlPullParserException::class, IOException::class)
private fun skip(parser: XmlPullParser) {
    if (parser.eventType != XmlPullParser.START_TAG) {
        throw IllegalStateException()
    }
    var depth = 1
    while (depth != 0) {
        when (parser.next()) {
            XmlPullParser.END_TAG -> depth--
            XmlPullParser.START_TAG -> depth++
        }
    }
}

Java

private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
    if (parser.getEventType() != XmlPullParser.START_TAG) {
        throw new IllegalStateException();
    }
    int depth = 1;
    while (depth != 0) {
        switch (parser.next()) {
        case XmlPullParser.END_TAG:
            depth--;
            break;
        case XmlPullParser.START_TAG:
            depth++;
            break;
        }
    }
 }

כך זה עובד:

  • הפעולה הזו גורמת לחריגה אם האירוע הנוכחי הוא לא START_TAG
  • היא צורכת את ה-START_TAG ואת כל האירועים עד וכולל END_TAG התואם.
  • התכונה עוקבת אחר עומק הסידור כדי לוודא שהיא נעצרת ב-END_TAG הנכון ולא ב- התג הראשון שהוא נתקל בו אחרי START_TAG המקורי.

כך, אם הרכיב הנוכחי מכיל רכיבים מקוננים, הערך של הערך של depth לא יהיה 0 עד שהמנתח ינצל את כל האירועים שבין START_TAG המקורי ו-END_TAG התואם שלו. עבור לדוגמה, כדאי להביא בחשבון את האופן שבו המנתח מדלג על הרכיב <author>, שמכיל 2 רכיבים מקוננים, <name> <uri>:

  • בפעם הראשונה דרך הלולאה while, התג הבא על המנתח פגישות אחרי <author> הוא START_TAG עבור <name> הערך של depth עולה ל- 2.
  • בפעם השנייה דרך הלולאה while, יתווסף התג הבא למנתח הוא </name> END_TAG. הערך ל-depth יורדים ל-1.
  • בפעם השלישית דרך הלולאה while, התג הבא על המנתח הוא <uri> START_TAG. הערך ל-depth עליות ל-2.
  • בפעם הרביעית דרך הלולאה while, התג הבא על המנתח הוא </uri> END_TAG. הערך של depth פוחתות ל-1.
  • בפעם החמישית והאחרונה דרך הלולאה while, התג שהמנתח נתקל בו הוא END_TAG </author>. הערך של depth יורד ל- 0, שמציין שהרכיב <author> בוצע בהצלחה בוצע דילוג.

צריכת נתוני XML

האפליקציה לדוגמה מאחזרת ומנתחת את פיד ה-XML באופן אסינכרוני. הפעולה הזו מסירה את העיבוד מה-thread הראשי בממשק המשתמש. מתי הושלם, האפליקציה מעדכנת את ממשק המשתמש בפעילות הראשית שלה, NetworkActivity

בקטע הבא, השיטה loadPage() מבצעת את הפעולות הבאות:

  • מאתחלת משתנה מחרוזת עם כתובת ה-URL של פיד ה-XML.
  • הפעלת השיטה downloadXml(url), אם ההגדרות והרשת של המשתמש מאפשר זאת. בשיטה הזו, המערכת מורידה ומנתחת את הפיד, ומחזירה את תוצאת המחרוזת שמוצגים בממשק המשתמש.

Kotlin

class NetworkActivity : Activity() {

    companion object {

        const val WIFI = "Wi-Fi"
        const val ANY = "Any"
        const val SO_URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"
        // Whether there is a Wi-Fi connection.
        private var wifiConnected = false
        // Whether there is a mobile connection.
        private var mobileConnected = false

        // Whether the display should be refreshed.
        var refreshDisplay = true
        // The user's current network preference setting.
        var sPref: String? = null
    }
    ...
    // Asynchronously downloads the XML feed from stackoverflow.com.
    fun loadPage() {

        if (sPref.equals(ANY) && (wifiConnected || mobileConnected)) {
            downloadXml(SO_URL)
        } else if (sPref.equals(WIFI) && wifiConnected) {
            downloadXml(SO_URL)
        } else {
            // Show error.
        }
    }
    ...
}

Java

public class NetworkActivity extends Activity {
    public static final String WIFI = "Wi-Fi";
    public static final String ANY = "Any";
    private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest";

    // Whether there is a Wi-Fi connection.
    private static boolean wifiConnected = false;
    // Whether there is a mobile connection.
    private static boolean mobileConnected = false;
    // Whether the display should be refreshed.
    public static boolean refreshDisplay = true;
    public static String sPref = null;
    ...
    // Asynchronously downloads the XML feed from stackoverflow.com.
    public void loadPage() {

        if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {
            downloadXml(URL);
        }
        else if ((sPref.equals(WIFI)) && (wifiConnected)) {
            downloadXml(URL);
        } else {
            // Show error.
        }
    }

השיטה downloadXml מפעילה את השיטות הבאות ב-Kotlin:

  • lifecycleScope.launch(Dispatchers.IO), שמשתמשת בקורוטין של Kotlin מפעילים את השיטה loadXmlFromNetwork() בשרשור IO. היא מעבירה את כתובת ה-URL של הפיד בתור הפרמטר. השיטה loadXmlFromNetwork() מאחזרת ומעבדת הפיד. כאשר הוא מסתיים, היא מחזירה מחרוזת תוצאה.
  • withContext(Dispatchers.Main), שמשתמש בקורוטינים של Kotlin כדי לחזור ל-thread הראשי, לוקח את מוחזר מחרוזת, ומציג אותו בממשק המשתמש.

בשפת התכנות Java, התהליך הוא כך:

  • Executor מבצע הרצה את השיטה loadXmlFromNetwork() בשרשור ברקע. היא מעבירה את כתובת ה-URL של הפיד בתור הפרמטר. השיטה loadXmlFromNetwork() מאחזרת ומעבדת הפיד. כאשר הוא מסתיים, היא מחזירה מחרוזת תוצאה.
  • Handler מתקשר אל post כדי לחזור ל-thread הראשי, מוחזר מחרוזת, ומציג אותו בממשק המשתמש.

Kotlin

// Implementation of Kotlin coroutines used to download XML feed from stackoverflow.com.
private fun downloadXml(vararg urls: String) {
    var result: String? = null
    lifecycleScope.launch(Dispatchers.IO) {
        result = try {
            loadXmlFromNetwork(urls[0])
        } catch (e: IOException) {
            resources.getString(R.string.connection_error)
        } catch (e: XmlPullParserException) {
            resources.getString(R.string.xml_error)
        }
        withContext(Dispatchers.Main) {
            setContentView(R.layout.main)
            // Displays the HTML string in the UI via a WebView.
            findViewById<WebView>(R.id.webview)?.apply {
                loadData(result?: "", "text/html", null)
            }
        }
    }
}

Java

// Implementation of Executor and Handler used to download XML feed asynchronously from stackoverflow.com.
private void downloadXml(String... urls) {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Handler handler = new Handler(Looper.getMainLooper());
    executor.execute(() -> {
        String result;
            try {
                result = loadXmlFromNetwork(urls[0]);
            } catch (IOException e) {
                result = getResources().getString(R.string.connection_error);
            } catch (XmlPullParserException e) {
                result = getResources().getString(R.string.xml_error);
            }
        String finalResult = result;
        handler.post(() -> {
            setContentView(R.layout.main);
            // Displays the HTML string in the UI via a WebView.
            WebView myWebView = (WebView) findViewById(R.id.webview);
            myWebView.loadData(finalResult, "text/html", null);
        });
    });
}

השיטה loadXmlFromNetwork() שמופעלת מ: downloadXml מוצג בקטע הקוד הבא. הוא מבצע את הפעולות הבאות:

  1. יוצר StackOverflowXmlParser. הוא גם יוצר משתנים List מתוך Entry אובייקטים (entries) ועבור title, url ו-summary, כדי להחזיק את שחולצו מפיד ה-XML עבור השדות האלה.
  2. הפונקציה שולחת קריאה אל downloadUrl(), שמאחזרת את הפיד ומחזירה אותו בתור InputStream.
  3. נעשה שימוש ב-StackOverflowXmlParser כדי לנתח את InputStream. StackOverflowXmlParser מאכלס List מתוך entries עם נתונים מהפיד.
  4. מתבצע עיבוד של entries List ומשלבת את נתוני הפיד עם תגי עיצוב של HTML.
  5. פונקציה זו מחזירה מחרוזת HTML המוצגת בפעילות הראשית ממשק משתמש.

Kotlin

// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
@Throws(XmlPullParserException::class, IOException::class)
private fun loadXmlFromNetwork(urlString: String): String {
    // Checks whether the user set the preference to include summary text.
    val pref: Boolean = PreferenceManager.getDefaultSharedPreferences(this)?.run {
        getBoolean("summaryPref", false)
    } ?: false

    val entries: List<Entry> = downloadUrl(urlString)?.use { stream ->
        // Instantiates the parser.
        StackOverflowXmlParser().parse(stream)
    } ?: emptyList()

    return StringBuilder().apply {
        append("<h3>${resources.getString(R.string.page_title)}</h3>")
        append("<em>${resources.getString(R.string.updated)} ")
        append("${formatter.format(rightNow.time)}</em>")
        // StackOverflowXmlParser returns a List (called "entries") of Entry objects.
        // Each Entry object represents a single post in the XML feed.
        // This section processes the entries list to combine each entry with HTML markup.
        // Each entry is displayed in the UI as a link that optionally includes
        // a text summary.
        entries.forEach { entry ->
            append("<p><a href='")
            append(entry.link)
            append("'>" + entry.title + "</a></p>")
            // If the user set the preference to include summary text,
            // adds it to the display.
            if (pref) {
                append(entry.summary)
            }
        }
    }.toString()
}

// Given a string representation of a URL, sets up a connection and gets
// an input stream.
@Throws(IOException::class)
private fun downloadUrl(urlString: String): InputStream? {
    val url = URL(urlString)
    return (url.openConnection() as? HttpURLConnection)?.run {
        readTimeout = 10000
        connectTimeout = 15000
        requestMethod = "GET"
        doInput = true
        // Starts the query.
        connect()
        inputStream
    }
}

Java

// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException {
    InputStream stream = null;
    // Instantiates the parser.
    StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser();
    List<Entry> entries = null;
    String title = null;
    String url = null;
    String summary = null;
    Calendar rightNow = Calendar.getInstance();
    DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa");

    // Checks whether the user set the preference to include summary text.
    SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    boolean pref = sharedPrefs.getBoolean("summaryPref", false);

    StringBuilder htmlString = new StringBuilder();
    htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>");
    htmlString.append("<em>" + getResources().getString(R.string.updated) + " " +
            formatter.format(rightNow.getTime()) + "</em>");

    try {
        stream = downloadUrl(urlString);
        entries = stackOverflowXmlParser.parse(stream);
    // Makes sure that the InputStream is closed after the app is
    // finished using it.
    } finally {
        if (stream != null) {
            stream.close();
        }
     }

    // StackOverflowXmlParser returns a List (called "entries") of Entry objects.
    // Each Entry object represents a single post in the XML feed.
    // This section processes the entries list to combine each entry with HTML markup.
    // Each entry is displayed in the UI as a link that optionally includes
    // a text summary.
    for (Entry entry : entries) {
        htmlString.append("<p><a href='");
        htmlString.append(entry.link);
        htmlString.append("'>" + entry.title + "</a></p>");
        // If the user set the preference to include summary text,
        // adds it to the display.
        if (pref) {
            htmlString.append(entry.summary);
        }
    }
    return htmlString.toString();
}

// Given a string representation of a URL, sets up a connection and gets
// an input stream.
private InputStream downloadUrl(String urlString) throws IOException {
    URL url = new URL(urlString);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setReadTimeout(10000 /* milliseconds */);
    conn.setConnectTimeout(15000 /* milliseconds */);
    conn.setRequestMethod("GET");
    conn.setDoInput(true);
    // Starts the query.
    conn.connect();
    return conn.getInputStream();
}