Implementing IXmlWriter Part 4: Collapsing Empty Elements

This is part 4 of my Implementing IXmlWriter post series.

One of the enhancements that XML introduced over SGML was a shorthand for specifying an element with no content by adding a trailing slash at the end of an open element. For example, <br/> is equivalent to <br></br>. Let’s add this functionality to the previous version of IXmlWriter.

Here’s the test case:

StringXmlWriter xmlWriter;

xmlWriter.WriteStartElement("root");
  xmlWriter.WriteStartElement("emptyElement");
  xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();

std::string strXML = xmlWriter.GetXmlString();
// strXML should be <root><emptyElement/></root>

How does this affect our previous implementation?

  1. We clearly cannot write the > character to close the start element in WriteStartElement().
  2. WriteEndElement() needs to be able to detect if a still-opened start element was written and if so, to write the /> sequence. Otherwise, it needs to write the full end element string.
  3. Because the > character will not be written in WriteStartElement(), we also need to worry about closing the start element in virtually every function.

The simplest way to implement this feature that I can think of is to keep an extra bool which remembers whether a unclosed start element has been written, and to handle this case in all relevant functions. Because we have all the previous test cases, I simply made the relevant changes to WriteStartElement() and then kept running the test cases, adding code as necessary to fix failures. I ended up with the following implementation:

class StringXmlWriter
{
private:
    std::stack<std::string> m_openedElements;
    std::string m_xmlStr;
    bool m_unclosedStartElement;

public:
    StringXmlWriter() : m_unclosedStartElement(false) {}

    void WriteStartElement(const std::string& localName)
    {
        if (m_unclosedStartElement) {
            m_xmlStr += '>';
            m_unclosedStartElement = false;
        }

        m_openedElements.push(localName);
        m_xmlStr += '<';
        m_xmlStr += localName;
        m_unclosedStartElement = true;
    }

    void WriteEndElement()
    {
        if (m_unclosedStartElement) {
            m_xmlStr += "/>";
            m_unclosedStartElement = false;
        } else {
            std::string lastOpenedElement = m_openedElements.top();
            m_xmlStr += "</";
            m_xmlStr += lastOpenedElement;
            m_xmlStr += '>';
        }
        m_openedElements.pop();
    }

    void WriteString(const std::string& value)
    {
        if (m_unclosedStartElement) {
            m_xmlStr += '>';
            m_unclosedStartElement = false;
        }

        typedef std::string::const_iterator iter_t;
        for (iter_t iter = value.begin(); iter != value.end(); ++iter) {
            if (*iter == '&') {
                m_xmlStr += "&amp;";
            } else if (*iter == '<') {
                m_xmlStr += "&lt;";
            } else if (*iter == '>') {
                m_xmlStr += "&gt;";
            } else {
                m_xmlStr += *iter;
            }
        }
    }

    void WriteElementString(const std::string& localName,
                            const std::string& value)
    {
        WriteStartElement(localName);
        WriteString(value);
        WriteEndElement();
    }

    std::string GetXmlString() const
    {
        return m_xmlStr;
    }
};

Remember, although the bool-based approach may not be the most elegant nor the eventual long-term solution, the idea with test-driven development is to write the simplest code possible to pass the existing test cases. If future test cases show the need, I will freely change this implementation detail, as the growing test suite allows me to make changes with great confidence.

About Steven Engelhardt, CFA, AIF
Adjunct Professor of Software Engineering at DePaul University • Software Engineering, Data & Analytics in FinTech • Lives in Chicago, IL

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s