XMLWriter.cpp 19.47 KiB
/****************************************************************************
**
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia. For licensing terms and
** conditions see http://qt.digia.com/licensing. For further information
** use the contact form at http://qt.digia.com/contact-us.
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
** $QT_END_LICENSE$
****************************************************************************/
#include <QCoreApplication>
#include <QDateTime>
#include <QIODevice>
#include <QList>
#include <QPair>
#include <QStack>
#include <QtDebug>
#include "XMLWriter.h"
/* Issues:
 * - Switch to Qt's d-pointer semantics, if in Qt.
 * - Remove namespace(PatternistSDK), and change name, if in Qt.
 * - Is it really necessary to pass the tag name to endElement()?
 * - Could it be of interest to let the user control the encoding? Are those cases common
 *   enough to justify support in Qt? Using anything but UTF-8 or UTF-16
 *   means asking for trouble, from an interoperability perspective.
/* Design rationalis, comments:
 * - The class is called XMLWriter to harvest familiarity by being consistent with
 *   Java's XMLWriter class. If XMLWriter is moved to Qt, the name QXmlWriter is perhaps suitable.
 * - The class does not handle indentation because the "do one thing well"-principle is
 *   in use. XMLWriter should be fast and not assume a certain idea of indentation. Indentation
 *   should be implemented in a standalone QXmlContentHandler that performs the indentation and
 *   "has a" QXmlContentHandler which it in addition calls, and by that proxying/piping another
 *   QXmlContentHandler(which most likely is an XMLWriter). Thus, achieving a modularized,
 *   flexibly approach to indentation. A reason is also that indentation is very subjective.
 *   The indenter class should probably be called XMLIndenter/QXmlIndenter.
 * - It could be of interest to implement QXmlDTDHandler such that it would be possible to serialize
 *   DTDs. Must be done before BC becomes significant.
 * - I think the most valuable of this class is its Q_ASSERT tests. Many programmers have severe problems
 *   producing XML, and the tests helps them catching their mistakes. They therefore promote
 *   interoperability. Do not remove them. If any are wrong, fix them instead.
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
using namespace QPatternistSDK; /** * A namespace binding, prefix/namespace URI. */ typedef QPair<QString, QString> NSBinding; typedef QList<NSBinding> NSBindingList; #ifdef QT_NO_DEBUG # define DEBUG_CODE(code) #else # define DEBUG_CODE(code) code #endif class XMLWriter::Private { public: inline Private(QIODevice *devP) : insideCDATA(false), addModificationNote(false), dev(devP) { hasContentStack.push(true); } #ifdef QT_NO_DEBUG inline void validateQName(const QString &) const { } inline void verifyNS(const QString &) const { } #else /** * Simple test of that @p name is an acceptable QName. */ inline void validateQName(const QString &name) { Q_ASSERT_X(!name.isEmpty(), Q_FUNC_INFO, "An XML name cannot be empty."); Q_ASSERT_X(!name.endsWith(QLatin1Char(':')), Q_FUNC_INFO, "An XML name cannot end with a colon(QLatin1Char(':'))."); Q_ASSERT_X(!name.contains(QRegExp(QLatin1String("[ \t\n]"))), Q_FUNC_INFO, "An XML name cannot contain whitespace."); } /** * Ensures that the prefix of @p qName is declared. */ inline void verifyNS(const QString &qName) const { const QString prefix(qName.left(qName.indexOf(QLatin1Char(':')))); if(qName.contains(QLatin1Char(':')) && prefix != QLatin1String("xml")) { bool foundPrefix = false; const QStack<NSBindingList>::const_iterator end(namespaceTracker.constEnd()); QStack<NSBindingList>::const_iterator it(namespaceTracker.constBegin()); for(; it != end; ++it) { const NSBindingList::const_iterator lend((*it).constEnd()); NSBindingList::const_iterator lit((*it).constBegin()); for(; lit != lend; ++it) { if((*lit).first == prefix) { foundPrefix = true; break;
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
} } if(foundPrefix) break; } Q_ASSERT_X(foundPrefix, "XMLWriter::startElement()", qPrintable(QString::fromLatin1("The prefix %1 is not declared. All prefixes " "except 'xml' must be declared.").arg(prefix))); } } #endif inline QString escapeElementContent(const QString &ch) { const int l = ch.length(); QString retval; for(int i = 0; i != l; ++i) { const QChar c(ch.at(i)); if(c == QLatin1Char(QLatin1Char('&'))) retval += QLatin1String("&amp;"); else if(c == QLatin1Char(QLatin1Char('<'))) retval += QLatin1String("&lt;"); else retval += c; } return retval; } inline QString escapeAttributeContent(const QString &ch) { const int l = ch.length(); QString retval; for(int i = 0; i != l; ++i) { const QChar c(ch.at(i)); /* We don't have to escape '\'' because we use '\"' as attribute delimiter. */ if(c == QLatin1Char('&')) retval += QLatin1String("&amp;"); else if(c == QLatin1Char('<')) retval += QLatin1String("&lt;"); else if(c == QLatin1Char('"')) retval += QLatin1String("&quot;"); else retval += c; } return retval; } inline QString escapeCDATAContent(const QString &ch) { const int l = ch.length(); QString retval; qint8 atEnd = 0; for(int i = 0; i != l; ++i) { const QChar c(ch.at(i)); /* Escape '>' if in "]]>" */ if(c == QLatin1Char(']')) { if(atEnd == 0 || atEnd == 1)
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
++atEnd; else atEnd = 0; retval += QLatin1Char(']'); } else if(c == QLatin1Char('>')) { if(atEnd == 2) retval += QLatin1String("&gt;"); else { atEnd = 0; retval += QLatin1Char('>'); } } else retval += c; } return retval; } /** * We wrap dev in this function such that we can deploy the Q_ASSERT_X * macro in each place it's used. */ inline QIODevice *device() const { Q_ASSERT_X(dev, Q_FUNC_INFO, "No device specified for XMLWriter; one must be specified with " "setDevice() or via the constructor before XMLWriter can be used."); return dev; } /** * @returns true on success, otherwise false */ inline bool serialize(const QString &data) { const QByteArray utf8(data.toUtf8()); return device()->write(utf8) == utf8.size(); } /** * @returns true on success, otherwise false */ inline bool serialize(const char data) { return device()->putChar(data); } /** * @returns true on success, otherwise false */ inline bool serialize(const char *data) { return device()->write(data) == qstrlen(data); } inline bool hasElementContent() const { return hasContentStack.top(); } inline void handleElement() { if(!hasElementContent()) serialize('>');
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
/* This element is content for the parent. */ hasContentStack.top() = true; } NSBindingList namespaces; bool insideCDATA; bool addModificationNote; QString msg; QIODevice *dev; QStack<bool> hasContentStack; QString errorString; DEBUG_CODE(QStack<QString> tags;) DEBUG_CODE(QStack<NSBindingList> namespaceTracker;) }; /** * Reduces complexity. The empty else clause is for avoiding mess when macro * is used in the 'then' branch of an if clause, which is followed by an else clause. */ #define serialize(string) if(!d->serialize(string)) \ { \ d->errorString = d->device()->errorString(); \ return false; \ } \ else do {} while (false) XMLWriter::XMLWriter(QIODevice *outStream) : d(new Private(outStream)) { } XMLWriter::~XMLWriter() { delete d; } bool XMLWriter::startDocument() { if(!device()->isOpen() && !device()->open(QIODevice::WriteOnly)) return false; if(d->addModificationNote) { if(d->msg.isNull()) { d->msg = QString::fromLatin1("NOTE: This file was automatically generated " "by %1 at %2. All changes to this file will be lost.") .arg(QCoreApplication::instance()->applicationName(), QDateTime::currentDateTime().toString()); } if(!comment(d->msg)) return false; serialize('\n'); } serialize(QLatin1String("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")); return true; } bool XMLWriter::startElement(const QString &/*namespaceURI*/, const QString &/*localName*/, const QString &qName, const QXmlAttributes &atts) { return startElement(qName, atts); } bool XMLWriter::startElement(const QString &qName,
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
const QXmlAttributes &atts) { Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO, "Only characters() can be received when inside CDATA."); Q_ASSERT_X(!qName.startsWith(QLatin1String("xmlns")), Q_FUNC_INFO, "startElement should not be used for declaring prefixes, " "use startPrefixMapping() for that."); d->validateQName(qName); d->verifyNS(qName); d->handleElement(); serialize('<'); serialize(qName); DEBUG_CODE(d->tags.push(qName)); DEBUG_CODE(d->namespaceTracker.push(d->namespaces)); /* Add namespace declarations. */ const NSBindingList::const_iterator end(d->namespaces.constEnd()); NSBindingList::const_iterator it(d->namespaces.constBegin()); for(; it != end; ++it) { if((*it).first.isEmpty()) serialize(" xmlns="); else { serialize(" xmlns:"); serialize((*it).first); serialize('='); } serialize('"'); serialize(d->escapeElementContent((*it).second)); serialize('"'); } d->namespaces.clear(); const int c = atts.count(); /* Serialize attributes. */ for(int i = 0; i != c; ++i) { d->validateQName(atts.qName(i)); d->verifyNS(atts.qName(i)); serialize(' '); serialize(atts.qName(i)); serialize("=\""); serialize(d->escapeAttributeContent(atts.value(i))); serialize('"'); } d->hasContentStack.push(false); return true; } bool XMLWriter::endElement(const QString &/*namespaceURI*/, const QString &/*localName*/, const QString &qName) { return endElement(qName); } bool XMLWriter::endElement(const QString &qName) { Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO, "Only characters() can be received when inside CDATA.");
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
Q_ASSERT_X(d->tags.pop() == qName, Q_FUNC_INFO, "The element tags are not balanced, the produced XML is invalid."); DEBUG_CODE(d->namespaceTracker.pop()); /* "this" element is content for our parent, so ensure hasElementContent is true. */ if(d->hasElementContent()) { serialize(QLatin1String("</")); serialize(qName); serialize('>'); } else serialize(QLatin1String("/>")); d->hasContentStack.pop(); return true; } bool XMLWriter::startPrefixMapping(const QString &prefix, const QString &uri) { Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO, "Only characters() can be received when inside CDATA."); Q_ASSERT_X(prefix.toLower() != QLatin1String("xml") || (prefix.toLower() == QLatin1String("xml") && (uri == QLatin1String("http://www.w3.org/TR/REC-xml-names/") || uri.isEmpty())), Q_FUNC_INFO, "The prefix 'xml' can only be bound to the namespace " "\"http://www.w3.org/TR/REC-xml-names/\"."); Q_ASSERT_X(prefix.toLower() != QLatin1String("xml") && uri != QLatin1String("http://www.w3.org/TR/REC-xml-names/"), Q_FUNC_INFO, "The namespace \"http://www.w3.org/TR/REC-xml-names/\" can only be bound to the " "\"xml\" prefix."); d->namespaces.append(qMakePair(prefix, uri)); return true; } bool XMLWriter::processingInstruction(const QString &target, const QString &data) { Q_ASSERT_X(target.toLower() != QLatin1String("xml"), Q_FUNC_INFO, "A processing instruction cannot have the name xml in any " "capitalization, because it is reserved."); Q_ASSERT_X(!data.contains(QLatin1String("?>")), Q_FUNC_INFO, "The content of a processing instruction cannot contain the string \"?>\"."); Q_ASSERT_X(!d->insideCDATA, "XMLWriter::processingInstruction()", "Only characters() can be received when inside CDATA."); d->handleElement(); serialize(QLatin1String("<?")); serialize(target); serialize(' '); serialize(data); serialize(QLatin1String("?>")); return true; } bool XMLWriter::characters(const QString &ch) { Q_ASSERT_X(d->tags.count() >= 1, Q_FUNC_INFO, "Text nodes can only appear inside elements(no elements sent)."); d->handleElement(); if(d->insideCDATA)
491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
serialize(d->escapeCDATAContent(ch)); else serialize(d->escapeElementContent(ch)); return true; } bool XMLWriter::comment(const QString &ch) { Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO, "Only characters() can be received when inside CDATA."); Q_ASSERT_X(!ch.contains(QLatin1String("--")), Q_FUNC_INFO, "XML comments may not contain double-hyphens(\"--\")."); Q_ASSERT_X(!ch.endsWith(QLatin1Char('-')), Q_FUNC_INFO, "XML comments cannot end with a hyphen, \"-\"(add a space, for example)."); /* A comment starting with "<!---" is ok. */ d->handleElement(); serialize(QLatin1String("<!--")); serialize(ch); serialize(QLatin1String("-->")); return true; } bool XMLWriter::startCDATA() { Q_ASSERT_X(d->insideCDATA, Q_FUNC_INFO, "startCDATA() has already been called."); Q_ASSERT_X(d->tags.count() >= 1, Q_FUNC_INFO, "CDATA sections can only appear inside elements(no elements sent)."); d->insideCDATA = true; serialize(QLatin1String("<![CDATA[")); return true; } bool XMLWriter::endCDATA() { d->insideCDATA = false; serialize("]]>"); return true; } bool XMLWriter::startDTD(const QString &name, const QString &publicId, const QString &systemId) { Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO, "Only characters() can be received when inside CDATA."); Q_ASSERT_X(!name.isEmpty(), Q_FUNC_INFO, "The DOCTYPE name cannot be empty."); Q_ASSERT_X(d->tags.isEmpty() && d->namespaces.isEmpty(), Q_FUNC_INFO, "No content such as namespace declarations or elements can be serialized " "before the DOCTYPE declaration, the XML is invalid."); Q_ASSERT_X(!publicId.contains(QLatin1Char('"')), Q_FUNC_INFO, "The PUBLIC ID cannot contain quotes('\"')."); Q_ASSERT_X(!systemId.contains(QLatin1Char('"')), Q_FUNC_INFO, "The SYSTEM ID cannot contain quotes('\"')."); serialize(QLatin1String("<!DOCTYPE ")); serialize(name); if(!publicId.isEmpty()) { Q_ASSERT_X(!systemId.isEmpty(), Q_FUNC_INFO, "When a public identifier is specified, a system identifier " "must also be specified in order to produce valid XML."); serialize(" PUBLIC \""); serialize(publicId);
561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
serialize('"'); } if(!systemId.isEmpty()) { if (publicId.isEmpty()) { serialize(" SYSTEM"); } serialize(" \""); serialize(systemId); serialize('"'); } return true; } bool XMLWriter::endDTD() { Q_ASSERT_X(d->tags.isEmpty() && d->namespaces.isEmpty(), Q_FUNC_INFO, "Content such as namespace declarations or elements cannot occur inside " "the DOCTYPE declaration, the XML is invalid."); serialize(QLatin1String(">\n")); return true; } bool XMLWriter::startEntity(const QString &) { return true; } bool XMLWriter::endEntity(const QString &) { return true; } void XMLWriter::setMessage(const QString &msg) { d->msg = msg; } QString XMLWriter::modificationMessage() const { return d->msg; } bool XMLWriter::endDocument() { Q_ASSERT_X(d->tags.isEmpty(), Q_FUNC_INFO, "endDocument() called before all elements were closed with endElement()."); d->device()->close(); return true; } QString XMLWriter::errorString() const { return d->errorString; } bool XMLWriter::ignorableWhitespace(const QString &ch) { return characters(ch); } bool XMLWriter::endPrefixMapping(const QString &) { /* Again, should we do something with this? */ return true; }
631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
bool XMLWriter::skippedEntity(const QString &) { return true; } void XMLWriter::setDocumentLocator(QXmlLocator *) { } QIODevice *XMLWriter::device() const { return d->dev; } void XMLWriter::setDevice(QIODevice *dev) { d->dev = dev; } void XMLWriter::setAddMessage(const bool toggle) { d->addModificationNote = toggle; } bool XMLWriter::addModificationMessage() const { return d->addModificationNote; } #undef serialize // vim: et:ts=4:sw=4:sts=4