تحليل بيانات 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> الذي يحتوي على عنصرين متداخلين، هما <name> و<uri>:

  • في المرة الأولى خلال التكرار الحلقي while، تكون العلامة التالية التي يصادفها المحلّل بعد <author> هي العلامة START_TAG للسمة <name>. تزيد قيمة depth إلى 2.
  • في المرة الثانية خلال التكرار الحلقي while، تكون العلامة التالية التي يصادفها المحلل اللغوي هي END_TAG </name>. تقل قيمة depth إلى 1.
  • في المرة الثالثة خلال التكرار الحلقي while، تكون العلامة التالية التي يصادفها المحلل اللغوي هي START_TAG <uri>. تزيد قيمة depth إلى 2.
  • في المرّة الرابعة خلال التكرار الحلقي while، تكون العلامة التالية التي يصادفها المحلل اللغوي هي END_TAG </uri>. تقل قيمة depth إلى 1.
  • في المرة الخامسة والأخيرة من خلال التكرار الحلقي while، تكون العلامة التالية التي يصادفها المحلل اللغوي هي END_TAG </author>. تقل قيمة depth إلى 0، ما يشير إلى أنّه تم تخطّي العنصر <author> بنجاح.

استخدام بيانات XML

يجلب التطبيق النموذجي خلاصة XML ويحلّلها بشكل غير متزامن. يؤدي هذا الإجراء إلى إزالة المعالجة من سلسلة محادثات واجهة المستخدم الرئيسية. عند اكتمال المعالجة، يحدّث التطبيق واجهة المستخدم في أنشطته الرئيسية، 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)، الذي يستخدم coroutines في Kotlin لإطلاق الطريقة loadXmlFromNetwork() على سلسلة IO. تمرِّر هذه الميزة عنوان URL للخلاصة كمَعلمة. تجلب الطريقة loadXmlFromNetwork() الخلاصة وتعالجها. عند الانتهاء، تمرر سلسلة النتيجة.
  • إنّ الدالة withContext(Dispatchers.Main)، التي تستخدم الكوروتينات في لغة Kotlin للرجوع إلى سلسلة التعليمات الرئيسية، تأخذ السلسلة المعروضة وتعرضها في واجهة المستخدم.

في لغة البرمجة Java، تكون العملية كما يلي:

  • تنفِّذ Executor الطريقة loadXmlFromNetwork() في سلسلة محادثات في الخلفية. تمرِّر هذه الميزة عنوان URL للخلاصة كمَعلمة. تجلب الطريقة loadXmlFromNetwork() الخلاصة وتعالجها. عند الانتهاء، تمرر سلسلة النتيجة.
  • تطلب دالة Handler post للرجوع إلى سلسلة التعليمات الرئيسية، وتأخذ السلسلة المعروضة، وتعرضها في واجهة المستخدم.

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. تعالج هذه السمة List entries وتدمج بيانات الخلاصة مع ترميز 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();
}