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 }