RSS
Rich Site Summary (RSS) is a format for delivering regularly changing web content and is essentially an entire website, news broadcast, blog, or similar condensed down to bullet points. Each item consists of bare-bones information like the date and a descriptive title and is supplied with a hyperlink to the website page that contains the full article.
The data is extended from XML and must adhere to defined standards as described at http://www.rssboard.org/rss-specification.
Boiling it down to the basics for the purposes of this example, the XML looks as follows:
<rss> <channel> <title></title> <description></description> <link></link> <image> <url></url> <title></title> <link></link> <width></width> <height></height> </image> <item> <title></title> <description></description> <link></link> <pubDate></pubDate> </item> <item> … </item> </channel> </rss>
Inside the root <rss> node, we have a <channel> node, which in turn contains an <image> node and a collection of one or more <item> nodes.
We’ll model these nodes as classes, but first we need to pull in the XML module and write a small helper class to do some parsing for us. In cm-lib.pro and cm-ui.pro, add the xml module to the modules in the QT variable; consider this example:
QT += sql network xml
Next, create a new XmlHelper class in a new folder cm-lib/source/utilities.
xml-helper.h:
#ifndef XMLHELPER_H #define XMLHELPER_H
#include <QDomNode> #include <QString>
namespace cm { namespace utilities {
class XmlHelper { public: static QString toString(const QDomNode& domNode);
private: XmlHelper(){} static void appendNode(const QDomNode& domNode, QString& output); };
}}
#endif
xml-helper.cpp:
#include "xml-helper.h" namespace cm { namespace utilities {
QString XmlHelper::toString(const QDomNode& domNode) { QString returnValue; for(auto i = 0; i < domNode.childNodes().size(); ++i) { QDomNode subNode = domNode.childNodes().at(i); appendNode(subNode, returnValue); } return returnValue; }
void XmlHelper::appendNode(const QDomNode& domNode, QString& output) { if(domNode.nodeType() == QDomNode::TextNode) { output.append(domNode.nodeValue()); return; }
if(domNode.nodeType() == QDomNode::AttributeNode) { output.append(" "); output.append(domNode.nodeName()); output.append("=""); output.append(domNode.nodeValue()); output.append("""); return; }
if(domNode.nodeType() == QDomNode::ElementNode) { output.append("<"); output.append(domNode.nodeName()); // Add attributes for(auto i = 0; i < domNode.attributes().size(); ++i) { QDomNode subNode = domNode.attributes().item(i); appendNode(subNode, output); } output.append(">"); for(auto i = 0; i < domNode.childNodes().size(); ++i) { QDomNode subNode = domNode.childNodes().at(i); appendNode(subNode, output); } output.append("</" + domNode.nodeName() + ">"); } }
}}
I won’t go into too much detail about what this class does as it isn't the focus of the chapter, but essentially, if we receive an XML node that contains HTML markup (which is quite common in RSS), the XML parser gets a bit confused and breaks up the HTML into XML nodes too, which isn’t what we want. Consider this example:
<xmlNode>
Here is something from a website that has a <a href=”http://www.bbc.co.uk”>hyperlink</a> in it.
</xmlNode>
In this case, the XML parser will see <a> as XML and break up the content into three child nodes similar to this:
<xmlNode>
<textNode1>Here is something from a website that has a </textNode1>
<a href=”http://www.bbc.co.uk”>hyperlink</a>
<textNode2>in it.</textNode2>
</xmlNode>
This makes it difficult to display the contents of xmlNode to the user on the UI. Instead, we use XmlHelper to parse the contents manually and construct a single string, which is much easier to work with.
Now, let’s move on to the RSS classes. In a new cm-lib/source/rss folder, create new RssChannel, RssImage, and RssItem classes.
rss-image.h:
#ifndef RSSIMAGE_H #define RSSIMAGE_H
#include <QObject> #include <QScopedPointer> #include <QtXml/QDomNode> #include <cm-lib_global.h>
namespace cm { namespace rss {
class CMLIBSHARED_EXPORT RssImage : public QObject { Q_OBJECT Q_PROPERTY(quint16 ui_height READ height CONSTANT) Q_PROPERTY(QString ui_link READ link CONSTANT) Q_PROPERTY(QString ui_title READ title CONSTANT) Q_PROPERTY(QString ui_url READ url CONSTANT) Q_PROPERTY(quint16 ui_width READ width CONSTANT)
public: explicit RssImage(QObject* parent = nullptr, const QDomNode& domNode = QDomNode()); ~RssImage();
quint16 height() const; const QString& link() const; const QString& title() const; const QString& url() const; quint16 width() const;
private: class Implementation; QScopedPointer<Implementation> implementation; };
}} #endif
rss-image.cpp:
#include "rss-image.h" namespace cm { namespace rss {
class RssImage::Implementation { public: QString url; // Mandatory. URL of GIF, JPEG or PNG that represents the channel. QString title; // Mandatory. Describes the image. QString link; // Mandatory. URL of the site. quint16 width; // Optional. Width in pixels. Max 144, default
88. quint16 height; // Optional. Height in pixels. Max 400, default
31
void update(const QDomNode& domNode) { QDomElement imageUrl = domNode.firstChildElement("url"); if(!imageUrl.isNull()) { url = imageUrl.text(); } QDomElement imageTitle = domNode.firstChildElement("title"); if(!imageTitle.isNull()) { title = imageTitle.text(); } QDomElement imageLink = domNode.firstChildElement("link"); if(!imageLink.isNull()) { link = imageLink.text(); } QDomElement imageWidth = domNode.firstChildElement("width"); if(!imageWidth.isNull()) { width = static_cast<quint16>(imageWidth.text().toShort()); } else { width = 88; } QDomElement imageHeight = domNode.firstChildElement("height"); if(!imageHeight.isNull()) { height = static_cast<quint16>
(imageHeight.text().toShort()); } else { height = 31; } } };
RssImage::RssImage(QObject* parent, const QDomNode& domNode) : QObject(parent) { implementation.reset(new Implementation()); implementation->update(domNode); }
RssImage::~RssImage() { }
quint16 RssImage::height() const { return implementation->height; }
const QString& RssImage::link() const { return implementation->link; }
const QString& RssImage::title() const { return implementation->title; }
const QString& RssImage::url() const { return implementation->url; }
quint16 RssImage::width() const { return implementation->width; }
}}
This class is just a regular plain data model with the exception that it will be constructed from an XML <image> node represented by Qt’s QDomNode class. We use the firstChildElement() method to locate the <url>, <title>, and <link> mandatory child nodes and then access the value of each node via the text() method. The <width> and <height> nodes are optional and if they are not present, we use the default image size of 88 x 31 pixels.
rss-item.h:
#ifndef RSSITEM_H #define RSSITEM_H
#include <QDateTime> #include <QObject> #include <QscopedPointer> #include <QtXml/QDomNode> #include <cm-lib_global.h>
namespace cm { namespace rss {
class CMLIBSHARED_EXPORT RssItem : public QObject { Q_OBJECT Q_PROPERTY(QString ui_description READ description CONSTANT) Q_PROPERTY(QString ui_link READ link CONSTANT) Q_PROPERTY(QDateTime ui_pubDate READ pubDate CONSTANT) Q_PROPERTY(QString ui_title READ title CONSTANT)
public: RssItem(QObject* parent = nullptr, const QDomNode& domNode = QDomNode()); ~RssItem();
const QString& description() const; const QString& link() const; const QDateTime& pubDate() const; const QString& title() const;
private: class Implementation; QScopedPointer<Implementation> implementation; };
}}
#endif
rss-item.cpp:
#include "rss-item.h" #include <QTextStream> #include <utilities/xml-helper.h>
using namespace cm::utilities;
namespace cm { namespace rss { class RssItem::Implementation { public: Implementation(RssItem* _rssItem) : rssItem(_rssItem) { }
RssItem* rssItem{nullptr}; QString description; // This or Title mandatory. Either the
synopsis or full story. HTML is allowed. QString link; // Optional. Link to full story. Populated
if Description is only the synopsis. QDateTime pubDate; // Optional. When the item was published.
RFC 822 format e.g. Sun, 19 May 2002 15:21:36 GMT. QString title; // This or Description mandatory.
void update(const QDomNode& domNode) { for(auto i = 0; i < domNode.childNodes().size(); ++i) { QDomNode childNode = domNode.childNodes().at(i); if(childNode.nodeName() == "description") { description = XmlHelper::toString(childNode); } } QDomElement itemLink = domNode.firstChildElement("link"); if(!itemLink.isNull()) { link = itemLink.text(); } QDomElement itemPubDate = domNode.firstChildElement("pubDate"); if(!itemPubDate.isNull()) { pubDate = QDateTime::fromString(itemPubDate.text(),
Qt::RFC2822Date); } QDomElement itemTitle = domNode.firstChildElement("title"); if(!itemTitle.isNull()) { title = itemTitle.text(); } } };
RssItem::RssItem(QObject* parent, const QDomNode& domNode) { implementation.reset(new Implementation(this)); implementation->update(domNode); }
RssItem::~RssItem() { }
const QString& RssItem::description() const { return implementation->description; }
const QString& RssItem::link() const { return implementation->link; }
const QDateTime& RssItem::pubDate() const { return implementation->pubDate; }
const QString& RssItem::title() const { return implementation->title; }
}}
This class is much the same as the last. This time we put our XMLHelper class to use when parsing the <description> node as that has a good chance of containing HTML tags. Also note that Qt also helpfully contains the Qt::RFC2822Date format specifier when converting a string to a QDateTime object using the static QDateTime::fromString() method. This is the format used in the RSS specification and saves us from having to manually parse the dates ourselves.
rss-channel.h:
#ifndef RSSCHANNEL_H #define RSSCHANNEL_H
#include <QDateTime> #include <QtXml/QDomElement> #include <QtXml/QDomNode> #include <QList> #include <QObject> #include <QtQml/QQmlListProperty> #include <QString>
#include <cm-lib_global.h> #include <rss/rss-image.h> #include <rss/rss-item.h>
namespace cm { namespace rss {
class CMLIBSHARED_EXPORT RssChannel : public QObject { Q_OBJECT Q_PROPERTY(QString ui_description READ description CONSTANT) Q_PROPERTY(cm::rss::RssImage* ui_image READ image CONSTANT) Q_PROPERTY(QQmlListProperty<cm::rss::RssItem> ui_items READ
ui_items CONSTANT) Q_PROPERTY(QString ui_link READ link CONSTANT) Q_PROPERTY(QString ui_title READ title CONSTANT)
public: RssChannel(QObject* parent = nullptr, const QDomNode& domNode = QDomNode()); ~RssChannel();
void addItem(RssItem* item); const QString& description() const; RssImage* image() const; const QList<RssItem*>& items() const; const QString& link() const; void setImage(RssImage* image); const QString& title() const; QQmlListProperty<RssItem> ui_items();
static RssChannel* fromXml(const QByteArray& xmlData, QObject*
parent = nullptr);
private: class Implementation; QScopedPointer<Implementation> implementation; };
}}
#endif
rss-channel.cpp:
#include "rss-channel.h" #include <QtXml/QDomDocument>
namespace cm { namespace rss {
class RssChannel::Implementation { public: QString description; // Mandatory. Phrase or sentence describing the channel. RssImage* image{nullptr}; // Optional. Image representing the channel. QList<RssItem*> items; // Optional. Collection representing stories. QString link; // Mandatory. URL to the corresponding HTML website. QString title; // Mandatory. THe name of the Channel.
void update(const QDomNode& domNode) { QDomElement channelDescription = domNode.firstChildElement("description"); if(!channelDescription.isNull()) { description = channelDescription.text(); } QDomElement channelLink = domNode.firstChildElement("link"); if(!channelLink.isNull()) { link = channelLink.text(); } QDomElement channelTitle = domNode.firstChildElement("title"); if(!channelTitle.isNull()) { title = channelTitle.text(); } } };
RssChannel::RssChannel(QObject* parent, const QDomNode& domNode) : QObject(parent) { implementation.reset(new Implementation()); implementation->update(domNode); }
RssChannel::~RssChannel() { }
void RssChannel::addItem(RssItem* item) { if(!implementation->items.contains(item)) { item->setParent(this); implementation->items.push_back(item); } }
const QString& RssChannel::description() const { return implementation->description; }
RssImage* RssChannel::image() const { return implementation->image; }
const QList<RssItem*>& RssChannel::items() const { return implementation->items; }
const QString& RssChannel::link() const { return implementation->link; }
void RssChannel::setImage(RssImage* image) { if(implementation->image) { implementation->image->deleteLater(); implementation->image = nullptr; } image->setParent(this); implementation->image = image; }
const QString& RssChannel::title() const { return implementation->title; } QQmlListProperty<RssItem> RssChannel::ui_items() { return QQmlListProperty<RssItem>(this, implementation->items); }
RssChannel* RssChannel::fromXml(const QByteArray& xmlData, QObject* parent) { QDomDocument doc; doc.setContent(xmlData); auto channelNodes = doc.elementsByTagName("channel"); // Rss must have 1 channel if(channelNodes.size() != 1) return nullptr; RssChannel* channel = new RssChannel(parent, channelNodes.at(0)); auto imageNodes = doc.elementsByTagName("image"); if(imageNodes.size() > 0) { channel->setImage(new RssImage(channel, imageNodes.at(0))); } auto itemNodes = doc.elementsByTagName("item"); for (auto i = 0; i < itemNodes.size(); ++i) { channel->addItem(new RssItem(channel, itemNodes.item(i))); } return channel; }
}}
This class is broadly the same as the previous classes, but because this is the root object of our XML tree, we also have a static fromXml() method. The goal here is to take the byte array from the RSS web request response containing the RSS feed XML and have the method create an RSS Channel, Image, and Items hierarchy for us.
We pass the XML byte array into the Qt QDomDocument class, much like we have done previously with JSON and the QJsonDocument class. We find the <channel> tag using the elementsByTagName() method and then construct a new RssChannel object using that tag as the QDomNode parameter of the constructor. The RssChannel populates its own properties, thanks to the update() method. We then locate the <image> and <item> child nodes and create new RssImage and RssItem instances that are added to the root RssChannel object. Again, the classes are capable of populating their own properties from the supplied QDomNode.
Before we forget, let’s also register the classes in main():
qmlRegisterType<cm::rss::RssChannel>("CM", 1, 0, "RssChannel"); qmlRegisterType<cm::rss::RssImage>("CM", 1, 0, "RssImage"); qmlRegisterType<cm::rss::RssItem>("CM", 1, 0, "RssItem");
We can now add an RssChannel to our MasterController for the UI to bind to:
- In MasterController, add a new rssChannel private member variable of the RssChannel* type
- Add an rssChannel() accessor method
- Add a rssChannelChanged() signal
- Add a Q_PROPERTY named ui_rssChannel using the accessor for READ and signal for NOTIFY
Rather than creating one construction when we don’t have any RSS data to feed it, we’ll do it in the RSS reply delegate:
void MasterController::onRssReplyReceived(int statusCode, QByteArray body) { qDebug() << "Received RSS request response code " << statusCode << ":"; qDebug() << body;
if(implementation->rssChannel) { implementation->rssChannel->deleteLater(); implementation->rssChannel = nullptr; emit rssChannelChanged(); }
implementation->rssChannel = RssChannel::fromXml(body, this); emit rssChannelChanged(); }
We perform some housekeeping that checks whether we already have an old channel object in memory and if we do, it safely deletes it using the deleteLater() method of QObject. We then go ahead and construct a new channel using the XML data from the web request.
We will display the RSS items in the response in a similar way to how we managed the search results, with a ListView and associated delegate. Add RssItemDelegate.qml to cm-ui/components and perform the usual steps of editing the components.qrc and qmldir files:
import QtQuick 2.9 import assets 1.0 import CM 1.0
Item { property RssItem rssItem implicitWidth: parent.width implicitHeight: background.height
Rectangle { id: background width: parent.width height: textPubDate.implicitHeight + textTitle.implicitHeight +
borderBottom.height + (Style.sizeItemMargin * 3) color: Style.colourPanelBackground
Text { id: textPubDate anchors { top: parent.top left: parent.left right: parent.right margins: Style.sizeItemMargin } text: Qt.formatDateTime(rssItem.ui_pubDate, "ddd, d MMM
yyyy @ h:mm ap") font { pixelSize: Style.pixelSizeDataControls italic: true weight: Font.Light } color: Style.colorItemDateFont }
Text { id: textTitle anchors { top: textPubDate.bottom left: parent.left right: parent.right margins: Style.sizeItemMargin } text: rssItem.ui_title font { pixelSize: Style.pixelSizeDataControls } color: Style.colorItemTitleFont wrapMode: Text.Wrap }
Rectangle { id: borderBottom anchors { top: textTitle.bottom left: parent.left right: parent.right topMargin: Style.sizeItemMargin } height: 1 color: Style.colorItemBorder }
MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onEntered: background.state = "hover" onExited: background.state = "" onClicked: if(rssItem.ui_link !== "") { Qt.openUrlExternally(rssItem.ui_link); } }
states: [ State { name: "hover" PropertyChanges { target: background color: Style.colourPanelBackgroundHover } } ] } }
To support this component, we will need to add a few more Style properties:
readonly property color colourItemBackground: "#fefefe" readonly property color colourItemBackgroundHover: "#efefef" readonly property color colorItemBorder: "#efefef" readonly property color colorItemDateFont: "#636363" readonly property color colorItemTitleFont: "#131313" readonly property real sizeItemMargin: 5
We can now utilize this delegate in RssView:
import QtQuick 2.9 import assets 1.0 import components 1.0
Item { Rectangle { anchors.fill: parent color: Style.colourBackground }
ListView { id: itemsView anchors { top: parent.top left: parent.left right: parent.right bottom: commandBar.top margins: Style.sizeHeaderMargin } clip: true model: masterController.ui_rssChannel ? masterController.ui_rssChannel.ui_items : 0 delegate: RssItemDelegate { rssItem: modelData } }
CommandBar { id: commandBar commandList: masterController.ui_commandController.ui_rssViewContextCommands } }
Build and run, navigate to the RSS View, and click on the Refresh button to make the web request and display the response:
Hover over the items to see the cursor effects and click on an item to open it in your default web browser. Qt handles this action for us in the Qt.openUrlExternally() method, to which we pass the RSS Item link property.