Reading an xml cookbook file with QXmlStreamReader

This article shows how to read an xml cookbook file with the help of QXmlStreamReader. The xml file can be seen below:

<?xml version="1.0" encoding="UTF-8"?>
<!--cookbook with no DTD-->
<cookbook>
    <recipe title="Bread">
        <ingredientList>
            <ingredient>flour</ingredient>
            <ingredient>water</ingredient>
            <ingredient>yeast</ingredient>
            <ingredient>salt</ingredient>
        </ingredientList>
        <instructions>Instructions to make bread.</instructions>
    </recipe>
    <recipe title="Pancake">
        <ingredientList>
            <ingredient>flour</ingredient>
            <ingredient>salt</ingredient>
            <ingredient>egg</ingredient>
            <ingredient>milk</ingredient>
            <ingredient>oil</ingredient>
        </ingredientList>
        <instructions>Instructions to make pancakes.</instructions>
    </recipe>
</cookbook>

This example was prepared in accordance with QXmlStream Bookmarks Example. To enable xml support, do not forget to add QT += xml into your project file.

The technique presented in this article makes use of the following methods of the QXmlStreamReader:

bool QXmlStreamReader::readNextStartElement()
Reads until the next start element within the current element. Returns false when an end element was reached or when an error occured.

void QXmlStreamReader::skipCurrentElement()
This method is useful for skipping unknown elements. It reads until the end of the current element, skipping any child nodes.
Introduction - How to read simple xml

The following section summarizes how to read simple xml files. Let's assume that our file contains two kinds of child elements 'childA' and 'childB' in an arbitrary order. We choose to read only the 'childA' elements and ignore the 'childB' (or anything else for that matter). This situation is shown in the xml file and the figure below (the elements that do not interest us are marked with a dashed pattern).

<?xml version="1.0" encoding="UTF-8"?>
<!--example with no DTD-->
<root>
   <childA>text1</childA>
   <childA>text2</childA>
   <childB>text3</childB>
   <childA>text4</childA>
</root>

To allow reading an arbitrary number of elements in an arbitrary order, we are going to use the while statement in combination with QXmlStreamReader::readNextStartElement(). To skip the elements that do not interest us we are going to use QXmlStreamReader::skipCurrentElement(). The code snippet for the child elements only looks like this:

while(reader.readNextStartElement()){
     if(reader.name() == "childA"){
          QString s = reader.readElementText();
          qDebug(qPrintable(s));
     }
     else
          reader.skipCurrentElement();
}

The entire implementation can be seen below. Notice, that before we read the child elements, we read the root element and check that it has the desired name (in this general case the name is "root", in the cookbook example the name should be "cookbook").

int main(int argc, char *argv[])
{
    QFile file("test.xml");
    if(!file.open(QFile::ReadOnly | QFile::Text)){
        qDebug() << "Cannot read file" << file.errorString();
        exit(0);
    }

    QXmlStreamReader reader(&file);

    if (reader.readNextStartElement()) {
        if (reader.name() == "root"){
            while(reader.readNextStartElement()){
                if(reader.name() == "childA"){
                    QString s = reader.readElementText();
                    qDebug(qPrintable(s));
                }
                else
                    reader.skipCurrentElement();
            }
        }
        else
            reader.raiseError(QObject::tr("Incorrect file"));
    }

    return 0;
}

The output looks like this:

text1
text2
text4

To read xml files with children and subchildren, we are going to use the same technique, but additionally, we are going to use the while loop also in the child elements that contain subchildren. The xml file and the figure for this constellation can be seen below (the elements of no interest are marked with a dashed pattern).

<?xml version="1.0" encoding="UTF-8"?>
<!--example with no DTD-->
<root>
   <childA>
      <subchild1>subtext1</subchild1>
      <subchild2>subtext2</subchild2>
   </childA>
   <childA>
      <subchild1>subtext3</subchild1>
      <subchild2>subtext4</subchild2>
   </childA>
   <childB>text5</childB>
   <childB>text6</childB>
</root>

Let's assume that we are only interested in the 'childA' and its 'subchild1' and not interested in the 'subchild2'. We also choose to ignore the 'childB' elements altogether. Since the 'childA' contains subchildren, we have to use the while loop also within the 'childA', i.e. there is a while loop within a while loop. This can be seen below:

if (reader.readNextStartElement()) {
        if (reader.name() == "root"){
            while(reader.readNextStartElement()){
                if(reader.name() == "childA"){
                    while(reader.readNextStartElement()){
                        if(reader.name() == "subchild1"){
                            QString s = reader.readElementText();
                            qDebug(qPrintable(s));
                        }
                        else
                            reader.skipCurrentElement();
                    }
                }
                else
                    reader.skipCurrentElement();
            }
        }
        else
            reader.raiseError(QObject::tr("Incorrect file"));
}

The output looks like this:

subtext1
subtext3

Reading the cookbook input file

Based on the explanation above, in the following section I am going to show you how to read the cookbook xml file. I am going to create a class XmlCookBookReader responsible for parsing the xml file. I am also going to create classes Cookbook and Recipe and populate them with the parsed information. Notice that unlike the examples above, the input file also contains an attribute 'title'.

<?xml version="1.0" encoding="UTF-8"?>
<cookbook>
    <recipe title="Bread">
        <ingredientList>
            <ingredient>flour</ingredient>
            <ingredient>water</ingredient>
            <ingredient>yeast</ingredient>
            <ingredient>salt</ingredient>
        </ingredientList>
        <instructions>Instructions to make bread.</instructions>
    </recipe>
    <recipe title="Pancake">
        <ingredientList>
            <ingredient>flour</ingredient>
            <ingredient>salt</ingredient>
            <ingredient>egg</ingredient>
            <ingredient>milk</ingredient>
            <ingredient>oil</ingredient>
        </ingredientList>
        <instructions>Instructions to make pancakes.</instructions>
    </recipe>
</cookbook>

The Cookbook and Recipe class

To represent the cookboook in our application we are going to create classes CookBook and Recipe. The class Recipe contains member variables title, instructions and a list of ingredients, as well as the necessary accessor, mutator and print() methods.

class Recipe
{
public:
    QString title() const {return m_title;}
    QStringList ingredients() const {return m_ingredients;}
    QString instructions() const {return m_instructions;}
    void setTitle(const QString& title) {this->m_title = title;}
    void setIngredients(const QStringList& ingredientList){
        this->m_ingredients = ingredientList;
    }
    void setInstructions(const QString& instructions){
        this->m_instructions = instructions;
    }
    void print() const;
private:
    QString m_title;
    QStringList m_ingredients;
    QString m_instructions;
};

The class CookBook contains a list of Recipe objects, as well as a mutator and print() method.

class CookBook
{
public:
    void addRecipe(Recipe* recipe);
    void print() const;
private:
    QList<Recipe*> m_recipes;
};

How to use the custom classes in main.cpp

The main.cpp file contains readXml(const QString& fileName). The function attempts to open an xml file of a given name and produces an error string in case it fails. If the file is opened correctly, we create an instance of the Cookbook class and pass its pointer to an instance of a XmlCookBookReader. After that, we read the xml file with xmlReader.read(&file) and print the contents of the cookbook.

void readXml(const QString& fileName){
    QFile file(fileName);
    if(!file.open(QFile::ReadOnly | QFile::Text)){
        qDebug() << "Cannot read file" << file.errorString();
        return;
    }

    CookBook* cookbook = new CookBook;
    XmlCookBookReader xmlReader(cookbook);

    if (!xmlReader.read(&file))
        qDebug() << "Parse error in file " << xmlReader.errorString();
    else
        cookbook->print();
}

int main(int argc, char *argv[])
{
    readXml("test.xml");
    return 0;
}

How to create the XmlCookBookReader class

To encapsulate the reading of the xml file we are going to create a class XmlCookBookReader. The class holds a pointer to a Cookbook instance that we wish to populate. It also contains the QXmlStreamReader object which provides the necessary xml reading functionality. The class also contains helper reading methods that separate the reading implementations of the individual subchildren.

class XmlCookBookReader
{
public:
    XmlCookBookReader(CookBook* cookbook);
    bool read(QIODevice *device);
    QString errorString() const;
private:
    QXmlStreamReader reader;
    CookBook* m_cookbook;

    void readCookBook();
    void readRecipe();
    void readTitle(Recipe *recipe);
    void readIngredientList(Recipe *recipe);
    void readInstructions(Recipe *recipe);
};

The constructor of the XmlCookBookReader is responsible for assigning the m_cookbook pointer to the CookBook object that we created in the main.cpp.

XmlCookBookReader::XmlCookBookReader(CookBook* cookbook)
{
    m_cookbook = cookbook;
}

The XmlCookBookReader::read(QIODevice *device) method is responsible for checking the name of the root element (in this case "cookbook"). If the name is correct, the method calls readCookBook(). The QXmlStreamReader accepts any type of QIODevice (in this implementation it happens to be a QFile).

bool XmlCookBookReader::read(QIODevice *device)
{
    reader.setDevice(device);

    if (reader.readNextStartElement()) {
        if (reader.name() == "cookbook")
            readCookBook();
        else
            reader.raiseError(QObject::tr("Not a cookbook file"));
    }
    return !reader.error();
}

In our case, the method XmlCookBookReader::read(QIODevice *device) will be called from main.cpp in the following way:

CookBook* cookbook = new CookBook;
XmlCookBookReader xmlReader(cookbook);
xmlReader.read(&file);

The method XmlCookBookReader::readCookBook() reads the contents of the "recipe" elements, and if need be, skips all elements that do not match "recipe".

void XmlCookBookReader::readCookBook(){
    while(reader.readNextStartElement()){
        if(reader.name() == "recipe")
            readRecipe();
        else
            reader.skipCurrentElement();
    }
}

The method XmlCookBookReader::readRecipe() reads a single recipe:

    <recipe title="Bread">
        <ingredientList>
            <ingredient>flour</ingredient>
            <ingredient>water</ingredient>
            <ingredient>yeast</ingredient>
            <ingredient>salt</ingredient>
        </ingredientList>
        <instructions>Instructions to make bread.</instructions>
    </recipe>

To read a single recipe, we assert that the reader.name() is really "recipe". After that we create an instance of Recipe, retrieve the value of the attribute "title" and with the help of the while statement iterate over the subchildren and call the appropriate helper reading methods to pupulate the Recipe. If need be, we skip unknown elements.

void XmlCookBookReader::readRecipe(){
    Q_ASSERT(reader.isStartElement() &&
             reader.name() == "recipe");

    Recipe* rec = new Recipe;

    readTitle(rec);
    while (reader.readNextStartElement()) {
        if (reader.name() == "ingredientList")
            readIngredientList(rec);
        else if (reader.name() == "instructions")
            readInstructions(rec);
        else
            reader.skipCurrentElement();
    }

    m_cookbook->addRecipe(rec);
}

To retrieve the value of the attribute "title" we are going to use the method XmlCookBookReader::readTitle().

    <recipe title="Bread">
        <ingredientList>
            <ingredient>flour</ingredient>
            <ingredient>water</ingredient>
            <ingredient>yeast</ingredient>
            <ingredient>salt</ingredient>
        </ingredientList>
        <instructions>Instructions to make bread.</instructions>
    </recipe>

Firstly, we assert that the attribute "title" exists, then we retrieve the value of the attribute with the help of reader.attributes().value("title").toString().

void XmlCookBookReader::readTitle(Recipe *recipe){
    Q_ASSERT(reader.name() == "recipe" &&
             reader.attributes().hasAttribute("title"));

    QString title =
            reader.attributes().value("title").toString();
    recipe->setTitle(title);
}

To read the 'instructions' we are going to use the method XmlCookBookReader::readInstructions().

    <recipe title="Bread">
        <ingredientList>
            <ingredient>flour</ingredient>
            <ingredient>water</ingredient>
            <ingredient>yeast</ingredient>
            <ingredient>salt</ingredient>
        </ingredientList>
        <instructions>Instructions to make bread.</instructions>
    </recipe>

Firsly, we assert that the reader.isStartElement() and that its name is "instructions". After that we call reader.readElementText() and set the instructions of the Recipe to the retrieved value.

void XmlCookBookReader::readInstructions(Recipe *recipe){
    Q_ASSERT(reader.isStartElement() &&
             reader.name() == "instructions");

    QString instructions = reader.readElementText();
    recipe->setInstructions(instructions);
}

To read the ingredients of the ingredientList we are going to use the method XmlCookBookReader::readIngredientList().

    <recipe title="Bread">
        <ingredientList>
            <ingredient>flour</ingredient>
            <ingredient>water</ingredient>
            <ingredient>yeast</ingredient>
            <ingredient>salt</ingredient>
        </ingredientList>
        <instructions>Instructions to make bread.</instructions>
    </recipe>

Once more, we assert that the reader.isStartElement() and that its name is "ingredientList". After that, we gather all the ingredients in a while loop. Finally, we set the ingredientList of the Recipe instance to the retrieved values.

void XmlCookBookReader::readIngredientList(Recipe *recipe){
    Q_ASSERT(reader.isStartElement() &&
             reader.name() == "ingredientList");

    QStringList ingredList;
    while(reader.readNextStartElement()){
        if(reader.name() == "ingredient"){
            QString ingr = reader.readElementText();
            ingredList.append(ingr);
        }
        else
            reader.skipCurrentElement();
    }
    recipe->setIngredients(ingredList);
}

Additional remark

Let's for a moment assume that our ingredient list contains the ingredient names as attributes, rather than element text, i.e.

<ingredient name="flour"/>

rather than

<ingredient>flour</ingredient>

This would change the code for the ingredients to the code below. Notice the additional reader.skipCurrentElement() in the if block.

void XmlCookBookReader::readIngredientList(Recipe *recipe){
    Q_ASSERT(reader.isStartElement() &&
             reader.name() == "ingredientList");

    QStringList ingredList;

    while(reader.readNextStartElement()){
        if(reader.name() == "ingredient" &&
                reader.attributes().hasAttribute("name")){
            QString ingr =
                    reader.attributes().value("name").toString();
            ingredList.append(ingr);
            reader.skipCurrentElement();
        }
        else
            reader.skipCurrentElement();
    }
    recipe->setIngredients(ingredList);
}
That's it! You can try out the example. You will get the following output:

Title: Bread
Ingredients: flour, water, yeast, salt
Instructions: Instructions to make bread.
Title: Pancake
Ingredients: flour, salt, egg, milk, oil
Instructions: Instructions to make pancakes.

Latest update: 06.08.2015
Created: 2015
© Walletfox.com, 2017