4eb1af46ef3b088286faf4569989e91b6d9b114a
[profile/ivi/qtxmlpatterns.git] / tests / auto / xmlpatternssdk / XMLWriter.cpp
1 /****************************************************************************
2 **
3 ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
6 **
7 ** This file is part of the test suite of the Qt Toolkit.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** GNU Lesser General Public License Usage
11 ** This file may be used under the terms of the GNU Lesser General Public
12 ** License version 2.1 as published by the Free Software Foundation and
13 ** appearing in the file LICENSE.LGPL included in the packaging of this
14 ** file. Please review the following information to ensure the GNU Lesser
15 ** General Public License version 2.1 requirements will be met:
16 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
17 **
18 ** In addition, as a special exception, Nokia gives you certain additional
19 ** rights. These rights are described in the Nokia Qt LGPL Exception
20 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
21 **
22 ** GNU General Public License Usage
23 ** Alternatively, this file may be used under the terms of the GNU General
24 ** Public License version 3.0 as published by the Free Software Foundation
25 ** and appearing in the file LICENSE.GPL included in the packaging of this
26 ** file. Please review the following information to ensure the GNU General
27 ** Public License version 3.0 requirements will be met:
28 ** http://www.gnu.org/copyleft/gpl.html.
29 **
30 ** Other Usage
31 ** Alternatively, this file may be used in accordance with the terms and
32 ** conditions contained in a signed written agreement between you and Nokia.
33 **
34 **
35 **
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include <QCoreApplication>
43 #include <QDateTime>
44 #include <QIODevice>
45 #include <QList>
46 #include <QPair>
47 #include <QStack>
48 #include <QtDebug>
49
50 #include "XMLWriter.h"
51
52 /* Issues:
53  * - Switch to Qt's d-pointer semantics, if in Qt.
54  * - Remove namespace(PatternistSDK), and change name, if in Qt.
55  * - Is it really necessary to pass the tag name to endElement()?
56  * - Could it be of interest to let the user control the encoding? Are those cases common
57  *   enough to justify support in Qt? Using anything but UTF-8 or UTF-16
58  *   means asking for trouble, from an interoperability perspective.
59  */
60
61 /* Design rationalis, comments:
62  *
63  * - The class is called XMLWriter to harvest familarity by being consistent with
64  *   Java's XMLWriter class. If XMLWriter is moved to Qt, the name QXmlWriter is perhaps suitable.
65  * - The class does not handle indentation because the "do one thing well"-principle is
66  *   in use. XMLWriter should be fast and not assume a certain idea of indentation. Indentation
67  *   should be implemented in a standalone QXmlContentHandler that performs the indentation and
68  *   "has a" QXmlContentHandler which it in addition calls, and by that proxying/piping another
69  *   QXmlContentHandler(which most likely is an XMLWriter). Thus, achieving a modularized,
70  *   flexibly approach to indentation. A reason is also that indentation is very subjective.
71  *   The indenter class should probably be called XMLIndenter/QXmlIndenter.
72  * - It could be of interest to implement QXmlDTDHandler such that it would be possible to serialize
73  *   DTDs. Must be done before BC becomes significant.
74  * - I think the most valuable of this class is its Q_ASSERT tests. Many programmers have severe problems
75  *   producing XML, and the tests helps them catching their mistakes. They therefore promote
76  *   interoperability. Do not remove them. If any are wrong, fix them instead.
77  */
78
79 using namespace QPatternistSDK;
80
81 /**
82  * A namespace binding, prefix/namespace URI.
83  */
84 typedef QPair<QString, QString> NSBinding;
85 typedef QList<NSBinding> NSBindingList;
86
87 #ifdef QT_NO_DEBUG
88 #   define DEBUG_CODE(code)
89 #else
90 #   define DEBUG_CODE(code) code
91 #endif
92
93 class XMLWriter::Private
94 {
95 public:
96     inline Private(QIODevice *devP) : insideCDATA(false),
97                                      addModificationNote(false),
98                                      dev(devP)
99     {
100         hasContentStack.push(true);
101     }
102
103 #ifdef QT_NO_DEBUG
104     inline void validateQName(const QString &) const
105     {
106     }
107
108     inline void verifyNS(const QString &) const
109     {
110     }
111 #else
112     /**
113      * Simple test of that @p name is an acceptable QName.
114      */
115     inline void validateQName(const QString &name)
116     {
117         Q_ASSERT_X(!name.isEmpty(), Q_FUNC_INFO,
118                    "An XML name cannot be empty.");
119         Q_ASSERT_X(!name.endsWith(QLatin1Char(':')), Q_FUNC_INFO,
120                    "An XML name cannot end with a colon(QLatin1Char(':')).");
121         Q_ASSERT_X(!name.contains(QRegExp(QLatin1String("[ \t\n]"))), Q_FUNC_INFO,
122                    "An XML name cannot contain whitespace.");
123     }
124
125     /**
126      * Ensures that the prefix of @p qName is declared.
127      */
128     inline void verifyNS(const QString &qName) const
129     {
130         const QString prefix(qName.left(qName.indexOf(QLatin1Char(':'))));
131
132         if(qName.contains(QLatin1Char(':')) && prefix != QLatin1String("xml"))
133         {
134             bool foundPrefix = false;
135             const QStack<NSBindingList>::const_iterator end(namespaceTracker.constEnd());
136             QStack<NSBindingList>::const_iterator it(namespaceTracker.constBegin());
137
138             for(; it != end; ++it)
139             {
140                 const NSBindingList::const_iterator lend((*it).constEnd());
141                 NSBindingList::const_iterator lit((*it).constBegin());
142
143                 for(; lit != lend; ++it)
144                 {
145                     if((*lit).first == prefix)
146                     {
147                         foundPrefix = true;
148                         break;
149                     }
150                 }
151                 if(foundPrefix)
152                     break;
153             }
154
155             Q_ASSERT_X(foundPrefix, "XMLWriter::startElement()",
156                        qPrintable(QString::fromLatin1("The prefix %1 is not declared. All prefixes "
157                                           "except 'xml' must be declared.").arg(prefix)));
158         }
159     }
160 #endif
161
162     inline QString escapeElementContent(const QString &ch)
163     {
164         const int l = ch.length();
165         QString retval;
166
167         for(int i = 0; i != l; ++i)
168         {
169             const QChar c(ch.at(i));
170
171             if(c == QLatin1Char(QLatin1Char('&')))
172                 retval += QLatin1String("&amp;");
173             else if(c == QLatin1Char(QLatin1Char('<')))
174                 retval += QLatin1String("&lt;");
175             else
176                 retval += c;
177         }
178
179         return retval;
180     }
181
182     inline QString escapeAttributeContent(const QString &ch)
183     {
184         const int l = ch.length();
185         QString retval;
186
187         for(int i = 0; i != l; ++i)
188         {
189             const QChar c(ch.at(i));
190
191             /* We don't have to escape '\'' because we use '\"' as attribute delimiter. */
192             if(c == QLatin1Char('&'))
193                 retval += QLatin1String("&amp;");
194             else if(c == QLatin1Char('<'))
195                 retval += QLatin1String("&lt;");
196             else if(c == QLatin1Char('"'))
197                 retval += QLatin1String("&quot;");
198             else
199                 retval += c;
200         }
201
202         return retval;
203     }
204
205     inline QString escapeCDATAContent(const QString &ch)
206     {
207         const int l = ch.length();
208         QString retval;
209         qint8 atEnd = 0;
210
211         for(int i = 0; i != l; ++i)
212         {
213             const QChar c(ch.at(i));
214
215             /* Escape '>' if in "]]>" */
216             if(c == QLatin1Char(']'))
217             {
218                 if(atEnd == 0 || atEnd == 1)
219                     ++atEnd;
220                 else
221                     atEnd = 0;
222
223                 retval += QLatin1Char(']');
224             }
225             else if(c == QLatin1Char('>'))
226             {
227                 if(atEnd == 2)
228                     retval += QLatin1String("&gt;");
229                 else
230                 {
231                     atEnd = 0;
232                     retval += QLatin1Char('>');
233                 }
234             }
235             else
236                 retval += c;
237         }
238
239         return retval;
240     }
241
242     /**
243      * We wrap dev in this function such that we can deploy the Q_ASSERT_X
244      * macro in each place it's used.
245      */
246     inline QIODevice *device() const
247     {
248         Q_ASSERT_X(dev, Q_FUNC_INFO,
249                    "No device specified for XMLWriter; one must be specified with "
250                    "setDevice() or via the constructor before XMLWriter can be used.");
251         return dev;
252     }
253
254     /**
255      * @returns true on success, otherwise false
256      */
257     inline bool serialize(const QString &data)
258     {
259         const QByteArray utf8(data.toUtf8());
260
261         return device()->write(utf8) == utf8.size();
262     }
263
264     /**
265      * @returns true on success, otherwise false
266      */
267     inline bool serialize(const char data)
268     {
269         return device()->putChar(data);
270     }
271
272     /**
273      * @returns true on success, otherwise false
274      */
275     inline bool serialize(const char *data)
276     {
277         return device()->write(data) == qstrlen(data);
278     }
279
280     inline bool hasElementContent() const
281     {
282         return hasContentStack.top();
283     }
284
285     inline void handleElement()
286     {
287         if(!hasElementContent())
288             serialize('>');
289
290         /* This element is content for the parent. */
291         hasContentStack.top() = true;
292     }
293
294     NSBindingList namespaces;
295     bool insideCDATA;
296     bool addModificationNote;
297     QString msg;
298     QIODevice *dev;
299     QStack<bool> hasContentStack;
300     QString errorString;
301     DEBUG_CODE(QStack<QString> tags;)
302     DEBUG_CODE(QStack<NSBindingList> namespaceTracker;)
303 };
304
305 /**
306  * Reduces complexity. The empty else clause is for avoiding mess when macro
307  * is used in the 'then' branch of an if clause, which is followed by an else clause.
308  */
309 #define serialize(string) if(!d->serialize(string)) \
310                           { \
311                               d->errorString = d->device()->errorString(); \
312                               return false; \
313                           } \
314                           else do {} while (false)
315
316 XMLWriter::XMLWriter(QIODevice *outStream) : d(new Private(outStream))
317 {
318 }
319
320 XMLWriter::~XMLWriter()
321 {
322     delete d;
323 }
324
325 bool XMLWriter::startDocument()
326 {
327     if(!device()->isOpen() && !device()->open(QIODevice::WriteOnly))
328         return false;
329
330     if(d->addModificationNote)
331     {
332         if(d->msg.isNull())
333         {
334             d->msg = QString::fromLatin1("NOTE: This file was automatically generated "
335                                          "by %1 at %2. All changes to this file will be lost.")
336                                          .arg(QCoreApplication::instance()->applicationName(),
337                                               QDateTime::currentDateTime().toString());
338         }
339         if(!comment(d->msg))
340             return false;
341
342         serialize('\n');
343     }
344
345     serialize(QLatin1String("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"));
346
347     return true;
348 }
349
350 bool XMLWriter::startElement(const QString &/*namespaceURI*/,
351                              const QString &/*localName*/,
352                              const QString &qName,
353                              const QXmlAttributes &atts)
354 {
355     return startElement(qName, atts);
356 }
357
358 bool XMLWriter::startElement(const QString &qName,
359                              const QXmlAttributes &atts)
360 {
361     Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO,
362                "Only characters() can be received when inside CDATA.");
363     Q_ASSERT_X(!qName.startsWith(QLatin1String("xmlns")), Q_FUNC_INFO,
364                "startElement should not be used for declaring prefixes, "
365                "use startPrefixMapping() for that.");
366
367     d->validateQName(qName);
368     d->verifyNS(qName);
369
370     d->handleElement();
371
372     serialize('<');
373     serialize(qName);
374
375     DEBUG_CODE(d->tags.push(qName));
376     DEBUG_CODE(d->namespaceTracker.push(d->namespaces));
377
378     /* Add namespace declarations. */
379     const NSBindingList::const_iterator end(d->namespaces.constEnd());
380     NSBindingList::const_iterator it(d->namespaces.constBegin());
381
382     for(; it != end; ++it)
383     {
384         if((*it).first.isEmpty())
385             serialize(" xmlns=");
386         else
387         {
388             serialize(" xmlns:");
389             serialize((*it).first);
390             serialize('=');
391         }
392
393         serialize('"');
394         serialize(d->escapeElementContent((*it).second));
395         serialize('"');
396     }
397     d->namespaces.clear();
398
399     const int c = atts.count();
400
401     /* Serialize attributes. */
402     for(int i = 0; i != c; ++i)
403     {
404         d->validateQName(atts.qName(i));
405         d->verifyNS(atts.qName(i));
406
407         serialize(' ');
408         serialize(atts.qName(i));
409         serialize("=\"");
410         serialize(d->escapeAttributeContent(atts.value(i)));
411         serialize('"');
412     }
413
414     d->hasContentStack.push(false);
415     return true;
416 }
417
418 bool XMLWriter::endElement(const QString &/*namespaceURI*/,
419                            const QString &/*localName*/,
420                            const QString &qName)
421 {
422     return endElement(qName);
423 }
424
425 bool XMLWriter::endElement(const QString &qName)
426 {
427     Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO,
428                "Only characters() can be received when inside CDATA.");
429     Q_ASSERT_X(d->tags.pop() == qName, Q_FUNC_INFO,
430                "The element tags are not balanced, the produced XML is invalid.");
431
432     DEBUG_CODE(d->namespaceTracker.pop());
433
434     /* "this" element is content for our parent, so ensure hasElementContent is true. */
435
436     if(d->hasElementContent())
437     {
438         serialize(QLatin1String("</"));
439         serialize(qName);
440         serialize('>');
441     }
442     else
443         serialize(QLatin1String("/>"));
444
445     d->hasContentStack.pop();
446
447     return true;
448 }
449
450 bool XMLWriter::startPrefixMapping(const QString &prefix, const QString &uri)
451 {
452     Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO,
453                "Only characters() can be received when inside CDATA.");
454     Q_ASSERT_X(prefix.toLower() != QLatin1String("xml") ||
455                (prefix.toLower() == QLatin1String("xml") &&
456                (uri == QLatin1String("http://www.w3.org/TR/REC-xml-names/") ||
457                                               uri.isEmpty())),
458                Q_FUNC_INFO,
459                "The prefix 'xml' can only be bound to the namespace "
460                "\"http://www.w3.org/TR/REC-xml-names/\".");
461     Q_ASSERT_X(prefix.toLower() != QLatin1String("xml") &&
462                 uri != QLatin1String("http://www.w3.org/TR/REC-xml-names/"),
463                Q_FUNC_INFO,
464                "The namespace \"http://www.w3.org/TR/REC-xml-names/\" can only be bound to the "
465                "\"xml\" prefix.");
466
467     d->namespaces.append(qMakePair(prefix, uri));
468     return true;
469 }
470
471 bool XMLWriter::processingInstruction(const QString &target,
472                                       const QString &data)
473 {
474     Q_ASSERT_X(target.toLower() != QLatin1String("xml"), Q_FUNC_INFO,
475                "A processing instruction cannot have the name xml in any "
476                "capitalization, because it is reserved.");
477     Q_ASSERT_X(!data.contains(QLatin1String("?>")), Q_FUNC_INFO,
478                "The content of a processing instruction cannot contain the string \"?>\".");
479     Q_ASSERT_X(!d->insideCDATA, "XMLWriter::processingInstruction()",
480                "Only characters() can be received when inside CDATA.");
481
482     d->handleElement();
483
484     serialize(QLatin1String("<?"));
485     serialize(target);
486     serialize(' ');
487     serialize(data);
488     serialize(QLatin1String("?>"));
489     return true;
490 }
491
492 bool XMLWriter::characters(const QString &ch)
493 {
494     Q_ASSERT_X(d->tags.count() >= 1, Q_FUNC_INFO,
495                "Text nodes can only appear inside elements(no elements sent).");
496     d->handleElement();
497
498     if(d->insideCDATA)
499         serialize(d->escapeCDATAContent(ch));
500     else
501         serialize(d->escapeElementContent(ch));
502
503     return true;
504 }
505
506 bool XMLWriter::comment(const QString &ch)
507 {
508     Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO,
509                "Only characters() can be received when inside CDATA.");
510     Q_ASSERT_X(!ch.contains(QLatin1String("--")), Q_FUNC_INFO,
511                "XML comments may not contain double-hyphens(\"--\").");
512     Q_ASSERT_X(!ch.endsWith(QLatin1Char('-')), Q_FUNC_INFO,
513                "XML comments cannot end with a hyphen, \"-\"(add a space, for example).");
514     /* A comment starting with "<!---" is ok. */
515
516     d->handleElement();
517
518     serialize(QLatin1String("<!--"));
519     serialize(ch);
520     serialize(QLatin1String("-->"));
521
522     return true;
523 }
524
525 bool XMLWriter::startCDATA()
526 {
527     Q_ASSERT_X(d->insideCDATA, Q_FUNC_INFO,
528                "startCDATA() has already been called.");
529     Q_ASSERT_X(d->tags.count() >= 1, Q_FUNC_INFO,
530                "CDATA sections can only appear inside elements(no elements sent).");
531     d->insideCDATA = true;
532     serialize(QLatin1String("<![CDATA["));
533     return true;
534 }
535
536 bool XMLWriter::endCDATA()
537 {
538     d->insideCDATA = false;
539     serialize("]]>");
540     return true;
541 }
542
543 bool XMLWriter::startDTD(const QString &name,
544                          const QString &publicId,
545                          const QString &systemId)
546 {
547     Q_ASSERT_X(!d->insideCDATA, Q_FUNC_INFO,
548                "Only characters() can be received when inside CDATA.");
549     Q_ASSERT_X(!name.isEmpty(), Q_FUNC_INFO,
550                "The DOCTYPE name cannot be empty.");
551     Q_ASSERT_X(d->tags.isEmpty() && d->namespaces.isEmpty(), Q_FUNC_INFO,
552                "No content such as namespace declarations or elements can be serialized "
553                "before the DOCTYPE declaration, the XML is invalid.");
554     Q_ASSERT_X(!publicId.contains(QLatin1Char('"')), Q_FUNC_INFO,
555                "The PUBLIC ID cannot contain quotes('\"').");
556     Q_ASSERT_X(!systemId.contains(QLatin1Char('"')), Q_FUNC_INFO,
557                "The SYSTEM ID cannot contain quotes('\"').");
558
559     serialize(QLatin1String("<!DOCTYPE "));
560     serialize(name);
561
562     if(!publicId.isEmpty())
563     {
564         Q_ASSERT_X(!systemId.isEmpty(), Q_FUNC_INFO,
565                    "When a public identifier is specified, a system identifier "
566                    "must also be specified in order to produce valid XML.");
567         serialize(" PUBLIC \"");
568         serialize(publicId);
569         serialize('"');
570     }
571
572     if(!systemId.isEmpty())
573     {
574         if(publicId.isEmpty())
575             serialize(" SYSTEM");
576
577         serialize(" \"");
578         serialize(systemId);
579         serialize('"');
580     }
581
582     return true;
583 }
584
585 bool XMLWriter::endDTD()
586 {
587     Q_ASSERT_X(d->tags.isEmpty() && d->namespaces.isEmpty(), Q_FUNC_INFO,
588                "Content such as namespace declarations or elements cannot occur inside "
589                "the DOCTYPE declaration, the XML is invalid.");
590     serialize(QLatin1String(">\n"));
591     return true;
592 }
593
594 bool XMLWriter::startEntity(const QString &)
595 {
596     return true;
597 }
598
599 bool XMLWriter::endEntity(const QString &)
600 {
601     return true;
602 }
603
604 void XMLWriter::setMessage(const QString &msg)
605 {
606     d->msg = msg;
607 }
608
609 QString XMLWriter::modificationMessage() const
610 {
611     return d->msg;
612 }
613
614 bool XMLWriter::endDocument()
615 {
616     Q_ASSERT_X(d->tags.isEmpty(), Q_FUNC_INFO,
617                "endDocument() called before all elements were closed with endElement().");
618     d->device()->close();
619     return true;
620 }
621
622 QString XMLWriter::errorString() const
623 {
624     return d->errorString;
625 }
626
627 bool XMLWriter::ignorableWhitespace(const QString &ch)
628 {
629     return characters(ch);
630 }
631
632 bool XMLWriter::endPrefixMapping(const QString &)
633 {
634     /* Again, should we do something with this? */
635     return true;
636 }
637
638 bool XMLWriter::skippedEntity(const QString &)
639 {
640     return true;
641 }
642
643 void XMLWriter::setDocumentLocator(QXmlLocator *)
644 {
645 }
646
647 QIODevice *XMLWriter::device() const
648 {
649     return d->dev;
650 }
651
652 void XMLWriter::setDevice(QIODevice *dev)
653 {
654     d->dev = dev;
655 }
656
657 void XMLWriter::setAddMessage(const bool toggle)
658 {
659     d->addModificationNote = toggle;
660 }
661
662 bool XMLWriter::addModificationMessage() const
663 {
664     return d->addModificationNote;
665 }
666
667 #undef serialize
668 // vim: et:ts=4:sw=4:sts=4
669