| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the test suite of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| ** 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 The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $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. |
| */ |
| |
| using namespace QPatternistSDK; |
| |
| /** |
| * A namespace binding, prefix/namespace URI. |
| */ |
| typedef QPair<QString, QString> NSBinding; |
| typedef QList<NSBinding> NSBindingList; |
| |
| 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; |
| } |
| } |
| 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("&"); |
| else if(c == QLatin1Char(QLatin1Char('<'))) |
| retval += QLatin1String("<"); |
| 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("&"); |
| else if(c == QLatin1Char('<')) |
| retval += QLatin1String("<"); |
| else if(c == QLatin1Char('"')) |
| retval += QLatin1String("""); |
| 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) |
| ++atEnd; |
| else |
| atEnd = 0; |
| |
| retval += QLatin1Char(']'); |
| } |
| else if(c == QLatin1Char('>')) |
| { |
| if(atEnd == 2) |
| retval += QLatin1String(">"); |
| 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('>'); |
| |
| /* 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; |
| QStack<QString> tags; |
| 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 &qName, const QXmlStreamAttributes &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); |
| |
| d->tags.push(qName); |
| 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(); |
| |
| for (const auto &attr : atts) { |
| const auto qName = attr.qualifiedName().toString(); |
| |
| d->validateQName(qName); |
| d->verifyNS(qName); |
| |
| serialize(' '); |
| serialize(qName); |
| serialize("=\""); |
| serialize(d->escapeAttributeContent(attr.value().toString())); |
| serialize('"'); |
| } |
| |
| d->hasContentStack.push(false); |
| return true; |
| } |
| |
| bool XMLWriter::endElement(const QString &qName) |
| { |
| Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO, |
| "Only characters() can be received when inside CDATA."); |
| Q_ASSERT_X(d->tags.pop() == qName, Q_FUNC_INFO, |
| "The element tags are not balanced, the produced XML is invalid."); |
| |
| 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) |
| 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); |
| 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); |
| } |
| |
| 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 |
| |