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 }