File indexing completed on 2024-05-12 05:10:14

0001 /***************************************************************************
0002     Copyright (C) 2008-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "xmlstatehandler.h"
0026 #include "tellico_xml.h"
0027 #include "../collection.h"
0028 #include "../collectionfactory.h"
0029 #include "../collections/bibtexcollection.h"
0030 #include "../fieldformat.h"
0031 #include "../images/image.h"
0032 #include "../images/imageinfo.h"
0033 #include "../images/imagefactory.h"
0034 #include "../utils/isbnvalidator.h"
0035 #include "../utils/string_utils.h"
0036 #include "../tellico_debug.h"
0037 
0038 #include <KLocalizedString>
0039 
0040 #include <QRegularExpression>
0041 
0042 namespace {
0043 
0044 inline
0045 QString attValue(const QXmlStreamAttributes& atts, const char* name, const QString& defaultValue=QString()) {
0046   return atts.hasAttribute(QLatin1String(name)) ? atts.value(QLatin1String(name)).toString() : defaultValue;
0047 }
0048 
0049 inline
0050 QString attValue(const QXmlStreamAttributes& atts, const char* name, const char* defaultValue) {
0051   Q_ASSERT(defaultValue);
0052   return attValue(atts, name, QLatin1String(defaultValue));
0053 }
0054 
0055 inline
0056 QString realFieldName(int syntaxVersion, const QStringRef& localName) {
0057   return (syntaxVersion < 2 && localName == QLatin1String("keywords")) ?
0058     QStringLiteral("keyword") :
0059     localName.toString();
0060 }
0061 
0062 }
0063 
0064 using Tellico::Import::SAX::StateHandler;
0065 using Tellico::Import::SAX::NullHandler;
0066 using Tellico::Import::SAX::RootHandler;
0067 using Tellico::Import::SAX::DocumentHandler;
0068 using Tellico::Import::SAX::CollectionHandler;
0069 using Tellico::Import::SAX::FieldsHandler;
0070 using Tellico::Import::SAX::FieldHandler;
0071 using Tellico::Import::SAX::FieldPropertyHandler;
0072 using Tellico::Import::SAX::BibtexPreambleHandler;
0073 using Tellico::Import::SAX::BibtexMacrosHandler;
0074 using Tellico::Import::SAX::BibtexMacroHandler;
0075 using Tellico::Import::SAX::EntryHandler;
0076 using Tellico::Import::SAX::FieldValueContainerHandler;
0077 using Tellico::Import::SAX::FieldValueHandler;
0078 using Tellico::Import::SAX::DateValueHandler;
0079 using Tellico::Import::SAX::TableColumnHandler;
0080 using Tellico::Import::SAX::ImagesHandler;
0081 using Tellico::Import::SAX::ImageHandler;
0082 using Tellico::Import::SAX::FiltersHandler;
0083 using Tellico::Import::SAX::FilterHandler;
0084 using Tellico::Import::SAX::FilterRuleHandler;
0085 using Tellico::Import::SAX::BorrowersHandler;
0086 using Tellico::Import::SAX::BorrowerHandler;
0087 using Tellico::Import::SAX::LoanHandler;
0088 
0089 StateHandler* StateHandler::nextHandler(const QStringRef& ns_, const QStringRef& localName_) {
0090   StateHandler* handler = nextHandlerImpl(ns_, localName_);
0091   if(!handler) {
0092     myWarning() << "no handler for" << localName_;
0093   }
0094   return handler ? handler : new NullHandler(d);
0095 }
0096 
0097 StateHandler* RootHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0098   if(localName_ == QLatin1String("tellico") || localName_ == QLatin1String("bookcase")) {
0099     return new DocumentHandler(d);
0100   }
0101   return new RootHandler(d);
0102 }
0103 
0104 StateHandler* DocumentHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0105   if(localName_ == QLatin1String("collection")) {
0106     return new CollectionHandler(d);
0107   } else if(localName_ == QLatin1String("filters")) {
0108     return new FiltersHandler(d);
0109   } else if(localName_ == QLatin1String("borrowers")) {
0110     return new BorrowersHandler(d);
0111   }
0112   return nullptr;
0113 }
0114 
0115 bool DocumentHandler::start(const QStringRef&, const QStringRef& localName_, const QXmlStreamAttributes& atts_) {
0116   // the syntax version field name changed from "version" to "syntaxVersion" in version 3
0117   QStringRef syntaxVersion = atts_.value(QLatin1String("syntaxVersion"));
0118   if(syntaxVersion.isEmpty()) {
0119     syntaxVersion = atts_.value(QLatin1String("version"));
0120     if(syntaxVersion.isEmpty()) {
0121       myWarning() << "no syntax version";
0122       return false;
0123     }
0124   }
0125   d->syntaxVersion = syntaxVersion.toUInt();
0126   if(d->syntaxVersion > Tellico::XML::syntaxVersion) {
0127     d->error = i18n("It is from a future version of Tellico.");
0128     return false;
0129   }
0130   if((d->syntaxVersion > 6 && localName_ != QLatin1String("tellico")) ||
0131      (d->syntaxVersion < 7 && localName_ != QLatin1String("bookcase"))) {
0132     // no error message
0133     myWarning() << "bad root element name";
0134     return false;
0135   }
0136   d->ns = d->syntaxVersion > 6 ? Tellico::XML::nsTellico : Tellico::XML::nsBookcase;
0137   return true;
0138 }
0139 
0140 bool DocumentHandler::end(const QStringRef&, const QStringRef&) {
0141   return true;
0142 }
0143 
0144 StateHandler* CollectionHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0145   if((d->syntaxVersion > 3 && localName_ == QLatin1String("fields")) ||
0146      (d->syntaxVersion < 4 && localName_ == QLatin1String("attributes"))) {
0147     return new FieldsHandler(d);
0148   } else if(localName_ == QLatin1String("bibtex-preamble")) {
0149     return new BibtexPreambleHandler(d);
0150   } else if(localName_ == QLatin1String("macros")) {
0151     return new BibtexMacrosHandler(d);
0152   } else if(localName_ == d->entryName) {
0153     return new EntryHandler(d);
0154   } else if(localName_ == QLatin1String("images")) {
0155     return new ImagesHandler(d);
0156   }
0157   return nullptr;
0158 }
0159 
0160 bool CollectionHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0161   d->collTitle = attValue(atts_, "title");
0162   d->collType = atts_.value(QLatin1String("type")).toInt();
0163   if(d->syntaxVersion > 6) {
0164     d->entryName = QStringLiteral("entry");
0165   } else {
0166     // old attribute
0167     d->entryName = attValue(atts_, "unit");
0168     // for error recovery, assume entry name is default if empty for now
0169     if(d->entryName.isEmpty()) {
0170       d->entryName = QLatin1String("entry");
0171     }
0172   }
0173   return true;
0174 }
0175 
0176 bool CollectionHandler::end(const QStringRef&, const QStringRef&) {
0177   if(!d->coll) {
0178     myWarning() << "no collection created";
0179     return false;
0180   }
0181   d->coll->addEntries(d->entries);
0182 
0183   // a little hidden capability was to just have a local path as an image file name
0184   // and on reading the xml file, Tellico would load the image file, too
0185   // here, we need to scan all the image values in all the entries and check
0186   // maybe this is too costly, especially since the capability wasn't advertised?
0187 
0188   const int maxImageWarnings = 3;
0189   int imageWarnings = 0;
0190 
0191   Data::FieldList fields = d->coll->imageFields();
0192   foreach(Data::EntryPtr entry, d->entries) {
0193     foreach(Data::FieldPtr field, fields) {
0194       QString value = entry->field(field);
0195       if(value.isEmpty()) {
0196         continue;
0197       }
0198       // image info should have already been loaded
0199       // if not, then there was no <image> in the XML
0200       // so it's a url, but maybe link only
0201       if(!ImageFactory::hasImageInfo(value)) {
0202         QUrl u(value); // previously used QUrl::fromUserInput but now expect proper url
0203         // also allow relative image urls
0204         if(u.isRelative()) {
0205           if(d->baseUrl.isEmpty()) {
0206             // assume a local file, as fromUserInput() would do
0207             u = QUrl::fromLocalFile(value);
0208           } else {
0209             u = d->baseUrl.resolved(u);
0210           }
0211         }
0212         // the image file name is a valid URL, but I want it to be a local URL or non empty remote one
0213         if(u.isValid()) {
0214           if(u.scheme() == QLatin1String("data")) {
0215             const QByteArray ba = QByteArray::fromPercentEncoding(u.path(QUrl::FullyEncoded).toLatin1());
0216             const int pos = ba.indexOf(',');
0217             if(ba.startsWith("image/") && pos > -1) {
0218               const QByteArray header = ba.left(pos);
0219               const int pos2 = header.indexOf(';');
0220               // we can only read images in base64
0221               if(header.contains("base64") && pos2 > -1) {
0222                 const QByteArray format = header.left(pos2).mid(6); // remove "image/";
0223                 const QString result = ImageFactory::addImage(QByteArray::fromBase64(ba.mid(pos+1)),
0224                                                               QString::fromLatin1(format));
0225                 if(result.isEmpty()) {
0226                   // clear value for the field in this case
0227                   value.clear();
0228                   ++imageWarnings;
0229                 } else {
0230                   value = result;
0231                 }
0232               }
0233             }
0234           } else if(u.isLocalFile() || !u.host().isEmpty()) {
0235             const QString result = ImageFactory::addImage(u, !d->showImageLoadErrors || imageWarnings >= maxImageWarnings /* quiet */);
0236             if(result.isEmpty()) {
0237               // clear value for the field in this case
0238               value.clear();
0239               ++imageWarnings;
0240             } else {
0241               value = result;
0242             }
0243           }
0244         } else {
0245           value = Data::Image::idClean(value);
0246         }
0247         // reset the image id to be whatever was loaded
0248         entry->setField(field->name(), value, false /* no modified date update */);
0249       }
0250     }
0251   }
0252   return true;
0253 }
0254 
0255 StateHandler* FieldsHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0256   if((d->syntaxVersion > 3 && localName_ == QLatin1String("field")) ||
0257      (d->syntaxVersion < 4 && localName_ == QLatin1String("attribute"))) {
0258     return new FieldHandler(d);
0259   }
0260   return nullptr;
0261 }
0262 
0263 bool FieldsHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0264   d->defaultFields = false;
0265   return true;
0266 }
0267 
0268 bool FieldsHandler::end(const QStringRef&, const QStringRef&) {
0269   // add default fields if there was a default field name, or no names at all
0270   const bool addFields = d->defaultFields || d->fields.isEmpty();
0271   // in syntax 4, the element name was changed to "entry", always, rather than depending on
0272   // on the entryName of the collection.
0273   if(d->syntaxVersion > 3) {
0274     d->entryName = QStringLiteral("entry");
0275     Data::Collection::Type type = static_cast<Data::Collection::Type>(d->collType);
0276     d->coll = CollectionFactory::collection(type, addFields);
0277   } else {
0278     d->coll = CollectionFactory::collection(d->entryName, addFields);
0279   }
0280 
0281   if(!d->collTitle.isEmpty()) {
0282     d->coll->setTitle(d->collTitle);
0283   }
0284 
0285   // add a default field for ID
0286   // checking the defaultFields bool since if it is true, we already added these default fields
0287   // even for old syntax versions
0288   if(d->syntaxVersion < 11 && !d->defaultFields) {
0289     d->coll->addField(Data::Field::createDefaultField(Data::Field::IDField));
0290   }
0291   // now add all the new fields
0292   d->coll->addFields(d->fields);
0293   if(d->syntaxVersion < 11 && !d->defaultFields) {
0294     d->coll->addField(Data::Field::createDefaultField(Data::Field::CreatedDateField));
0295     d->coll->addField(Data::Field::createDefaultField(Data::Field::ModifiedDateField));
0296   }
0297 
0298 //  as a special case, for old book collections with a bibtex-id field, convert to Bibtex
0299   if(d->syntaxVersion < 4 && d->collType == Data::Collection::Book
0300      && d->coll->hasField(QStringLiteral("bibtex-id"))) {
0301     d->coll = Data::BibtexCollection::convertBookCollection(d->coll);
0302   }
0303 
0304   return true;
0305 }
0306 
0307 StateHandler* FieldHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0308   if(localName_ == QLatin1String("prop")) {
0309     return new FieldPropertyHandler(d);
0310   }
0311   return nullptr;
0312 }
0313 
0314 bool FieldHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0315   // special case: if the i18n attribute equals true, then translate the title, description, category, and allowed
0316   isI18n = atts_.value(QLatin1String("i18n")) == QLatin1String("true");
0317 
0318   const QString name = attValue(atts_, "name", "unknown");
0319   if(name == QLatin1String("_default")) {
0320     d->defaultFields = true;
0321     return true;
0322   }
0323 
0324   QString title  = attValue(atts_, "title", i18n("Unknown"));
0325   if(isI18n && !title.isEmpty()) {
0326     title = i18n(title.toUtf8().constData());
0327   }
0328 
0329   QString typeStr = attValue(atts_, "type", QString::number(Data::Field::Line));
0330   Data::Field::Type type = static_cast<Data::Field::Type>(typeStr.toInt());
0331 
0332   Data::FieldPtr field;
0333   if(type == Data::Field::Choice) {
0334     QStringList allowed = FieldFormat::splitValue(attValue(atts_, "allowed"), FieldFormat::RegExpSplit);
0335     if(isI18n) {
0336       for(QStringList::Iterator word = allowed.begin(); word != allowed.end(); ++word) {
0337         (*word) = i18n((*word).toUtf8().constData());
0338       }
0339     }
0340     field = new Data::Field(name, title, allowed);
0341   } else {
0342     field = new Data::Field(name, title, type);
0343   }
0344 
0345   QString cat = attValue(atts_, "category");
0346   // at one point, the categories had keyboard accels
0347   if(d->syntaxVersion < 9) {
0348     cat.remove(QLatin1Char('&'));
0349   }
0350   if(isI18n && !cat.isEmpty()) {
0351     cat = i18n(cat.toUtf8().constData());
0352   }
0353   field->setCategory(cat);
0354 
0355   int flags = atts_.value(QLatin1String("flags")).toInt();
0356   // I also changed the enum values for syntax 3, but the only custom field
0357   // would have been bibtex-id
0358   if(d->syntaxVersion < 3 && name == QLatin1String("bibtex-id")) {
0359     flags = 0;
0360   }
0361 
0362   // in syntax version 4, added a flag to disallow deleting attributes
0363   // if it's a version before that and is the title, then add the flag
0364   if(d->syntaxVersion < 4 && name == QLatin1String("title")) {
0365     flags |= Data::Field::NoDelete;
0366   }
0367   // some of the flags may have been set in the constructor
0368   // in the case of old Dependent fields changing, for example
0369   // so combine with the existing flags
0370   field->setFlags(field->flags() | flags);
0371 
0372   QString formatStr = attValue(atts_, "format", QString::number(FieldFormat::FormatNone));
0373   FieldFormat::Type formatType = static_cast<FieldFormat::Type>(formatStr.toInt());
0374   field->setFormatType(formatType);
0375 
0376   QString desc = attValue(atts_, "description");
0377   if(isI18n && !desc.isEmpty()) {
0378     desc = i18n(desc.toUtf8().constData());
0379   }
0380   field->setDescription(desc);
0381 
0382   if(d->syntaxVersion < 5 && atts_.hasAttribute(QLatin1String("bibtex-field"))) {
0383     field->setProperty(QStringLiteral("bibtex"), attValue(atts_, "bibtex-field"));
0384   }
0385 
0386   // for syntax 8, rating fields got their own type
0387   if(d->syntaxVersion < 8) {
0388     Data::Field::convertOldRating(field); // does all its own checking
0389   }
0390   d->fields.append(field);
0391 
0392   return true;
0393 }
0394 
0395 bool FieldHandler::end(const QStringRef&, const QStringRef&) {
0396   // the value template for derived values used to be the field description
0397   // now it is the 'template' property
0398   // for derived value fields, if there is no property and the description has a '%'
0399   // move it to the property
0400   //
0401   // might be empty is we're only adding default fields
0402   if(!d->fields.isEmpty()) {
0403     Data::FieldPtr field = d->fields.back();
0404     if(field->hasFlag(Data::Field::Derived) &&
0405        field->property(QStringLiteral("template")).isEmpty() &&
0406        field->description().contains(QLatin1Char('%'))) {
0407       field->setProperty(QStringLiteral("template"), field->description());
0408       field->setDescription(QString());
0409     } else if(isI18n && field->type() == Data::Field::Table) {
0410       // translate table column headers if requestsed (such as Title, Artist, etc.
0411       const auto props = field->propertyList();
0412       for(auto i = props.constBegin(); i != props.constEnd(); ++i) {
0413         if(i.key().startsWith(QLatin1String("column"))) {
0414           field->setProperty(i.key(), i18n(i.value().toUtf8().constData()));
0415         }
0416       }
0417     }
0418   }
0419 
0420   return true;
0421 }
0422 
0423 bool FieldPropertyHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0424   // there should be at least one field already so we can add properties to it
0425   Q_ASSERT(!d->fields.isEmpty());
0426   Data::FieldPtr field = d->fields.back();
0427 
0428   m_propertyName = attValue(atts_, "name");
0429 
0430   // all track fields in music collections prior to version 9 get converted to three columns
0431   if(d->syntaxVersion < 9) {
0432     if(d->collType == Data::Collection::Album && field->name() == QLatin1String("track")) {
0433       field->setProperty(QStringLiteral("columns"), QStringLiteral("3"));
0434       field->setProperty(QStringLiteral("column1"), i18n("Title"));
0435       field->setProperty(QStringLiteral("column2"), i18n("Artist"));
0436       field->setProperty(QStringLiteral("column3"), i18n("Length"));
0437     } else if(d->collType == Data::Collection::Video && field->name() == QLatin1String("cast")) {
0438       field->setProperty(QStringLiteral("column1"), i18n("Actor/Actress"));
0439       field->setProperty(QStringLiteral("column2"), i18n("Role"));
0440     }
0441   }
0442 
0443   return true;
0444 }
0445 
0446 bool FieldPropertyHandler::end(const QStringRef&, const QStringRef&) {
0447   Q_ASSERT(!m_propertyName.isEmpty());
0448   // add the previous property
0449   Data::FieldPtr field = d->fields.back();
0450   field->setProperty(m_propertyName, d->text);
0451   return true;
0452 }
0453 
0454 bool BibtexPreambleHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0455   return true;
0456 }
0457 
0458 bool BibtexPreambleHandler::end(const QStringRef&, const QStringRef&) {
0459   Q_ASSERT(d->coll);
0460   if(d->coll && d->collType == Data::Collection::Bibtex && !d->text.isEmpty()) {
0461     Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(d->coll.data());
0462     c->setPreamble(d->text);
0463   }
0464   return true;
0465 }
0466 
0467 StateHandler* BibtexMacrosHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0468   if(localName_ == QLatin1String("macro")) {
0469     return new BibtexMacroHandler(d);
0470   }
0471   return nullptr;
0472 }
0473 
0474 bool BibtexMacrosHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0475   return true;
0476 }
0477 
0478 bool BibtexMacrosHandler::end(const QStringRef&, const QStringRef&) {
0479   return true;
0480 }
0481 
0482 bool BibtexMacroHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0483   m_macroName = attValue(atts_, "name");
0484   return true;
0485 }
0486 
0487 bool BibtexMacroHandler::end(const QStringRef&, const QStringRef&) {
0488   if(d->coll && d->collType == Data::Collection::Bibtex && !m_macroName.isEmpty() && !d->text.isEmpty()) {
0489     Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(d->coll.data());
0490     c->addMacro(m_macroName, d->text);
0491   }
0492   return true;
0493 }
0494 
0495 StateHandler* EntryHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0496   if(d->coll->hasField(realFieldName(d->syntaxVersion, localName_))) {
0497     return new FieldValueHandler(d);
0498   }
0499   return new FieldValueContainerHandler(d);
0500 }
0501 
0502 bool EntryHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0503   // the entries must come after the fields
0504   if(!d->coll || d->coll->fields().isEmpty()) {
0505     // special case for very old versions which did not have user-editable fields
0506     // also maybe a new version has bad formatting, try to recover by assuming default fields
0507     d->defaultFields = true;
0508     FieldsHandler handler(d);
0509     // fake the end of a fields element, which will add the default fields
0510     handler.end(QStringRef(), QStringRef());
0511     myWarning() << "entries should come after fields are defined, attempting to recover";
0512   }
0513   bool ok;
0514   const int id = atts_.value(QLatin1String("id")).toInt(&ok);
0515   Data::EntryPtr entry;
0516   if(ok && id > -1) {
0517     entry = new Data::Entry(d->coll, id);
0518   } else {
0519     entry = new Data::Entry(d->coll);
0520   }
0521   d->entries.append(entry);
0522   return true;
0523 }
0524 
0525 bool EntryHandler::end(const QStringRef&, const QStringRef&) {
0526   Data::EntryPtr entry = d->entries.back();
0527   Q_ASSERT(entry);
0528   if(!d->modifiedDate.isEmpty() && d->coll->hasField(QStringLiteral("mdate"))) {
0529     entry->setField(QStringLiteral("mdate"), d->modifiedDate);
0530     d->modifiedDate.clear();
0531   }
0532   return true;
0533 }
0534 
0535 StateHandler* FieldValueContainerHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0536   if(d->coll->hasField(realFieldName(d->syntaxVersion, localName_))) {
0537     return new FieldValueHandler(d);
0538   }
0539   return new FieldValueContainerHandler(d);
0540 }
0541 
0542 bool FieldValueContainerHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0543   return true;
0544 }
0545 
0546 bool FieldValueContainerHandler::end(const QStringRef&, const QStringRef&) {
0547   Data::FieldPtr f = d->currentField;
0548   if(f && f->type() == Data::Field::Table) {
0549     Data::EntryPtr entry = d->entries.back();
0550     Q_ASSERT(entry);
0551     QString fieldValue = entry->field(f->name());
0552     // don't allow table value to end with empty row
0553     while(fieldValue.endsWith(FieldFormat::rowDelimiterString())) {
0554       fieldValue.chop(FieldFormat::rowDelimiterString().length());
0555       // no need to update the modified date when setting the entry's field value
0556       entry->setField(f->name(), fieldValue, false /* no modified date update */);
0557     }
0558   }
0559 
0560   return true;
0561 }
0562 
0563 StateHandler* FieldValueHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0564   if(localName_ == QLatin1String("year") ||
0565      localName_ == QLatin1String("month") ||
0566      localName_ == QLatin1String("day")) {
0567     return new DateValueHandler(d);
0568   } else if(localName_ == QLatin1String("column")) {
0569     return new TableColumnHandler(d);
0570   }
0571   return nullptr;
0572 }
0573 
0574 bool FieldValueHandler::start(const QStringRef&, const QStringRef& localName_, const QXmlStreamAttributes& atts_) {
0575   d->currentField = d->coll->fieldByName(realFieldName(d->syntaxVersion, localName_));
0576   Q_ASSERT(d->currentField);
0577   m_i18n = atts_.value(QLatin1String("i18n")) == QLatin1String("true");
0578   m_validateISBN = (localName_ == QLatin1String("isbn")) &&
0579                    (atts_.value(QLatin1String("validate")) != QLatin1String("no"));
0580   return true;
0581 }
0582 
0583 bool FieldValueHandler::end(const QStringRef&, const QStringRef& localName_) {
0584   Data::EntryPtr entry = d->entries.back();
0585   Q_ASSERT(entry);
0586   QString fieldName = d->currentField ? d->currentField->name() : realFieldName(d->syntaxVersion, localName_);
0587 
0588   Data::FieldPtr f = d->currentField;
0589   if(!f) {
0590     myWarning() << "no field named " << fieldName;
0591     return true;
0592   }
0593   // if it's a derived value, no field value is added
0594   if(f->hasFlag(Data::Field::Derived)) {
0595     return true;
0596   }
0597 
0598   QString fieldValue = d->text;
0599   if(d->syntaxVersion < 4 && f->type() == Data::Field::Bool) {
0600     // in version 3 and prior, checkbox attributes had no text(), set it to "true"
0601     fieldValue = QStringLiteral("true");
0602   } else if(d->syntaxVersion < 8 && f->type() == Data::Field::Rating) {
0603     // in version 8, old rating fields get changed
0604     bool ok;
0605     uint i = Tellico::toUInt(fieldValue, &ok);
0606     if(ok) {
0607       fieldValue = QString::number(i);
0608     }
0609   } else if(!d->textBuffer.isEmpty()) {
0610     // for dates and tables, the value is built up from child elements
0611     if(!d->text.isEmpty()) {
0612       myWarning() << "ignoring value for field" << localName_ << ":" << d->text;
0613     }
0614     fieldValue = d->textBuffer;
0615     // the text buffer has the column delimiter at the end, remove it
0616     if(f->type() == Data::Field::Table) {
0617       fieldValue.chop(FieldFormat::columnDelimiterString().length());
0618     }
0619     d->textBuffer.clear();
0620   } else if(fieldValue.isEmpty() && f->type() == Data::Field::Table) {
0621     // allow for empty table rows
0622     fieldValue = FieldFormat::rowDelimiterString();
0623   }
0624   // this is not an else branch, the data may be in the textBuffer
0625   if(d->syntaxVersion < 9 && d->coll->type() == Data::Collection::Album && fieldName == QLatin1String("track")) {
0626     // yes, this assumes the artist has already been set
0627     fieldValue += FieldFormat::columnDelimiterString();
0628     fieldValue += entry->field(QStringLiteral("artist"));
0629   }
0630   if(fieldValue.isEmpty()) {
0631     return true;
0632   }
0633 
0634   // special case: if the i18n attribute equals true, then translate the title, description, and category
0635   if(m_i18n) {
0636     fieldValue = i18n(fieldValue.toUtf8().constData());
0637   }
0638   // special case for isbn fields, go ahead and validate
0639   if(m_validateISBN) {
0640     ISBNValidator val(nullptr);
0641     val.fixup(fieldValue);
0642   }
0643   if(f->type() == Data::Field::Table) {
0644     QString oldValue = entry->field(fieldName);
0645     if(!oldValue.isEmpty()) {
0646       if(!oldValue.endsWith(FieldFormat::rowDelimiterString())) {
0647         oldValue += FieldFormat::rowDelimiterString();
0648       }
0649       fieldValue.prepend(oldValue);
0650     }
0651   } else if(f->hasFlag(Data::Field::AllowMultiple)) {
0652     // for fields with multiple values, we need to add on the new value
0653     const QString oldValue = entry->field(fieldName);
0654     if(!oldValue.isEmpty()) {
0655       fieldValue = oldValue + FieldFormat::delimiterString() + fieldValue;
0656     }
0657   }
0658 
0659   // since the modified date value in the entry gets changed every time we set a new value
0660   // we have to save it and set it after changing all the others
0661   if(fieldName == QLatin1String("mdate")) {
0662     d->modifiedDate = fieldValue;
0663   } else {
0664     // no need to update the modified date when setting the entry's field value
0665     entry->setField(fieldName, fieldValue, false /* no modified date update */);
0666   }
0667   return true;
0668 }
0669 
0670 bool DateValueHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0671   return true;
0672 }
0673 
0674 bool DateValueHandler::end(const QStringRef&, const QStringRef& localName_) {
0675   QStringList tokens;
0676   if(d->textBuffer.isEmpty()) {
0677     // the data value is y-m-d even if there are no date values, so create list of blank tokens
0678     tokens = QStringList() << QString() << QString() << QString();
0679   } else {
0680 #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
0681     tokens = d->textBuffer.split(QLatin1Char('-'), QString::KeepEmptyParts);
0682 #else
0683     tokens = d->textBuffer.split(QLatin1Char('-'), Qt::KeepEmptyParts);
0684 #endif
0685   }
0686   Q_ASSERT(tokens.size() == 3);
0687   while(tokens.size() < 3) {
0688     tokens += QString();
0689   }
0690   if(localName_ == QLatin1String("year")) {
0691     tokens[0] = d->text;
0692   } else if(localName_ == QLatin1String("month")) {
0693     // enforce two digits for month
0694     while(d->text.length() < 2) {
0695       d->text.prepend(QLatin1Char('0'));
0696     }
0697     tokens[1] = d->text;
0698   } else if(localName_ == QLatin1String("day")) {
0699     // enforce two digits for day
0700     while(d->text.length() < 2) {
0701       d->text.prepend(QLatin1Char('0'));
0702     }
0703     tokens[2] = d->text;
0704   }
0705   d->textBuffer = tokens.join(QLatin1String("-"));
0706   return true;
0707 }
0708 
0709 bool TableColumnHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0710   return true;
0711 }
0712 
0713 bool TableColumnHandler::end(const QStringRef&, const QStringRef&) {
0714   // for old collections, if the second column holds the track length, bump it to next column
0715   if(d->syntaxVersion < 9 &&
0716      d->coll->type() == Data::Collection::Album &&
0717      d->currentField->name() == QLatin1String("track") &&
0718      !d->textBuffer.isEmpty() &&
0719      d->textBuffer.contains(FieldFormat::columnDelimiterString()) == 0) {
0720     static const QRegularExpression rx(QLatin1String("^\\d+:\\d\\d$"));
0721     if(rx.match(d->text).hasMatch()) {
0722       d->text += FieldFormat::columnDelimiterString();
0723       d->text += d->entries.back()->field(QStringLiteral("artist"));
0724     }
0725   }
0726 
0727   d->textBuffer += d->text + FieldFormat::columnDelimiterString();
0728   return true;
0729 }
0730 
0731 StateHandler* ImagesHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0732   if(localName_ == QLatin1String("image")) {
0733     return new ImageHandler(d);
0734   }
0735   return nullptr;
0736 }
0737 
0738 bool ImagesHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0739   // reset variable that gets updated in the image handler
0740   d->hasImages = false;
0741   return true;
0742 }
0743 
0744 bool ImagesHandler::end(const QStringRef&, const QStringRef&) {
0745   return true;
0746 }
0747 
0748 bool ImageHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0749   m_format = attValue(atts_, "format");
0750   m_link = atts_.value(QLatin1String("link")) == QLatin1String("true");
0751   // idClean() already calls shareString()
0752   m_imageId = m_link ? shareString(attValue(atts_, "id"))
0753                      : Data::Image::idClean(attValue(atts_, "id"));
0754   m_width = atts_.value(QLatin1String("width")).toInt();
0755   m_height = atts_.value(QLatin1String("height")).toInt();
0756   return true;
0757 }
0758 
0759 bool ImageHandler::end(const QStringRef&, const QStringRef&) {
0760   bool needToAddInfo = true;
0761   if(d->loadImages && !d->text.isEmpty()) {
0762     QByteArray ba = QByteArray::fromBase64(d->text.toLatin1());
0763     if(!ba.isEmpty()) {
0764       QString result = ImageFactory::addImage(ba, m_format, m_imageId);
0765       if(result.isEmpty()) {
0766         myDebug() << "null image for" << m_imageId;
0767       }
0768       d->hasImages = true;
0769       needToAddInfo = false;
0770     }
0771   }
0772   if(needToAddInfo) {
0773     // a width or height of 0 is ok here
0774     Data::ImageInfo info(m_imageId, m_format.toLatin1(), m_width, m_height, m_link);
0775     ImageFactory::cacheImageInfo(info);
0776   }
0777   return true;
0778 }
0779 
0780 StateHandler* FiltersHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0781   if(localName_ == QLatin1String("filter")) {
0782     return new FilterHandler(d);
0783   }
0784   return nullptr;
0785 }
0786 
0787 bool FiltersHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0788   return true;
0789 }
0790 
0791 bool FiltersHandler::end(const QStringRef&, const QStringRef&) {
0792   return true;
0793 }
0794 
0795 StateHandler* FilterHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0796   if(localName_ == QLatin1String("rule")) {
0797     return new FilterRuleHandler(d);
0798   }
0799   return nullptr;
0800 }
0801 
0802 bool FilterHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0803   d->filter = new Filter(Filter::MatchAny);
0804   d->filter->setName(attValue(atts_, "name"));
0805 
0806   if(atts_.value(QLatin1String("match")) == QLatin1String("all")) {
0807     d->filter->setMatch(Filter::MatchAll);
0808   }
0809   return true;
0810 }
0811 
0812 bool FilterHandler::end(const QStringRef&, const QStringRef&) {
0813   if(d->coll && !d->filter->isEmpty()) {
0814     d->coll->addFilter(d->filter);
0815   }
0816   d->filter = FilterPtr();
0817   return true;
0818 }
0819 
0820 bool FilterRuleHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0821   QString field = attValue(atts_, "field");
0822   // empty field means match any of them
0823   QString pattern = attValue(atts_, "pattern");
0824   // empty pattern is bad
0825   if(pattern.isEmpty()) {
0826     myWarning() << "empty rule!";
0827     return true;
0828   }
0829   /* If anything is updated here, be sure to update tellicoxmlexporter */
0830   QString function = attValue(atts_, "function").toLower();
0831   FilterRule::Function func;
0832   if(function == QLatin1String("contains")) {
0833     func = FilterRule::FuncContains;
0834   } else if(function == QLatin1String("notcontains")) {
0835     func = FilterRule::FuncNotContains;
0836   } else if(function == QLatin1String("equals")) {
0837     func = FilterRule::FuncEquals;
0838   } else if(function == QLatin1String("notequals")) {
0839     func = FilterRule::FuncNotEquals;
0840   } else if(function == QLatin1String("regexp")) {
0841     func = FilterRule::FuncRegExp;
0842   } else if(function == QLatin1String("notregexp")) {
0843     func = FilterRule::FuncNotRegExp;
0844   } else if(function == QLatin1String("before")) {
0845     func = FilterRule::FuncBefore;
0846   } else if(function == QLatin1String("after")) {
0847     func = FilterRule::FuncAfter;
0848   } else if(function == QLatin1String("greaterthan")) {
0849     func = FilterRule::FuncGreater;
0850   } else if(function == QLatin1String("lessthan")) {
0851     func = FilterRule::FuncLess;
0852   } else {
0853     myWarning() << "invalid rule function:" << function;
0854     return true;
0855   }
0856   d->filter->append(new FilterRule(field, pattern, func));
0857   return true;
0858 }
0859 
0860 bool FilterRuleHandler::end(const QStringRef&, const QStringRef&) {
0861   return true;
0862 }
0863 
0864 StateHandler* BorrowersHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0865   if(localName_ == QLatin1String("borrower")) {
0866     return new BorrowerHandler(d);
0867   }
0868   return nullptr;
0869 }
0870 
0871 bool BorrowersHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0872   return true;
0873 }
0874 
0875 bool BorrowersHandler::end(const QStringRef&, const QStringRef&) {
0876   return true;
0877 }
0878 
0879 StateHandler* BorrowerHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0880   if(localName_ == QLatin1String("loan")) {
0881     return new LoanHandler(d);
0882   }
0883   return nullptr;
0884 }
0885 
0886 bool BorrowerHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0887   QString name = attValue(atts_, "name");
0888   QString uid = attValue(atts_, "uid");
0889   d->borrower = new Data::Borrower(name, uid);
0890 
0891   return true;
0892 }
0893 
0894 bool BorrowerHandler::end(const QStringRef&, const QStringRef&) {
0895   if(d->coll && !d->borrower->isEmpty()) {
0896     d->coll->addBorrower(d->borrower);
0897   }
0898   d->borrower = Data::BorrowerPtr();
0899   return true;
0900 }
0901 
0902 bool LoanHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0903   m_id = attValue(atts_, "entryRef").toInt();
0904   m_uid = attValue(atts_, "uid");
0905   m_loanDate = attValue(atts_, "loanDate");
0906   m_dueDate = attValue(atts_, "dueDate");
0907   m_inCalendar = atts_.value(QLatin1String("calendar")) == QLatin1String("true");
0908   return true;
0909 }
0910 
0911 bool LoanHandler::end(const QStringRef&, const QStringRef&) {
0912   Data::EntryPtr entry = d->coll->entryById(m_id);
0913   if(!entry) {
0914     myWarning() << "no entry with id = " << m_id;
0915     return true;
0916   }
0917   QDate loanDate, dueDate;
0918   if(!m_loanDate.isEmpty()) {
0919     loanDate = QDate::fromString(m_loanDate, Qt::ISODate);
0920   }
0921   if(!m_dueDate.isEmpty()) {
0922     dueDate = QDate::fromString(m_dueDate, Qt::ISODate);
0923   }
0924 
0925   Data::LoanPtr loan(new Data::Loan(entry, loanDate, dueDate, d->text));
0926   loan->setUID(m_uid);
0927   loan->setInCalendar(m_inCalendar);
0928   d->borrower->addLoan(loan);
0929   return true;
0930 }