File indexing completed on 2024-04-28 04:41:56

0001 /* This file is part of the KDE project
0002  * Copyright (C) 2001-2007 by OpenMFG, LLC <info@openmfg.com>
0003  * Copyright (C) 2007-2010 by Adam Pigg <adam@piggz.co.uk>
0004  * Copyright (C) 2011-2015 Jarosław Staniek <staniek@kde.org>
0005  *
0006  * This library is free software; you can redistribute it and/or
0007  * modify it under the terms of the GNU Lesser General Public
0008  * License as published by the Free Software Foundation; either
0009  * version 2.1 of the License, or (at your option) any later version.
0010  *
0011  * This library is distributed in the hope that it will be useful,
0012  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0013  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0014  * Lesser General Public License for more details.
0015  *
0016  * You should have received a copy of the GNU Lesser General Public
0017  * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
0018  */
0019 
0020 #include "KReportDesign_p.h"
0021 #include "KReportElement.h"
0022 #include "KReportUtils.h"
0023 #include "KReportUtils_p.h"
0024 #include "KReportPluginManager.h"
0025 #include "KReportPluginInterface.h"
0026 #include "kreport_debug.h"
0027 
0028 #include <QDebug>
0029 #include <QDomDocument>
0030 #include <QDomElement>
0031 #include <QSizeF>
0032 
0033 KReportDesign::Private::Private(KReportDesign *design)
0034  : q(design)
0035  , showGrid(DEFAULT_SHOW_GRID)
0036  , snapToGrid(DEFAULT_SNAP_TO_GRID)
0037  , gridDivisions(DEFAULT_GRID_DIVISIONS)
0038  , pageUnit(DEFAULT_UNIT)
0039  , sections(static_cast<int>(KReportSection::Type::Detail))
0040 {
0041     memset(static_cast<void*>(sections.data()), 0, sizeof(void*) * sections.length());
0042     pageLayout.setUnits(QPageLayout::Point); // initializate because of https://bugreports.qt.io/browse/QTBUG-47551
0043 }
0044 
0045 KReportDesign::Private::~Private()
0046 {
0047     qDeleteAll(sections);
0048 }
0049 
0050 KReportDesignGlobal::KReportDesignGlobal()
0051     : defaultSectionHeight(CM_TO_POINT(2.0))
0052     , defaultSectionBackgroundColor(Qt::white)
0053 {
0054     defaultPageLayout.setUnits(QPageLayout::Point);
0055     defaultPageLayout.setMargins(QMarginsF(DEFAULT_PAGE_MARGIN_PT,
0056                                            DEFAULT_PAGE_MARGIN_PT,
0057                                            DEFAULT_PAGE_MARGIN_PT,
0058                                            DEFAULT_PAGE_MARGIN_PT));
0059     defaultPageLayout.setMode(QPageLayout::StandardMode);
0060     defaultPageLayout.setOrientation(DEFAULT_PAGE_ORIENTATION);
0061 }
0062 
0063 KReportSection::Type KReportDesignGlobal::sectionType(const QString& typeName)
0064 {
0065     initSectionTypes();
0066     return sectionTypesForName.value(typeName); // returns Invalid type for invalid name
0067 }
0068 
0069 inline uint qHash(KReportSection::Type sectionType, uint seed = 0)
0070 {
0071     return qHash(static_cast<uint>(sectionType), seed);
0072 }
0073 
0074 QString KReportDesignGlobal::sectionTypeName(KReportSection::Type sectionType)
0075 {
0076     initSectionTypes();
0077     return sectionTypeNames.value(sectionType);
0078 }
0079 
0080 void KReportDesignGlobal::initSectionTypes()
0081 {
0082     if (!sectionTypesForName.isEmpty()) {
0083         return;
0084     }
0085     for (const SectionTypeInfo *info = sectionTypes; info->name; ++info) {
0086         sectionTypesForName.insert(QString::fromLatin1(info->name), info->type);
0087         sectionTypeNames.insert(info->type, QString::fromLatin1(info->name));
0088     }
0089 }
0090 
0091 const KReportDesignGlobal::SectionTypeInfo KReportDesignGlobal::sectionTypes[] = {
0092     { KReportSection::Type::Invalid, "" },
0093     { KReportSection::Type::PageHeaderAny, "header-page-any" },
0094     { KReportSection::Type::PageHeaderEven, "header-page-even" },
0095     { KReportSection::Type::PageHeaderOdd, "header-page-odd" },
0096     { KReportSection::Type::PageHeaderFirst, "header-page-first" },
0097     { KReportSection::Type::PageHeaderLast, "header-page-last" },
0098     { KReportSection::Type::PageFooterAny, "footer-page-any" },
0099     { KReportSection::Type::PageFooterEven, "footer-page-even" },
0100     { KReportSection::Type::PageFooterOdd, "footer-page-odd" },
0101     { KReportSection::Type::PageFooterFirst, "footer-page-first" },
0102     { KReportSection::Type::PageFooterLast, "footer-page-last" },
0103     { KReportSection::Type::ReportHeader, "header-report" },
0104     { KReportSection::Type::ReportFooter, "footer-report" },
0105     { KReportSection::Type::GroupHeader, "group-header" },
0106     { KReportSection::Type::GroupFooter, "group-footer" },
0107     { KReportSection::Type::Detail, "detail" },
0108     { KReportSection::Type::Invalid, nullptr }
0109 };
0110 
0111 Q_GLOBAL_STATIC(KReportDesignGlobal, s_global)
0112 
0113 //static
0114 KReportDesignGlobal* KReportDesignGlobal::self()
0115 {
0116     return s_global;
0117 }
0118 
0119 static void setStatus(KReportDesignReadingStatus *status, const QString& details,
0120                       const QDomNode &node)
0121 {
0122     if (status) {
0123         status->setErrorDetails(details);
0124         status->setErrorLineNumber(node.lineNumber() == -1 ? 0 /* mark error */ : node.lineNumber());
0125         status->setErrorColumnNumber(node.columnNumber() == -1 ? 0 /* mark error */ : node.columnNumber());
0126     }
0127 }
0128 
0129 static bool checkElement(const QDomNode &node, KReportDesignReadingStatus *status)
0130 {
0131     if (node.isElement()) {
0132         return true;
0133     }
0134     setStatus(status, QString::fromLatin1("Element expected inside of <%1>")
0135               .arg(node.parentNode().toElement().tagName()), node);
0136     return false;
0137 }
0138 
0139 static void setNoAttributeStatus(const QDomElement &el, const QString &attrName, KReportDesignReadingStatus *status)
0140 {
0141     setStatus(status, QString::fromLatin1("Attribute \"%1\" expected inside of <%1>")
0142               .arg(attrName).arg(el.tagName()), el);
0143 }
0144 
0145 #if 0 // TODO unused for now
0146 static bool checkAttribute(const QDomElement &el, const char *attrName, KReportDesignReadingStatus *status)
0147 {
0148     if (el.hasAttribute(QLatin1String(attrName))) {
0149         return true;
0150     }
0151     setNoAttributeStatus(el, attrName, status);
0152     return false;
0153 }
0154 #endif
0155 
0156 KReportSection KReportDesign::Private::processSectionElement(const QDomElement &el,
0157                                                              KReportDesignReadingStatus *status)
0158 {
0159     kreportDebug() << el.nodeName();
0160     const KReportSection::Type sectionType
0161         = s_global->sectionType(KReportUtils::readSectionTypeNameAttribute(el));
0162     if (sectionType == KReportSection::Type::Invalid) {
0163         setStatus(status,
0164                   QString::fromLatin1(
0165                       "Invalid value of report:section-type=\"%1\" in element <%2>")
0166                       .arg(KReportUtils::readSectionTypeNameAttribute(el))
0167                       .arg(el.tagName()),
0168                   el);
0169         return KReportSection();
0170     }
0171     KReportSection section;
0172     section.setType(sectionType);
0173     const QSizeF size(KReportUtils::readSizeAttributes(el));
0174     if (size.height() >= 0) {
0175         section.setHeight(size.height());
0176     }
0177     section.setBackgroundColor(
0178         KReportUtils::attr(el, QLatin1String("fo:background-color"), QColor()));
0179     for (QDomNode node = el.firstChild(); !node.isNull(); node = node.nextSibling()) {
0180         if (!checkElement(node, status)) {
0181             return KReportSection();
0182         }
0183         KReportElement element = processSectionElementChild(node.toElement(), status);
0184         if (!element.rect().isValid() || (status && status->isError())) {
0185             return KReportSection();
0186         }
0187         (void)section.addElement(element);
0188     }
0189     return section;
0190 }
0191 
0192 KReportPluginInterface* KReportDesign::Private::findPlugin(const QString &typeName,
0193                                                             const QDomElement &el,
0194                                                             KReportDesignReadingStatus *status)
0195 {
0196     KReportPluginInterface* plugin = KReportPluginManager::self()->plugin(typeName);
0197     if (!plugin) {
0198         setStatus(status, QString::fromLatin1("No such plugin \"%1\"").arg(typeName), el);
0199         return nullptr;
0200     }
0201     return plugin;
0202 }
0203 
0204 KReportElement KReportDesign::Private::processSectionElementChild(const QDomElement &el,
0205                                                                   KReportDesignReadingStatus *status)
0206 {
0207     const QByteArray name = el.tagName().toLatin1();
0208     const char* elNamespace = "report:";
0209 
0210     if (!name.startsWith(elNamespace)) {
0211         unexpectedElement(el, status);
0212         return KReportElement();
0213     }
0214     const QByteArray reportElementName = name.mid(qstrlen(elNamespace));
0215     //qDebug() << "Found Report Element:" << reportElementName;
0216     KReportPluginInterface *plugin = findPlugin(QLatin1String(reportElementName), el, status);
0217     if (!plugin) {
0218         return KReportElement();
0219     }
0220     KReportElement element = plugin->createElement();
0221     if (!plugin->loadElement(&element, el, status)) {
0222         return KReportElement();
0223     }
0224     element.setName(KReportUtils::readNameAttribute(el));
0225     if (element.name().isEmpty()) {
0226         setNoAttributeStatus(el, QLatin1String("report:name"), status);
0227         return KReportElement();
0228     }
0229     return element;
0230 }
0231 
0232 bool KReportDesign::Private::processGroupElement(const QDomElement &el,
0233                                                  KReportDesignReadingStatus *status)
0234 {
0235     Q_UNUSED(el);
0236     Q_UNUSED(status);
0237     //! @todo
0238     return true;
0239 }
0240 
0241 //! The report:detail element contains a single report:section child of type 'detail'
0242 //! and 0 or more report:group children.
0243 bool KReportDesign::Private::processDetailElement(const QDomElement &el,
0244                                                   KReportDesignReadingStatus *status)
0245 {
0246     QDomElement sectionEl;
0247     for (QDomNode node = el.firstChild(); !node.isNull(); node = node.nextSibling()) {
0248         if (!checkElement(node, status)) {
0249             return false;
0250         }
0251         QDomElement childEl = node.toElement();
0252         const QByteArray name = childEl.tagName().toLatin1();
0253         if (name == "report:section") {
0254             if (!sectionEl.isNull()) {
0255                 return false;
0256             }
0257             KReportSection section = processSectionElement(childEl, status);
0258             if (status && status->isError()) {
0259                 return false;
0260             }
0261             if (section.type() != KReportSection::Type::Detail) {
0262                 setStatus(status,
0263                     QString::fromLatin1("Only section of type \"detail\" allowed in <report:detail>"), el);
0264                 return false;
0265             }
0266             q->addSection(section);
0267         }
0268         else if (name == "report:group") {
0269             if (!processGroupElement(childEl, status)) {
0270                 return false;
0271             }
0272         }
0273         else {
0274             unexpectedElement(childEl, status);
0275             return false;
0276         }
0277     }
0278     // finally make sure we have one report:section
0279     (void)requiredChildElement(el, "report:section", status);
0280     if (status && status->isError()) {
0281         return false;
0282     }
0283     return true;
0284 }
0285 
0286 /*! <pre>
0287        <report:body>
0288         *<report:section>..</report:section> (up to 12 sections)
0289          <report:detail>
0290            <report:group> // any number of groups
0291              *<report:section>..</report:section> (group-header, group-footer)
0292            </report:group>
0293          </report:detail>
0294        <report:body>
0295     <pre>
0296 */
0297 bool KReportDesign::Private::processBodyElementChild(const QDomElement &el,
0298                                                      KReportDesignReadingStatus *status)
0299 {
0300     const QByteArray name = el.tagName().toLatin1();
0301     //kreportDebug() << name;
0302     if (name == "report:section") {
0303         KReportSection section = processSectionElement(el, status);
0304         if (status && status->isError()) {
0305             return false;
0306         }
0307         if (q->hasSection(section.type())) {
0308             setStatus(status, QString::fromLatin1("Could not add two sections of type \"%1\" "
0309                                                   "to the same report design")
0310                                 .arg(s_global->sectionTypeName(section.type())), el);
0311             return false;
0312         }
0313         if (section.type() == KReportSection::Type::Detail) {
0314             setStatus(status,
0315                 QString::fromLatin1("Section of type \"detail\" not allowed in <report:body>"), el);
0316             return false;
0317         }
0318         q->addSection(section);
0319 #if 0 //TODO
0320         if (section(KReportSectionData::sectionTypeFromString(sectiontype)) == 0) {
0321             insertSection(KReportSectionData::sectionTypeFromString(sectiontype));
0322             section(KReportSectionData::sectionTypeFromString(sectiontype))->initFromXML(sec);
0323         }
0324 #endif
0325     } else if (name == "report:detail") {
0326         if (!processDetailElement(el, status)) {
0327             return false;
0328         }
0329 #if 0 //TODO
0330         ReportSectionDetail * rsd = new ReportSectionDetail(this);
0331         rsd->initFromXML(&sec);
0332         setDetail(rsd);
0333 #endif
0334     }
0335     return true;
0336 }
0337 
0338 /* NOTE: don't translate these extremely detailed messages. */
0339 //! @todo Load page options
0340 bool KReportDesign::Private::processContentElementChild(const QDomElement &el,
0341                                                         KReportDesignReadingStatus *status)
0342 {
0343     const QByteArray name = el.tagName().toLatin1();
0344     QPageLayout defaultPageLayout = KReportDesign::defaultPageLayout();
0345     //kreportDebug() << name;
0346     if (name == "report:title") {
0347         title = el.text();
0348 #ifdef KREPORT_SCRIPTING
0349     } else if (name == "report:script") {
0350         script = el.firstChildElement().text();
0351         originalInterpreter = KReportUtils::attr(
0352             el, QLatin1String("report:script-interpreter"), QString());
0353 #endif
0354     } else if (name == "report:grid") {
0355         showGrid = KReportUtils::attr(
0356             el, QLatin1String("report:grid-visible"), DEFAULT_SHOW_GRID);
0357         snapToGrid = KReportUtils::attr(
0358             el, QLatin1String("report:grid-snap"), DEFAULT_SNAP_TO_GRID);
0359         gridDivisions = KReportUtils::attr(
0360             el, QLatin1String("report:grid-divisions"), DEFAULT_GRID_DIVISIONS);
0361         const QString pageUnitString
0362             = KReportUtils::attr(el, QLatin1String("report:page-unit"), QString());
0363 
0364         pageUnit = KReportUnit::symbolToType(pageUnitString);
0365         if (!pageUnit.isValid()) {
0366             pageUnit = DEFAULT_UNIT;
0367             if (!pageUnitString.isEmpty()) {
0368                 kreportWarning() << "Invalid page unit" << pageUnitString << "specified in" << name
0369                            << "element, defaulting to" << pageUnit.symbol();
0370             }
0371         }
0372     }
0373     else if (name == "report:page-style") { // see https://git.reviewboard.kde.org/r/115314
0374         const QByteArray pagetype = el.text().toLatin1();
0375         if (pagetype == "predefined") {
0376             pageLayout.setPageSize(KReportUtils::pageSize(
0377                 KReportUtils::attr(el, QLatin1String("report:page-size"),
0378                                    QPageSize(DEFAULT_PAGE_SIZE).key())));
0379         } else if (pagetype.isEmpty() || pagetype == "custom") {
0380             QSizeF size(KReportUtils::attr(el, QLatin1String("fo:page-width"), -1.0),
0381                         KReportUtils::attr(el, QLatin1String("fo:page-height"), -1.0));
0382             if (size.isValid()) {
0383                 pageLayout.setPageSize(QPageSize(size, QPageSize::Point));
0384             } else {
0385                 pageLayout.setPageSize(defaultPageLayout.pageSize());
0386             }
0387         } else if (pagetype == "label") {
0388             //! @todo?
0389             pageLayout.setPageSize(defaultPageLayout.pageSize());
0390         }
0391         QMarginsF margins(KReportUtils::attr(el, QLatin1String("fo:margin-left"),
0392                                              defaultPageLayout.margins().left()),
0393                           KReportUtils::attr(el, QLatin1String("fo:margin-top"),
0394                                              defaultPageLayout.margins().top()),
0395                           KReportUtils::attr(el, QLatin1String("fo:margin-right"),
0396                                              defaultPageLayout.margins().right()),
0397                           KReportUtils::attr(el, QLatin1String("fo:margin-bottom"),
0398                                              defaultPageLayout.margins().bottom()));
0399         bool b = pageLayout.setMargins(margins);
0400         if (!b) {
0401             qWarning() << "Failed to set page margins to" << margins;
0402         }
0403         const QString s = KReportUtils::attr(
0404             el, QLatin1String("report:print-orientation"), QString());
0405         if (s == QLatin1String("portrait")) {
0406             pageLayout.setOrientation(QPageLayout::Portrait);
0407         } else if (s == QLatin1String("landscape")) {
0408             pageLayout.setOrientation(QPageLayout::Landscape);
0409         }
0410         else {
0411             pageLayout.setOrientation(defaultPageLayout.orientation());
0412         }
0413     } else if (name == "report:body") {
0414         for (QDomNode node = el.firstChild(); !node.isNull(); node = node.nextSibling()) {
0415             if (!checkElement(node, status)) {
0416                 return false;
0417             }
0418             if (!processBodyElementChild(node.toElement(), status)) {
0419                 return false;
0420             }
0421         }
0422     }
0423     return true;
0424 }
0425 
0426 void KReportDesign::Private::unexpectedElement(const QDomElement &element,
0427                                                KReportDesignReadingStatus *status) const
0428 {
0429     setStatus(status, QString::fromLatin1("Unexpected child element <%1> found in <%2>")
0430           .arg(element.tagName()).arg(element.parentNode().toElement().tagName()), element);
0431 }
0432 
0433 QDomElement KReportDesign::Private::requiredChildElement(const QDomElement &parent,
0434                                                          const char* childElementName,
0435                                                          KReportDesignReadingStatus *status) const
0436 {
0437     const QDomElement result = parent.firstChildElement(QLatin1String(childElementName));
0438     if (result.isNull()) {
0439         setStatus(status, QString::fromLatin1("Child element <%1> not found in <%2>")
0440               .arg(QLatin1String(childElementName)).arg(parent.tagName()), parent);
0441     }
0442     return result;
0443 }
0444 
0445 /* NOTE: don't translate these extremely detailed messages. */
0446 bool KReportDesign::Private::processDocument(const QDomDocument &doc,
0447                                              KReportDesignReadingStatus *status)
0448 {
0449     const QDomElement rootEl = doc.documentElement();
0450     const QLatin1String rootElName("kexireport"); // legacy name kept for compatibility
0451     if (doc.doctype().name() != rootElName) {
0452         setStatus(status, QString::fromLatin1("Document type should be \"%1\"").arg(rootElName), rootEl);
0453         return false;
0454     }
0455     if (rootEl.tagName() != rootElName) {
0456         setStatus(status, QString::fromLatin1("Root element should be <%1>").arg(rootElName), rootEl);
0457         return false;
0458     }
0459     const QDomElement contentEl = requiredChildElement(rootEl, "report:content", status);
0460     if (status && status->isError()) {
0461         return false;
0462     }
0463     //! @todo check namespaces as in:
0464     //! <report:content xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
0465     //!     xmlns:report="http://kexi-project.org/report/2.0"
0466     //!     xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0">
0467 
0468 //    deleteDetail();
0469 
0470     for (QDomNode node = contentEl.firstChild(); !node.isNull(); node = node.nextSibling()) {
0471         if (!checkElement(node, status)) {
0472             return false;
0473         }
0474         if (!processContentElementChild(node.toElement(), status)) {
0475             return false;
0476         }
0477     }
0478 
0479     if (status) {
0480         *status = KReportDesignReadingStatus();
0481     }
0482     return true;
0483 }