File indexing completed on 2024-05-12 16:46:35

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         const QUrl u = QUrl::fromUserInput(value);
0203         // the image file name is a valid URL, but I want it to be a local URL or non empty remote one
0204         if(u.isValid() && (u.isLocalFile() || !u.host().isEmpty())) {
0205           const QString result = ImageFactory::addImage(u, !d->showImageLoadErrors || imageWarnings >= maxImageWarnings /* quiet */);
0206           if(result.isEmpty()) {
0207             // clear value for the field in this case
0208             value.clear();
0209             ++imageWarnings;
0210           } else {
0211             value = result;
0212           }
0213         } else {
0214           value = Data::Image::idClean(value);
0215         }
0216         // reset the image id to be whatever was loaded
0217         entry->setField(field->name(), value, false /* no modified date update */);
0218       }
0219     }
0220   }
0221   return true;
0222 }
0223 
0224 StateHandler* FieldsHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0225   if((d->syntaxVersion > 3 && localName_ == QLatin1String("field")) ||
0226      (d->syntaxVersion < 4 && localName_ == QLatin1String("attribute"))) {
0227     return new FieldHandler(d);
0228   }
0229   return nullptr;
0230 }
0231 
0232 bool FieldsHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0233   d->defaultFields = false;
0234   return true;
0235 }
0236 
0237 bool FieldsHandler::end(const QStringRef&, const QStringRef&) {
0238   // add default fields if there was a default field name, or no names at all
0239   const bool addFields = d->defaultFields || d->fields.isEmpty();
0240   // in syntax 4, the element name was changed to "entry", always, rather than depending on
0241   // on the entryName of the collection.
0242   if(d->syntaxVersion > 3) {
0243     d->entryName = QStringLiteral("entry");
0244     Data::Collection::Type type = static_cast<Data::Collection::Type>(d->collType);
0245     d->coll = CollectionFactory::collection(type, addFields);
0246   } else {
0247     d->coll = CollectionFactory::collection(d->entryName, addFields);
0248   }
0249 
0250   if(!d->collTitle.isEmpty()) {
0251     d->coll->setTitle(d->collTitle);
0252   }
0253 
0254   // add a default field for ID
0255   // checking the defaultFields bool since if it is true, we already added these default fields
0256   // even for old syntax versions
0257   if(d->syntaxVersion < 11 && !d->defaultFields) {
0258     d->coll->addField(Data::Field::createDefaultField(Data::Field::IDField));
0259   }
0260   // now add all the new fields
0261   d->coll->addFields(d->fields);
0262   if(d->syntaxVersion < 11 && !d->defaultFields) {
0263     d->coll->addField(Data::Field::createDefaultField(Data::Field::CreatedDateField));
0264     d->coll->addField(Data::Field::createDefaultField(Data::Field::ModifiedDateField));
0265   }
0266 
0267 //  as a special case, for old book collections with a bibtex-id field, convert to Bibtex
0268   if(d->syntaxVersion < 4 && d->collType == Data::Collection::Book
0269      && d->coll->hasField(QStringLiteral("bibtex-id"))) {
0270     d->coll = Data::BibtexCollection::convertBookCollection(d->coll);
0271   }
0272 
0273   return true;
0274 }
0275 
0276 StateHandler* FieldHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0277   if(localName_ == QLatin1String("prop")) {
0278     return new FieldPropertyHandler(d);
0279   }
0280   return nullptr;
0281 }
0282 
0283 bool FieldHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0284   // special case: if the i18n attribute equals true, then translate the title, description, category, and allowed
0285   const bool isI18n = atts_.value(QLatin1String("i18n")) == QLatin1String("true");
0286 
0287   const QString name = attValue(atts_, "name", "unknown");
0288   if(name == QLatin1String("_default")) {
0289     d->defaultFields = true;
0290     return true;
0291   }
0292 
0293   QString title  = attValue(atts_, "title", i18n("Unknown"));
0294   if(isI18n && !title.isEmpty()) {
0295     title = i18n(title.toUtf8().constData());
0296   }
0297 
0298   QString typeStr = attValue(atts_, "type", QString::number(Data::Field::Line));
0299   Data::Field::Type type = static_cast<Data::Field::Type>(typeStr.toInt());
0300 
0301   Data::FieldPtr field;
0302   if(type == Data::Field::Choice) {
0303     QStringList allowed = FieldFormat::splitValue(attValue(atts_, "allowed"), FieldFormat::RegExpSplit);
0304     if(isI18n) {
0305       for(QStringList::Iterator word = allowed.begin(); word != allowed.end(); ++word) {
0306         (*word) = i18n((*word).toUtf8().constData());
0307       }
0308     }
0309     field = new Data::Field(name, title, allowed);
0310   } else {
0311     field = new Data::Field(name, title, type);
0312   }
0313 
0314   QString cat = attValue(atts_, "category");
0315   // at one point, the categories had keyboard accels
0316   if(d->syntaxVersion < 9) {
0317     cat.remove(QLatin1Char('&'));
0318   }
0319   if(isI18n && !cat.isEmpty()) {
0320     cat = i18n(cat.toUtf8().constData());
0321   }
0322   field->setCategory(cat);
0323 
0324   int flags = atts_.value(QLatin1String("flags")).toInt();
0325   // I also changed the enum values for syntax 3, but the only custom field
0326   // would have been bibtex-id
0327   if(d->syntaxVersion < 3 && name == QLatin1String("bibtex-id")) {
0328     flags = 0;
0329   }
0330 
0331   // in syntax version 4, added a flag to disallow deleting attributes
0332   // if it's a version before that and is the title, then add the flag
0333   if(d->syntaxVersion < 4 && name == QLatin1String("title")) {
0334     flags |= Data::Field::NoDelete;
0335   }
0336   // some of the flags may have been set in the constructor
0337   // in the case of old Dependent fields changing, for example
0338   // so combine with the existing flags
0339   field->setFlags(field->flags() | flags);
0340 
0341   QString formatStr = attValue(atts_, "format", QString::number(FieldFormat::FormatNone));
0342   FieldFormat::Type formatType = static_cast<FieldFormat::Type>(formatStr.toInt());
0343   field->setFormatType(formatType);
0344 
0345   QString desc = attValue(atts_, "description");
0346   if(isI18n && !desc.isEmpty()) {
0347     desc = i18n(desc.toUtf8().constData());
0348   }
0349   field->setDescription(desc);
0350 
0351   if(d->syntaxVersion < 5 && atts_.hasAttribute(QLatin1String("bibtex-field"))) {
0352     field->setProperty(QStringLiteral("bibtex"), attValue(atts_, "bibtex-field"));
0353   }
0354 
0355   // for syntax 8, rating fields got their own type
0356   if(d->syntaxVersion < 8) {
0357     Data::Field::convertOldRating(field); // does all its own checking
0358   }
0359   d->fields.append(field);
0360 
0361   return true;
0362 }
0363 
0364 bool FieldHandler::end(const QStringRef&, const QStringRef&) {
0365   // the value template for derived values used to be the field description
0366   // now it is the 'template' property
0367   // for derived value fields, if there is no property and the description has a '%'
0368   // move it to the property
0369   //
0370   // might be empty is we're only adding default fields
0371   if(!d->fields.isEmpty()) {
0372     Data::FieldPtr field = d->fields.back();
0373     if(field->hasFlag(Data::Field::Derived) &&
0374        field->property(QStringLiteral("template")).isEmpty() &&
0375        field->description().contains(QLatin1Char('%'))) {
0376       field->setProperty(QStringLiteral("template"), field->description());
0377       field->setDescription(QString());
0378     }
0379   }
0380 
0381   return true;
0382 }
0383 
0384 bool FieldPropertyHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0385   // there should be at least one field already so we can add properties to it
0386   Q_ASSERT(!d->fields.isEmpty());
0387   Data::FieldPtr field = d->fields.back();
0388 
0389   m_propertyName = attValue(atts_, "name");
0390 
0391   // all track fields in music collections prior to version 9 get converted to three columns
0392   if(d->syntaxVersion < 9) {
0393     if(d->collType == Data::Collection::Album && field->name() == QLatin1String("track")) {
0394       field->setProperty(QStringLiteral("columns"), QStringLiteral("3"));
0395       field->setProperty(QStringLiteral("column1"), i18n("Title"));
0396       field->setProperty(QStringLiteral("column2"), i18n("Artist"));
0397       field->setProperty(QStringLiteral("column3"), i18n("Length"));
0398     } else if(d->collType == Data::Collection::Video && field->name() == QLatin1String("cast")) {
0399       field->setProperty(QStringLiteral("column1"), i18n("Actor/Actress"));
0400       field->setProperty(QStringLiteral("column2"), i18n("Role"));
0401     }
0402   }
0403 
0404   return true;
0405 }
0406 
0407 bool FieldPropertyHandler::end(const QStringRef&, const QStringRef&) {
0408   Q_ASSERT(!m_propertyName.isEmpty());
0409   // add the previous property
0410   Data::FieldPtr field = d->fields.back();
0411   field->setProperty(m_propertyName, d->text);
0412   return true;
0413 }
0414 
0415 bool BibtexPreambleHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0416   return true;
0417 }
0418 
0419 bool BibtexPreambleHandler::end(const QStringRef&, const QStringRef&) {
0420   Q_ASSERT(d->coll);
0421   if(d->coll && d->collType == Data::Collection::Bibtex && !d->text.isEmpty()) {
0422     Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(d->coll.data());
0423     c->setPreamble(d->text);
0424   }
0425   return true;
0426 }
0427 
0428 StateHandler* BibtexMacrosHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0429   if(localName_ == QLatin1String("macro")) {
0430     return new BibtexMacroHandler(d);
0431   }
0432   return nullptr;
0433 }
0434 
0435 bool BibtexMacrosHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0436   return true;
0437 }
0438 
0439 bool BibtexMacrosHandler::end(const QStringRef&, const QStringRef&) {
0440   return true;
0441 }
0442 
0443 bool BibtexMacroHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0444   m_macroName = attValue(atts_, "name");
0445   return true;
0446 }
0447 
0448 bool BibtexMacroHandler::end(const QStringRef&, const QStringRef&) {
0449   if(d->coll && d->collType == Data::Collection::Bibtex && !m_macroName.isEmpty() && !d->text.isEmpty()) {
0450     Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(d->coll.data());
0451     c->addMacro(m_macroName, d->text);
0452   }
0453   return true;
0454 }
0455 
0456 StateHandler* EntryHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0457   if(d->coll->hasField(realFieldName(d->syntaxVersion, localName_))) {
0458     return new FieldValueHandler(d);
0459   }
0460   return new FieldValueContainerHandler(d);
0461 }
0462 
0463 bool EntryHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0464   // the entries must come after the fields
0465   if(!d->coll || d->coll->fields().isEmpty()) {
0466     // special case for very old versions which did not have user-editable fields
0467     // also maybe a new version has bad formatting, try to recover by assuming default fields
0468     d->defaultFields = true;
0469     FieldsHandler handler(d);
0470     // fake the end of a fields element, which will add the default fields
0471     handler.end(QStringRef(), QStringRef());
0472     myWarning() << "entries should come after fields are defined, attempting to recover";
0473   }
0474   bool ok;
0475   const int id = atts_.value(QLatin1String("id")).toInt(&ok);
0476   Data::EntryPtr entry;
0477   if(ok && id > -1) {
0478     entry = new Data::Entry(d->coll, id);
0479   } else {
0480     entry = new Data::Entry(d->coll);
0481   }
0482   d->entries.append(entry);
0483   return true;
0484 }
0485 
0486 bool EntryHandler::end(const QStringRef&, const QStringRef&) {
0487   Data::EntryPtr entry = d->entries.back();
0488   Q_ASSERT(entry);
0489   if(!d->modifiedDate.isEmpty() && d->coll->hasField(QStringLiteral("mdate"))) {
0490     entry->setField(QStringLiteral("mdate"), d->modifiedDate);
0491     d->modifiedDate.clear();
0492   }
0493   return true;
0494 }
0495 
0496 StateHandler* FieldValueContainerHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0497   if(d->coll->hasField(realFieldName(d->syntaxVersion, localName_))) {
0498     return new FieldValueHandler(d);
0499   }
0500   return new FieldValueContainerHandler(d);
0501 }
0502 
0503 bool FieldValueContainerHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0504   return true;
0505 }
0506 
0507 bool FieldValueContainerHandler::end(const QStringRef&, const QStringRef&) {
0508   Data::FieldPtr f = d->currentField;
0509   if(f && f->type() == Data::Field::Table) {
0510     Data::EntryPtr entry = d->entries.back();
0511     Q_ASSERT(entry);
0512     QString fieldValue = entry->field(f->name());
0513     // don't allow table value to end with empty row
0514     while(fieldValue.endsWith(FieldFormat::rowDelimiterString())) {
0515       fieldValue.chop(FieldFormat::rowDelimiterString().length());
0516       // no need to update the modified date when setting the entry's field value
0517       entry->setField(f->name(), fieldValue, false /* no modified date update */);
0518     }
0519   }
0520 
0521   return true;
0522 }
0523 
0524 StateHandler* FieldValueHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0525   if(localName_ == QLatin1String("year") ||
0526      localName_ == QLatin1String("month") ||
0527      localName_ == QLatin1String("day")) {
0528     return new DateValueHandler(d);
0529   } else if(localName_ == QLatin1String("column")) {
0530     return new TableColumnHandler(d);
0531   }
0532   return nullptr;
0533 }
0534 
0535 bool FieldValueHandler::start(const QStringRef&, const QStringRef& localName_, const QXmlStreamAttributes& atts_) {
0536   d->currentField = d->coll->fieldByName(realFieldName(d->syntaxVersion, localName_));
0537   Q_ASSERT(d->currentField);
0538   m_i18n = atts_.value(QLatin1String("i18n")) == QLatin1String("true");
0539   m_validateISBN = (localName_ == QLatin1String("isbn")) &&
0540                    (atts_.value(QLatin1String("validate")) != QLatin1String("no"));
0541   return true;
0542 }
0543 
0544 bool FieldValueHandler::end(const QStringRef&, const QStringRef& localName_) {
0545   Data::EntryPtr entry = d->entries.back();
0546   Q_ASSERT(entry);
0547   QString fieldName = d->currentField ? d->currentField->name() : realFieldName(d->syntaxVersion, localName_);
0548 
0549   Data::FieldPtr f = d->currentField;
0550   if(!f) {
0551     myWarning() << "no field named " << fieldName;
0552     return true;
0553   }
0554   // if it's a derived value, no field value is added
0555   if(f->hasFlag(Data::Field::Derived)) {
0556     return true;
0557   }
0558 
0559   QString fieldValue = d->text;
0560   if(d->syntaxVersion < 4 && f->type() == Data::Field::Bool) {
0561     // in version 3 and prior, checkbox attributes had no text(), set it to "true"
0562     fieldValue = QStringLiteral("true");
0563   } else if(d->syntaxVersion < 8 && f->type() == Data::Field::Rating) {
0564     // in version 8, old rating fields get changed
0565     bool ok;
0566     uint i = Tellico::toUInt(fieldValue, &ok);
0567     if(ok) {
0568       fieldValue = QString::number(i);
0569     }
0570   } else if(!d->textBuffer.isEmpty()) {
0571     // for dates and tables, the value is built up from child elements
0572     if(!d->text.isEmpty()) {
0573       myWarning() << "ignoring value for field" << localName_ << ":" << d->text;
0574     }
0575     fieldValue = d->textBuffer;
0576     // the text buffer has the column delimiter at the end, remove it
0577     if(f->type() == Data::Field::Table) {
0578       fieldValue.chop(FieldFormat::columnDelimiterString().length());
0579     }
0580     d->textBuffer.clear();
0581   } else if(fieldValue.isEmpty() && f->type() == Data::Field::Table) {
0582     // allow for empty table rows
0583     fieldValue = FieldFormat::rowDelimiterString();
0584   }
0585   // this is not an else branch, the data may be in the textBuffer
0586   if(d->syntaxVersion < 9 && d->coll->type() == Data::Collection::Album && fieldName == QLatin1String("track")) {
0587     // yes, this assumes the artist has already been set
0588     fieldValue += FieldFormat::columnDelimiterString();
0589     fieldValue += entry->field(QStringLiteral("artist"));
0590   }
0591   if(fieldValue.isEmpty()) {
0592     return true;
0593   }
0594 
0595   // special case: if the i18n attribute equals true, then translate the title, description, and category
0596   if(m_i18n) {
0597     fieldValue = i18n(fieldValue.toUtf8().constData());
0598   }
0599   // special case for isbn fields, go ahead and validate
0600   if(m_validateISBN) {
0601     ISBNValidator val(nullptr);
0602     val.fixup(fieldValue);
0603   }
0604   if(f->type() == Data::Field::Table) {
0605     QString oldValue = entry->field(fieldName);
0606     if(!oldValue.isEmpty()) {
0607       if(!oldValue.endsWith(FieldFormat::rowDelimiterString())) {
0608         oldValue += FieldFormat::rowDelimiterString();
0609       }
0610       fieldValue.prepend(oldValue);
0611     }
0612   } else if(f->hasFlag(Data::Field::AllowMultiple)) {
0613     // for fields with multiple values, we need to add on the new value
0614     const QString oldValue = entry->field(fieldName);
0615     if(!oldValue.isEmpty()) {
0616       fieldValue = oldValue + FieldFormat::delimiterString() + fieldValue;
0617     }
0618   }
0619 
0620   // since the modified date value in the entry gets changed every time we set a new value
0621   // we have to save it and set it after changing all the others
0622   if(fieldName == QLatin1String("mdate")) {
0623     d->modifiedDate = fieldValue;
0624   } else {
0625     // no need to update the modified date when setting the entry's field value
0626     entry->setField(fieldName, fieldValue, false /* no modified date update */);
0627   }
0628   return true;
0629 }
0630 
0631 bool DateValueHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0632   return true;
0633 }
0634 
0635 bool DateValueHandler::end(const QStringRef&, const QStringRef& localName_) {
0636   QStringList tokens;
0637   if(d->textBuffer.isEmpty()) {
0638     // the data value is y-m-d even if there are no date values, so create list of blank tokens
0639     tokens = QStringList() << QString() << QString() << QString();
0640   } else {
0641 #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
0642     tokens = d->textBuffer.split(QLatin1Char('-'), QString::KeepEmptyParts);
0643 #else
0644     tokens = d->textBuffer.split(QLatin1Char('-'), Qt::KeepEmptyParts);
0645 #endif
0646   }
0647   Q_ASSERT(tokens.size() == 3);
0648   while(tokens.size() < 3) {
0649     tokens += QString();
0650   }
0651   if(localName_ == QLatin1String("year")) {
0652     tokens[0] = d->text;
0653   } else if(localName_ == QLatin1String("month")) {
0654     // enforce two digits for month
0655     while(d->text.length() < 2) {
0656       d->text.prepend(QLatin1Char('0'));
0657     }
0658     tokens[1] = d->text;
0659   } else if(localName_ == QLatin1String("day")) {
0660     // enforce two digits for day
0661     while(d->text.length() < 2) {
0662       d->text.prepend(QLatin1Char('0'));
0663     }
0664     tokens[2] = d->text;
0665   }
0666   d->textBuffer = tokens.join(QLatin1String("-"));
0667   return true;
0668 }
0669 
0670 bool TableColumnHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0671   return true;
0672 }
0673 
0674 bool TableColumnHandler::end(const QStringRef&, const QStringRef&) {
0675   // for old collections, if the second column holds the track length, bump it to next column
0676   if(d->syntaxVersion < 9 &&
0677      d->coll->type() == Data::Collection::Album &&
0678      d->currentField->name() == QLatin1String("track") &&
0679      !d->textBuffer.isEmpty() &&
0680      d->textBuffer.contains(FieldFormat::columnDelimiterString()) == 0) {
0681     const QRegularExpression rx(QLatin1String("^\\d+:\\d\\d$"));
0682     if(rx.match(d->text).hasMatch()) {
0683       d->text += FieldFormat::columnDelimiterString();
0684       d->text += d->entries.back()->field(QStringLiteral("artist"));
0685     }
0686   }
0687 
0688   d->textBuffer += d->text + FieldFormat::columnDelimiterString();
0689   return true;
0690 }
0691 
0692 StateHandler* ImagesHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0693   if(localName_ == QLatin1String("image")) {
0694     return new ImageHandler(d);
0695   }
0696   return nullptr;
0697 }
0698 
0699 bool ImagesHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0700   // reset variable that gets updated in the image handler
0701   d->hasImages = false;
0702   return true;
0703 }
0704 
0705 bool ImagesHandler::end(const QStringRef&, const QStringRef&) {
0706   return true;
0707 }
0708 
0709 bool ImageHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0710   m_format = attValue(atts_, "format");
0711   m_link = atts_.value(QLatin1String("link")) == QLatin1String("true");
0712   // idClean() already calls shareString()
0713   m_imageId = m_link ? shareString(attValue(atts_, "id"))
0714                      : Data::Image::idClean(attValue(atts_, "id"));
0715   m_width = atts_.value(QLatin1String("width")).toInt();
0716   m_height = atts_.value(QLatin1String("height")).toInt();
0717   return true;
0718 }
0719 
0720 bool ImageHandler::end(const QStringRef&, const QStringRef&) {
0721   bool needToAddInfo = true;
0722   if(d->loadImages && !d->text.isEmpty()) {
0723     QByteArray ba = QByteArray::fromBase64(d->text.toLatin1());
0724     if(!ba.isEmpty()) {
0725       QString result = ImageFactory::addImage(ba, m_format, m_imageId);
0726       if(result.isEmpty()) {
0727         myDebug() << "null image for" << m_imageId;
0728       }
0729       d->hasImages = true;
0730       needToAddInfo = false;
0731     }
0732   }
0733   if(needToAddInfo) {
0734     // a width or height of 0 is ok here
0735     Data::ImageInfo info(m_imageId, m_format.toLatin1(), m_width, m_height, m_link);
0736     ImageFactory::cacheImageInfo(info);
0737   }
0738   return true;
0739 }
0740 
0741 StateHandler* FiltersHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0742   if(localName_ == QLatin1String("filter")) {
0743     return new FilterHandler(d);
0744   }
0745   return nullptr;
0746 }
0747 
0748 bool FiltersHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0749   return true;
0750 }
0751 
0752 bool FiltersHandler::end(const QStringRef&, const QStringRef&) {
0753   return true;
0754 }
0755 
0756 StateHandler* FilterHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0757   if(localName_ == QLatin1String("rule")) {
0758     return new FilterRuleHandler(d);
0759   }
0760   return nullptr;
0761 }
0762 
0763 bool FilterHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0764   d->filter = new Filter(Filter::MatchAny);
0765   d->filter->setName(attValue(atts_, "name"));
0766 
0767   if(atts_.value(QLatin1String("match")) == QLatin1String("all")) {
0768     d->filter->setMatch(Filter::MatchAll);
0769   }
0770   return true;
0771 }
0772 
0773 bool FilterHandler::end(const QStringRef&, const QStringRef&) {
0774   if(d->coll && !d->filter->isEmpty()) {
0775     d->coll->addFilter(d->filter);
0776   }
0777   d->filter = FilterPtr();
0778   return true;
0779 }
0780 
0781 bool FilterRuleHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0782   QString field = attValue(atts_, "field");
0783   // empty field means match any of them
0784   QString pattern = attValue(atts_, "pattern");
0785   // empty pattern is bad
0786   if(pattern.isEmpty()) {
0787     myWarning() << "empty rule!";
0788     return true;
0789   }
0790   /* If anything is updated here, be sure to update tellicoxmlexporter */
0791   QString function = attValue(atts_, "function").toLower();
0792   FilterRule::Function func;
0793   if(function == QLatin1String("contains")) {
0794     func = FilterRule::FuncContains;
0795   } else if(function == QLatin1String("notcontains")) {
0796     func = FilterRule::FuncNotContains;
0797   } else if(function == QLatin1String("equals")) {
0798     func = FilterRule::FuncEquals;
0799   } else if(function == QLatin1String("notequals")) {
0800     func = FilterRule::FuncNotEquals;
0801   } else if(function == QLatin1String("regexp")) {
0802     func = FilterRule::FuncRegExp;
0803   } else if(function == QLatin1String("notregexp")) {
0804     func = FilterRule::FuncNotRegExp;
0805   } else if(function == QLatin1String("before")) {
0806     func = FilterRule::FuncBefore;
0807   } else if(function == QLatin1String("after")) {
0808     func = FilterRule::FuncAfter;
0809   } else if(function == QLatin1String("greaterthan")) {
0810     func = FilterRule::FuncGreater;
0811   } else if(function == QLatin1String("lessthan")) {
0812     func = FilterRule::FuncLess;
0813   } else {
0814     myWarning() << "invalid rule function:" << function;
0815     return true;
0816   }
0817   d->filter->append(new FilterRule(field, pattern, func));
0818   return true;
0819 }
0820 
0821 bool FilterRuleHandler::end(const QStringRef&, const QStringRef&) {
0822   return true;
0823 }
0824 
0825 StateHandler* BorrowersHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0826   if(localName_ == QLatin1String("borrower")) {
0827     return new BorrowerHandler(d);
0828   }
0829   return nullptr;
0830 }
0831 
0832 bool BorrowersHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes&) {
0833   return true;
0834 }
0835 
0836 bool BorrowersHandler::end(const QStringRef&, const QStringRef&) {
0837   return true;
0838 }
0839 
0840 StateHandler* BorrowerHandler::nextHandlerImpl(const QStringRef&, const QStringRef& localName_) {
0841   if(localName_ == QLatin1String("loan")) {
0842     return new LoanHandler(d);
0843   }
0844   return nullptr;
0845 }
0846 
0847 bool BorrowerHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0848   QString name = attValue(atts_, "name");
0849   QString uid = attValue(atts_, "uid");
0850   d->borrower = new Data::Borrower(name, uid);
0851 
0852   return true;
0853 }
0854 
0855 bool BorrowerHandler::end(const QStringRef&, const QStringRef&) {
0856   if(d->coll && !d->borrower->isEmpty()) {
0857     d->coll->addBorrower(d->borrower);
0858   }
0859   d->borrower = Data::BorrowerPtr();
0860   return true;
0861 }
0862 
0863 bool LoanHandler::start(const QStringRef&, const QStringRef&, const QXmlStreamAttributes& atts_) {
0864   m_id = attValue(atts_, "entryRef").toInt();
0865   m_uid = attValue(atts_, "uid");
0866   m_loanDate = attValue(atts_, "loanDate");
0867   m_dueDate = attValue(atts_, "dueDate");
0868   m_inCalendar = atts_.value(QLatin1String("calendar")) == QLatin1String("true");
0869   return true;
0870 }
0871 
0872 bool LoanHandler::end(const QStringRef&, const QStringRef&) {
0873   Data::EntryPtr entry = d->coll->entryById(m_id);
0874   if(!entry) {
0875     myWarning() << "no entry with id = " << m_id;
0876     return true;
0877   }
0878   QDate loanDate, dueDate;
0879   if(!m_loanDate.isEmpty()) {
0880     loanDate = QDate::fromString(m_loanDate, Qt::ISODate);
0881   }
0882   if(!m_dueDate.isEmpty()) {
0883     dueDate = QDate::fromString(m_dueDate, Qt::ISODate);
0884   }
0885 
0886   Data::LoanPtr loan(new Data::Loan(entry, loanDate, dueDate, d->text));
0887   loan->setUID(m_uid);
0888   loan->setInCalendar(m_inCalendar);
0889   d->borrower->addLoan(loan);
0890   return true;
0891 }