diff --git a/XMLDB/Database.cpp b/XMLDB/Database.cpp index 189080d6..394a7a58 100644 --- a/XMLDB/Database.cpp +++ b/XMLDB/Database.cpp @@ -1,778 +1,796 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Database.h" #include "FileReader.h" #include "FileWriter.h" #include "Logging.h" #include "XMLCategory.h" #include "XMLImageDateCollection.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Utilities::StringSet; namespace { void checkForBackupFile(const QString &fileName, DB::UIDelegate &ui) { QString backupName = QFileInfo(fileName).absolutePath() + QString::fromLatin1("/.#") + QFileInfo(fileName).fileName(); QFileInfo backUpFile(backupName); QFileInfo indexFile(fileName); if (!backUpFile.exists() || indexFile.lastModified() > backUpFile.lastModified() || backUpFile.size() == 0) return; const long backupSizeKB = backUpFile.size() >> 10; const DB::UserFeedback choice = ui.questionYesNo( QString::fromUtf8("Autosave file found: '%1', %2KB.").arg(backupName).arg(backupSizeKB), i18n("Autosave file '%1' exists (size %3 KB) and is newer than '%2'. " "Should the autosave file be used?", backupName, fileName, backupSizeKB), i18n("Found Autosave File")); if (choice == DB::UserFeedback::Confirm) { qCInfo(XMLDBLog) << "Using autosave file:" << backupName; QFile in(backupName); if (in.open(QIODevice::ReadOnly)) { QFile out(fileName); if (out.open(QIODevice::WriteOnly)) { char data[1024]; int len; while ((len = in.read(data, 1024))) out.write(data, len); } } } } } // namespace bool XMLDB::Database::s_anyImageWithEmptySize = false; XMLDB::Database::Database(const QString &configFile, DB::UIDelegate &delegate) : ImageDB(delegate) , m_fileName(configFile) { checkForBackupFile(configFile, uiDelegate()); FileReader reader(this); reader.read(configFile); m_nextStackId = reader.nextStackId(); connect(categoryCollection(), &DB::CategoryCollection::itemRemoved, this, &Database::deleteItem); connect(categoryCollection(), &DB::CategoryCollection::itemRenamed, this, &Database::renameItem); connect(categoryCollection(), &DB::CategoryCollection::itemRemoved, &m_members, &DB::MemberMap::deleteItem); connect(categoryCollection(), &DB::CategoryCollection::itemRenamed, &m_members, &DB::MemberMap::renameItem); connect(categoryCollection(), &DB::CategoryCollection::categoryRemoved, &m_members, &DB::MemberMap::deleteCategory); } uint XMLDB::Database::totalCount() const { return m_images.count(); } /** * I was considering merging the two calls to this method (one for images, one for video), but then I * realized that all the work is really done after the check for whether the given * imageInfo is of the right type, and as a match can't be both, this really * would buy me nothing. */ QMap XMLDB::Database::classify(const DB::ImageSearchInfo &info, const QString &category, DB::MediaType typemask, DB::ClassificationMode mode) { QElapsedTimer timer; timer.start(); QMap map; DB::GroupCounter counter(category); Utilities::StringSet alreadyMatched = info.findAlreadyMatched(category); DB::ImageSearchInfo noMatchInfo = info; QString currentMatchTxt = noMatchInfo.categoryMatchText(category); if (currentMatchTxt.isEmpty()) noMatchInfo.setCategoryMatchText(category, DB::ImageDB::NONE()); else noMatchInfo.setCategoryMatchText(category, QString::fromLatin1("%1 & %2").arg(currentMatchTxt).arg(DB::ImageDB::NONE())); noMatchInfo.setCacheable(false); // Iterate through the whole database of images. for (const auto &imageInfo : m_images) { bool match = ((imageInfo)->mediaType() & typemask) && !(imageInfo)->isLocked() && info.match(imageInfo) && rangeInclude(imageInfo); if (match) { // If the given image is currently matched. // Now iterate through all the categories the current image // contains, and increase them in the map mapping from category // to count. StringSet items = (imageInfo)->itemsOfCategory(category); counter.count(items, imageInfo->date()); for (const auto &categoryName : items) { if (!alreadyMatched.contains(categoryName)) // We do not want to match "Jesper & Jesper" map[categoryName].add(imageInfo->date()); } // Find those with no other matches if (noMatchInfo.match(imageInfo)) map[DB::ImageDB::NONE()].count++; // this is a shortcut for the browser overview page, // where we are only interested whether there are sub-categories to a category if (mode == DB::ClassificationMode::PartialCount && map.size() > 1) { qCInfo(TimingLog) << "Database::classify(partial): " << timer.restart() << "ms."; return map; } } } QMap groups = counter.result(); for (QMap::iterator it = groups.begin(); it != groups.end(); ++it) { map[it.key()] = it.value(); } qCInfo(TimingLog) << "Database::classify(): " << timer.restart() << "ms."; return map; } void XMLDB::Database::renameCategory(const QString &oldName, const QString newName) { for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) { (*it)->renameCategory(oldName, newName); } } void XMLDB::Database::addToBlockList(const DB::FileNameList &list) { Q_FOREACH (const DB::FileName &fileName, list) { m_blockList.insert(fileName); } deleteList(list); } void XMLDB::Database::deleteList(const DB::FileNameList &list) { Q_FOREACH (const DB::FileName &fileName, list) { DB::ImageInfoPtr inf = fileName.info(); StackMap::iterator found = m_stackMap.find(inf->stackId()); if (inf->isStacked() && found != m_stackMap.end()) { const DB::FileNameList origCache = found.value(); DB::FileNameList newCache; Q_FOREACH (const DB::FileName &cacheName, origCache) { if (fileName != cacheName) newCache.append(cacheName); } if (newCache.size() <= 1) { // we're destroying a stack Q_FOREACH (const DB::FileName &cacheName, newCache) { DB::ImageInfoPtr cacheInf = cacheName.info(); cacheInf->setStackId(0); cacheInf->setStackOrder(0); } m_stackMap.remove(inf->stackId()); } else { m_stackMap.insert(inf->stackId(), newCache); } } m_imageCache.remove(inf->fileName().absolute()); m_images.remove(inf); } Exif::Database::instance()->remove(list); emit totalChanged(m_images.count()); emit imagesDeleted(list); emit dirty(); } void XMLDB::Database::renameItem(DB::Category *category, const QString &oldName, const QString &newName) { for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) { (*it)->renameItem(category->name(), oldName, newName); } } void XMLDB::Database::deleteItem(DB::Category *category, const QString &value) { for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) { (*it)->removeCategoryInfo(category->name(), value); } } void XMLDB::Database::lockDB(bool lock, bool exclude) { DB::ImageSearchInfo info = Settings::SettingsData::instance()->currentLock(); for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) { if (lock) { bool match = info.match(*it); if (!exclude) match = !match; (*it)->setLocked(match); } else (*it)->setLocked(false); } } void XMLDB::Database::clearDelayedImages() { m_delayedCache.clear(); m_delayedUpdate.clear(); } void XMLDB::Database::forceUpdate(const DB::ImageInfoList &images) { // FIXME: merge stack information DB::ImageInfoList newImages = images.sort(); if (m_images.count() == 0) { // case 1: The existing imagelist is empty. Q_FOREACH (const DB::ImageInfoPtr &imageInfo, newImages) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); m_images = newImages; } else if (newImages.count() == 0) { // case 2: No images to merge in - that's easy ;-) return; } else if (newImages.first()->date().start() > m_images.last()->date().start()) { // case 2: The new list is later than the existsing Q_FOREACH (const DB::ImageInfoPtr &imageInfo, newImages) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); m_images.appendList(newImages); } else if (m_images.isSorted()) { // case 3: The lists overlaps, and the existsing list is sorted Q_FOREACH (const DB::ImageInfoPtr &imageInfo, newImages) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); m_images.mergeIn(newImages); } else { // case 4: The lists overlaps, and the existsing list is not sorted in the overlapping range. Q_FOREACH (const DB::ImageInfoPtr &imageInfo, newImages) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); m_images.appendList(newImages); } } void XMLDB::Database::addImages(const DB::ImageInfoList &images, bool doUpdate) { Q_FOREACH (const DB::ImageInfoPtr &info, images) { info->addCategoryInfo(i18n("Media Type"), info->mediaType() == DB::Image ? i18n("Image") : i18n("Video")); m_delayedCache.insert(info->fileName().absolute(), info); m_delayedUpdate << info; } if (doUpdate) { commitDelayedImages(); } } void XMLDB::Database::commitDelayedImages() { uint imagesAdded = m_delayedUpdate.count(); if (imagesAdded > 0) { forceUpdate(m_delayedUpdate); m_delayedCache.clear(); m_delayedUpdate.clear(); // It's the responsibility of the caller to add the Exif information. // It's more efficient from an I/O perspective to minimize the number // of passes over the images, and with the ability to add the Exif // data in a transaction, there's no longer any need to read it here. emit totalChanged(m_images.count()); emit dirty(); } } void XMLDB::Database::renameImage(DB::ImageInfoPtr info, const DB::FileName &newName) { info->setFileName(newName); } DB::ImageInfoPtr XMLDB::Database::info(const DB::FileName &fileName) const { if (fileName.isNull()) return DB::ImageInfoPtr(); const QString name = fileName.absolute(); if (m_imageCache.contains(name)) return m_imageCache[name]; if (m_delayedCache.contains(name)) return m_delayedCache[name]; Q_FOREACH (const DB::ImageInfoPtr &imageInfo, m_images) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); if (m_imageCache.contains(name)) { return m_imageCache[name]; } return DB::ImageInfoPtr(); } bool XMLDB::Database::rangeInclude(DB::ImageInfoPtr info) const { if (m_selectionRange.start().isNull()) return true; DB::ImageDate::MatchType tp = info->date().isIncludedIn(m_selectionRange); if (m_includeFuzzyCounts) return (tp == DB::ImageDate::ExactMatch || tp == DB::ImageDate::RangeMatch); else return (tp == DB::ImageDate::ExactMatch); } DB::MemberMap &XMLDB::Database::memberMap() { return m_members; } void XMLDB::Database::save(const QString &fileName, bool isAutoSave) { FileWriter saver(this); saver.save(fileName, isAutoSave); } DB::MD5Map *XMLDB::Database::md5Map() { return &m_md5map; } bool XMLDB::Database::isBlocking(const DB::FileName &fileName) { return m_blockList.contains(fileName); } DB::FileNameList XMLDB::Database::images() { return m_images.files(); } DB::FileNameList XMLDB::Database::search( const DB::ImageSearchInfo &info, bool requireOnDisk) const { return searchPrivate(info, requireOnDisk, true); } DB::FileNameList XMLDB::Database::searchPrivate( const DB::ImageSearchInfo &info, bool requireOnDisk, bool onlyItemsMatchingRange) const { // When searching for images counts for the datebar, we want matches outside the range too. // When searching for images for the thumbnail view, we only want matches inside the range. DB::FileNameList result; for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it) { bool match = !(*it)->isLocked() && info.match(*it) && (!onlyItemsMatchingRange || rangeInclude(*it)); match &= !requireOnDisk || DB::ImageInfo::imageOnDisk((*it)->fileName()); if (match) result.append((*it)->fileName()); } return result; } void XMLDB::Database::sortAndMergeBackIn(const DB::FileNameList &fileNameList) { DB::ImageInfoList infoList; Q_FOREACH (const DB::FileName &fileName, fileNameList) infoList.append(fileName.info()); m_images.sortAndMergeBackIn(infoList); } DB::CategoryCollection *XMLDB::Database::categoryCollection() { return &m_categoryCollection; } QExplicitlySharedDataPointer XMLDB::Database::rangeCollection() { return QExplicitlySharedDataPointer( new XMLImageDateCollection(searchPrivate(Browser::BrowserWidget::instance()->currentContext(), false, false))); } void XMLDB::Database::reorder( const DB::FileName &item, const DB::FileNameList &selection, bool after) { Q_ASSERT(!item.isNull()); DB::ImageInfoList list = takeImagesFromSelection(selection); insertList(item, list, after); } // Remove all the images from the database that match the given selection and // return that sublist. // This returns the selected and erased images in the order in which they appear // in the image list itself. DB::ImageInfoList XMLDB::Database::takeImagesFromSelection(const DB::FileNameList &selection) { DB::ImageInfoList result; if (selection.isEmpty()) return result; // iterate over all images (expensive!!) TODO: improve? for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); /**/) { const DB::FileName imagefile = (*it)->fileName(); DB::FileNameList::const_iterator si = selection.begin(); // for each image, iterate over selection, break on match for (/**/; si != selection.end(); ++si) { const DB::FileName file = *si; if (imagefile == file) { break; } } // if image is not in selection, simply advance to next, if not add to result and erase if (si == selection.end()) { ++it; } else { result << *it; m_imageCache.remove((*it)->fileName().absolute()); it = m_images.erase(it); } // if all images from selection are in result (size of lists is equal) break. if (result.size() == selection.size()) break; } return result; } void XMLDB::Database::insertList( const DB::FileName &fileName, const DB::ImageInfoList &list, bool after) { DB::ImageInfoListIterator imageIt = m_images.begin(); for (; imageIt != m_images.end(); ++imageIt) { if ((*imageIt)->fileName() == fileName) { break; } } // since insert() inserts before iterator increment when inserting AFTER image if (after) imageIt++; for (DB::ImageInfoListConstIterator it = list.begin(); it != list.end(); ++it) { // the call to insert() destroys the given iterator so use the new one after the call imageIt = m_images.insert(imageIt, *it); m_imageCache.insert((*it)->fileName().absolute(), *it); // increment always to retain order of selected images imageIt++; } emit dirty(); } bool XMLDB::Database::stack(const DB::FileNameList &items) { unsigned int changed = 0; QSet stacks; QList images; unsigned int stackOrder = 1; Q_FOREACH (const DB::FileName &fileName, items) { DB::ImageInfoPtr imgInfo = fileName.info(); Q_ASSERT(imgInfo); if (imgInfo->isStacked()) { stacks << imgInfo->stackId(); stackOrder = qMax(stackOrder, imgInfo->stackOrder() + 1); } else { images << imgInfo; } } if (stacks.size() > 1) return false; // images already in different stacks -> can't stack DB::StackID stackId = (stacks.size() == 1) ? *(stacks.begin()) : m_nextStackId++; Q_FOREACH (DB::ImageInfoPtr info, images) { info->setStackOrder(stackOrder); info->setStackId(stackId); m_stackMap[stackId].append(info->fileName()); ++changed; ++stackOrder; } if (changed) emit dirty(); return changed; } void XMLDB::Database::unstack(const DB::FileNameList &items) { Q_FOREACH (const DB::FileName &fileName, items) { DB::FileNameList allInStack = getStackFor(fileName); if (allInStack.size() <= 2) { // we're destroying stack here Q_FOREACH (const DB::FileName &stackFileName, allInStack) { DB::ImageInfoPtr imgInfo = stackFileName.info(); Q_ASSERT(imgInfo); if (imgInfo->isStacked()) { m_stackMap.remove(imgInfo->stackId()); imgInfo->setStackId(0); imgInfo->setStackOrder(0); } } } else { DB::ImageInfoPtr imgInfo = fileName.info(); Q_ASSERT(imgInfo); if (imgInfo->isStacked()) { m_stackMap[imgInfo->stackId()].removeAll(fileName); imgInfo->setStackId(0); imgInfo->setStackOrder(0); } } } if (!items.isEmpty()) emit dirty(); } DB::FileNameList XMLDB::Database::getStackFor(const DB::FileName &referenceImg) const { DB::ImageInfoPtr imageInfo = info(referenceImg); if (!imageInfo || !imageInfo->isStacked()) return DB::FileNameList(); StackMap::iterator found = m_stackMap.find(imageInfo->stackId()); if (found != m_stackMap.end()) return found.value(); // it wasn't in the cache -> rebuild it m_stackMap.clear(); for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it) { if ((*it)->isStacked()) { DB::StackID stackid = (*it)->stackId(); m_stackMap[stackid].append((*it)->fileName()); } } found = m_stackMap.find(imageInfo->stackId()); if (found != m_stackMap.end()) return found.value(); else return DB::FileNameList(); } void XMLDB::Database::copyData(const DB::FileName &from, const DB::FileName &to) { (*info(to)).merge(*info(from)); } int XMLDB::Database::fileVersion() { // File format version, bump it up every time the format for the file changes. return 8; } // During profiling of loading, I found that a significant amount of time was spent in QDateTime::fromString. // Reviewing the code, I fount that it did a lot of extra checks we don't need (like checking if the string have // timezone information (which they won't in KPA), this function is a replacement that is faster than the original. QDateTime dateTimeFromString(const QString &str) { static QChar T = QChar::fromLatin1('T'); if (str[10] == T) return QDateTime(QDate::fromString(str.left(10), Qt::ISODate), QTime::fromString(str.mid(11), Qt::ISODate)); else return QDateTime::fromString(str, Qt::ISODate); } DB::ImageInfoPtr XMLDB::Database::createImageInfo(const DB::FileName &fileName, ReaderPtr reader, Database *db, const QMap *newToOldCategory) { static QString _label_ = QString::fromUtf8("label"); static QString _description_ = QString::fromUtf8("description"); static QString _startDate_ = QString::fromUtf8("startDate"); static QString _endDate_ = QString::fromUtf8("endDate"); static QString _yearFrom_ = QString::fromUtf8("yearFrom"); static QString _monthFrom_ = QString::fromUtf8("monthFrom"); static QString _dayFrom_ = QString::fromUtf8("dayFrom"); static QString _hourFrom_ = QString::fromUtf8("hourFrom"); static QString _minuteFrom_ = QString::fromUtf8("minuteFrom"); static QString _secondFrom_ = QString::fromUtf8("secondFrom"); static QString _yearTo_ = QString::fromUtf8("yearTo"); static QString _monthTo_ = QString::fromUtf8("monthTo"); static QString _dayTo_ = QString::fromUtf8("dayTo"); static QString _angle_ = QString::fromUtf8("angle"); static QString _md5sum_ = QString::fromUtf8("md5sum"); static QString _width_ = QString::fromUtf8("width"); static QString _height_ = QString::fromUtf8("height"); static QString _rating_ = QString::fromUtf8("rating"); static QString _stackId_ = QString::fromUtf8("stackId"); static QString _stackOrder_ = QString::fromUtf8("stackOrder"); static QString _videoLength_ = QString::fromUtf8("videoLength"); static QString _options_ = QString::fromUtf8("options"); static QString _0_ = QString::fromUtf8("0"); static QString _minus1_ = QString::fromUtf8("-1"); static QString _MediaType_ = i18n("Media Type"); static QString _Image_ = i18n("Image"); static QString _Video_ = i18n("Video"); QString label; if (reader->hasAttribute(_label_)) label = reader->attribute(_label_); else label = QFileInfo(fileName.relative()).completeBaseName(); QString description; if (reader->hasAttribute(_description_)) description = reader->attribute(_description_); DB::ImageDate date; if (reader->hasAttribute(_startDate_)) { QDateTime start; QString str = reader->attribute(_startDate_); if (!str.isEmpty()) start = dateTimeFromString(str); str = reader->attribute(_endDate_); if (!str.isEmpty()) date = DB::ImageDate(start, dateTimeFromString(str)); else date = DB::ImageDate(start); } else { int yearFrom = 0, monthFrom = 0, dayFrom = 0, yearTo = 0, monthTo = 0, dayTo = 0, hourFrom = -1, minuteFrom = -1, secondFrom = -1; yearFrom = reader->attribute(_yearFrom_, _0_).toInt(); monthFrom = reader->attribute(_monthFrom_, _0_).toInt(); dayFrom = reader->attribute(_dayFrom_, _0_).toInt(); hourFrom = reader->attribute(_hourFrom_, _minus1_).toInt(); minuteFrom = reader->attribute(_minuteFrom_, _minus1_).toInt(); secondFrom = reader->attribute(_secondFrom_, _minus1_).toInt(); yearTo = reader->attribute(_yearTo_, _0_).toInt(); monthTo = reader->attribute(_monthTo_, _0_).toInt(); dayTo = reader->attribute(_dayTo_, _0_).toInt(); date = DB::ImageDate(yearFrom, monthFrom, dayFrom, yearTo, monthTo, dayTo, hourFrom, minuteFrom, secondFrom); } int angle = reader->attribute(_angle_, _0_).toInt(); DB::MD5 md5sum(reader->attribute(_md5sum_)); s_anyImageWithEmptySize |= !reader->hasAttribute(_width_); int w = reader->attribute(_width_, _minus1_).toInt(); int h = reader->attribute(_height_, _minus1_).toInt(); QSize size = QSize(w, h); DB::MediaType mediaType = Utilities::isVideo(fileName) ? DB::Video : DB::Image; short rating = reader->attribute(_rating_, _minus1_).toShort(); DB::StackID stackId = reader->attribute(_stackId_, _0_).toULong(); unsigned int stackOrder = reader->attribute(_stackOrder_, _0_).toULong(); DB::ImageInfo *info = new DB::ImageInfo(fileName, label, description, date, angle, md5sum, size, mediaType, rating, stackId, stackOrder); if (reader->hasAttribute(_videoLength_)) info->setVideoLength(reader->attribute(_videoLength_).toInt()); DB::ImageInfoPtr result(info); possibleLoadCompressedCategories(reader, result, db, newToOldCategory); while (reader->readNextStartOrStopElement(_options_).isStartToken) { readOptions(result, reader, newToOldCategory); } info->addCategoryInfo(_MediaType_, info->mediaType() == DB::Image ? _Image_ : _Video_); return result; } void XMLDB::Database::readOptions(DB::ImageInfoPtr info, ReaderPtr reader, const QMap *newToOldCategory) { static QString _name_ = QString::fromUtf8("name"); static QString _value_ = QString::fromUtf8("value"); static QString _option_ = QString::fromUtf8("option"); static QString _area_ = QString::fromUtf8("area"); while (reader->readNextStartOrStopElement(_option_).isStartToken) { QString name = FileReader::unescape(reader->attribute(_name_)); // If the silent update to db version 6 has been done, use the updated category names. if (newToOldCategory) { name = newToOldCategory->key(name, name); } if (!name.isNull()) { // Read values while (reader->readNextStartOrStopElement(_value_).isStartToken) { QString value = reader->attribute(_value_); if (reader->hasAttribute(_area_)) { QStringList areaData = reader->attribute(_area_).split(QString::fromUtf8(" ")); int x = areaData[0].toInt(); int y = areaData[1].toInt(); int w = areaData[2].toInt(); int h = areaData[3].toInt(); QRect area = QRect(QPoint(x, y), QPoint(x + w - 1, y + h - 1)); if (!value.isNull()) { info->addCategoryInfo(name, value, area); } } else { if (!value.isNull()) { info->addCategoryInfo(name, value); } } reader->readEndElement(); } } } } void XMLDB::Database::possibleLoadCompressedCategories(ReaderPtr reader, DB::ImageInfoPtr info, Database *db, const QMap *newToOldCategory) { if (db == nullptr) return; Q_FOREACH (const DB::CategoryPtr categoryPtr, db->m_categoryCollection.categories()) { QString categoryName = categoryPtr->name(); QString oldCategoryName; if (newToOldCategory) { // translate to old categoryName, defaulting to the original name if not found: oldCategoryName = newToOldCategory->value(categoryName, categoryName); } else { oldCategoryName = categoryName; } QString str = reader->attribute(FileWriter::escape(oldCategoryName)); if (!str.isEmpty()) { QStringList list = str.split(QString::fromLatin1(","), QString::SkipEmptyParts); Q_FOREACH (const QString &tagString, list) { int id = tagString.toInt(); - QString name = static_cast(categoryPtr.data())->nameForId(id); - info->addCategoryInfo(categoryName, name); + if (id != 0 || categoryPtr->isSpecialCategory()) { + const QString name = static_cast(categoryPtr.data())->nameForId(id); + info->addCategoryInfo(categoryName, name); + } else { + QStringList tags = static_cast(categoryPtr.data())->namesForId(id); + if (tags.size() == 1) { + qCInfo(XMLDBLog) << "Fixing tag " << categoryName << "/" << tags[0] << "with id=0 for image" << info->fileName().relative(); + } else { + // insert marker category + QString markerTag = i18n("KPhotoAlbum - manual repair needed (%1)", + tags.join(i18nc("Separator in a list of tags", ", "))); + categoryPtr->addItem(markerTag); + info->addCategoryInfo(categoryName, markerTag); + qCWarning(XMLDBLog) << "Manual fix required for image" << info->fileName().relative(); + qCWarning(XMLDBLog) << "Image was marked with tag " << categoryName << "/" << markerTag; + } + for (const auto &name : tags) { + info->addCategoryInfo(categoryName, name); + } + } } } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileReader.cpp b/XMLDB/FileReader.cpp index 33022f7f..03388dfb 100644 --- a/XMLDB/FileReader.cpp +++ b/XMLDB/FileReader.cpp @@ -1,548 +1,572 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Local includes #include "FileReader.h" #include "CompressFileInfo.h" #include "Database.h" #include "Logging.h" #include "XMLCategory.h" #include #include // KDE includes #include // Qt includes #include #include #include #include #include #include #include void XMLDB::FileReader::read(const QString &configFile) { static QString versionString = QString::fromUtf8("version"); static QString compressedString = QString::fromUtf8("compressed"); ReaderPtr reader = readConfigFile(configFile); ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KPhotoAlbum")); if (!info.isStartToken) reader->complainStartElementExpected(QString::fromUtf8("KPhotoAlbum")); m_fileVersion = reader->attribute(versionString, QString::fromLatin1("1")).toInt(); if (m_fileVersion > Database::fileVersion()) { DB::UserFeedback ret = m_db->uiDelegate().warningContinueCancel( QString::fromLatin1("index.xml version %1 is newer than %2!").arg(m_fileVersion).arg(Database::fileVersion()), i18n("

The database file (index.xml) is from a newer version of KPhotoAlbum!

" "

Chances are you will be able to read this file, but when writing it back, " "information saved in the newer version will be lost

"), i18n("index.xml version mismatch"), QString::fromLatin1("checkDatabaseFileVersion")); if (ret != DB::UserFeedback::Confirm) exit(-1); } setUseCompressedFileFormat(reader->attribute(compressedString).toInt()); m_db->m_members.setLoading(true); loadCategories(reader); loadImages(reader); loadBlockList(reader); loadMemberGroups(reader); //loadSettings(reader); + repairDB(); + m_db->m_members.setLoading(false); checkIfImagesAreSorted(); checkIfAllImagesHaveSizeAttributes(); } void XMLDB::FileReader::createSpecialCategories() { // Setup the "Folder" category m_folderCategory = new XMLCategory(i18n("Folder"), QString::fromLatin1("folder"), DB::Category::TreeView, 32, false); m_folderCategory->setType(DB::Category::FolderCategory); // The folder category is not stored in the index.xml file, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if (m_db->m_categoryCollection.categoryForName(m_folderCategory->name())) m_db->m_categoryCollection.removeCategory(m_folderCategory->name()); m_db->m_categoryCollection.addCategory(m_folderCategory); dynamic_cast(m_folderCategory.data())->setShouldSave(false); // Setup the "Tokens" category DB::CategoryPtr tokenCat; if (m_fileVersion >= 7) { tokenCat = m_db->m_categoryCollection.categoryForSpecial(DB::Category::TokensCategory); } else { // Before version 7, the "Tokens" category name wasn't stored to the settings. So ... // look for a literal "Tokens" category ... tokenCat = m_db->m_categoryCollection.categoryForName(QString::fromUtf8("Tokens")); if (!tokenCat) { // ... and a translated "Tokens" category if we don't have the literal one. tokenCat = m_db->m_categoryCollection.categoryForName(i18n("Tokens")); } if (tokenCat) { // in this case we need to give the tokens category its special meaning: m_db->m_categoryCollection.removeCategory(tokenCat->name()); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } } if (!tokenCat) { // Create a new "Tokens" category tokenCat = new XMLCategory(i18n("Tokens"), QString::fromUtf8("tag"), DB::Category::TreeView, 32, true); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } // KPhotoAlbum 2.2 did not write the tokens to the category section, // so unless we do this small trick they will not show up when importing. for (char ch = 'A'; ch <= 'Z'; ++ch) { tokenCat->addItem(QString::fromUtf8("%1").arg(QChar::fromLatin1(ch))); } // Setup the "Media Type" category DB::CategoryPtr mediaCat; mediaCat = new XMLCategory(i18n("Media Type"), QString::fromLatin1("view-categories"), DB::Category::TreeView, 32, false); mediaCat->addItem(i18n("Image")); mediaCat->addItem(i18n("Video")); mediaCat->setType(DB::Category::MediaTypeCategory); dynamic_cast(mediaCat.data())->setShouldSave(false); // The media type is not stored in the media category, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if (m_db->m_categoryCollection.categoryForName(mediaCat->name())) m_db->m_categoryCollection.removeCategory(mediaCat->name()); m_db->m_categoryCollection.addCategory(mediaCat); } void XMLDB::FileReader::loadCategories(ReaderPtr reader) { static QString nameString = QString::fromUtf8("name"); static QString iconString = QString::fromUtf8("icon"); static QString viewTypeString = QString::fromUtf8("viewtype"); static QString showString = QString::fromUtf8("show"); static QString thumbnailSizeString = QString::fromUtf8("thumbnailsize"); static QString positionableString = QString::fromUtf8("positionable"); static QString metaString = QString::fromUtf8("meta"); static QString tokensString = QString::fromUtf8("tokens"); static QString valueString = QString::fromUtf8("value"); static QString idString = QString::fromUtf8("id"); static QString birthDateString = QString::fromUtf8("birthDate"); static QString categoriesString = QString::fromUtf8("Categories"); static QString categoryString = QString::fromUtf8("Category"); ElementInfo info = reader->readNextStartOrStopElement(categoriesString); if (!info.isStartToken) reader->complainStartElementExpected(categoriesString); while (reader->readNextStartOrStopElement(categoryString).isStartToken) { const QString categoryName = unescape(reader->attribute(nameString)); if (!categoryName.isNull()) { // Read Category info QString icon = reader->attribute(iconString); DB::Category::ViewType type = (DB::Category::ViewType)reader->attribute(viewTypeString, QString::fromLatin1("0")).toInt(); int thumbnailSize = reader->attribute(thumbnailSizeString, QString::fromLatin1("32")).toInt(); bool show = (bool)reader->attribute(showString, QString::fromLatin1("1")).toInt(); bool positionable = (bool)reader->attribute(positionableString, QString::fromLatin1("0")).toInt(); bool tokensCat = reader->attribute(metaString) == tokensString; DB::CategoryPtr cat = m_db->m_categoryCollection.categoryForName(categoryName); bool repairMode = false; if (cat) { DB::UserFeedback choice = m_db->uiDelegate().warningContinueCancel( QString::fromUtf8("Line %1, column %2: duplicate category '%3'") .arg(reader->lineNumber()) .arg(reader->columnNumber()) .arg(categoryName), i18n("

Line %1, column %2: duplicate category '%3'

" "

Choose continue to ignore the duplicate category and try an automatic repair, " "or choose cancel to quit.

", reader->lineNumber(), reader->columnNumber(), categoryName), i18n("Error in database file")); if (choice == DB::UserFeedback::Confirm) repairMode = true; else exit(-1); } else { cat = new XMLCategory(categoryName, icon, type, thumbnailSize, show, positionable); if (tokensCat) cat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(cat); } // Read values QStringList items; while (reader->readNextStartOrStopElement(valueString).isStartToken) { QString value = reader->attribute(valueString); if (reader->hasAttribute(idString)) { int id = reader->attribute(idString).toInt(); - static_cast(cat.data())->setIdMapping(value, id); + if (id != 0) { + static_cast(cat.data())->setIdMapping(value, id); + } else { + qCWarning(XMLDBLog) << "Tag" << categoryName << "/" << value << "has id=0!"; + m_repairTagsWithNullIds = true; + static_cast(cat.data())->setIdMapping(value, id, XMLCategory::IdMapping::UnsafeMapping); + } } if (reader->hasAttribute(birthDateString)) cat->setBirthDate(value, QDate::fromString(reader->attribute(birthDateString), Qt::ISODate)); items.append(value); reader->readEndElement(); } if (repairMode) { // merge with duplicate category qCInfo(XMLDBLog) << "Repairing category " << categoryName << ": merging items " << cat->items() << " with " << items; items.append(cat->items()); items.removeDuplicates(); } cat->setItems(items); } } createSpecialCategories(); if (m_fileVersion < 7) { m_db->uiDelegate().information( QString::fromLatin1("Standard category names are no longer used since index.xml " "version 7. Standard categories will be left untranslated from now on."), i18nc("Leave \"Folder\" and \"Media Type\" untranslated below, those will show up with " "these exact names. Thanks :-)", "

This version of KPhotoAlbum does not translate \"standard\" categories " "any more.

" "

This may mean that – if you use a locale other than English – some of your " "categories are now displayed in English.

" "

You can manually rename your categories any time and then save your database." "

" "

In some cases, you may get two additional empty categories, \"Folder\" and " "\"Media Type\". You can delete those.

"), i18n("Changed standard category names")); } } void XMLDB::FileReader::loadImages(ReaderPtr reader) { static QString fileString = QString::fromUtf8("file"); static QString imagesString = QString::fromUtf8("images"); static QString imageString = QString::fromUtf8("image"); ElementInfo info = reader->readNextStartOrStopElement(imagesString); if (!info.isStartToken) reader->complainStartElementExpected(imagesString); while (reader->readNextStartOrStopElement(imageString).isStartToken) { const QString fileNameStr = reader->attribute(fileString); if (fileNameStr.isNull()) { qCWarning(XMLDBLog, "Element did not contain a file attribute"); return; } const DB::FileName dbFileName = DB::FileName::fromRelativePath(fileNameStr); DB::ImageInfoPtr info = load(dbFileName, reader); if (m_db->md5Map()->containsFile(dbFileName)) { if (m_db->md5Map()->contains(info->MD5Sum())) { qCWarning(XMLDBLog) << "Merging duplicate entry for file" << dbFileName.relative(); DB::ImageInfoPtr existingInfo = m_db->info(dbFileName); existingInfo->merge(*info); } else { m_db->uiDelegate().error( QString::fromUtf8("Conflicting information for file '%1': duplicate entry with different MD5 sum! Bailing out...") .arg(dbFileName.relative()), i18n("

Line %1, column %2: duplicate entry for file '%3' with different MD5 sum.

" "

Manual repair required!

", reader->lineNumber(), reader->columnNumber(), dbFileName.relative()), i18n("Error in database file")); exit(-1); } } else { m_db->m_images.append(info); m_db->m_md5map.insert(info->MD5Sum(), dbFileName); } } } void XMLDB::FileReader::loadBlockList(ReaderPtr reader) { static QString fileString = QString::fromUtf8("file"); static QString blockListString = QString::fromUtf8("blocklist"); static QString blockString = QString::fromUtf8("block"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == blockListString) { reader->readNextStartOrStopElement(blockListString); while (reader->readNextStartOrStopElement(blockString).isStartToken) { QString fileName = reader->attribute(fileString); if (!fileName.isEmpty()) m_db->m_blockList.insert(DB::FileName::fromRelativePath(fileName)); reader->readEndElement(); } } } void XMLDB::FileReader::loadMemberGroups(ReaderPtr reader) { static QString categoryString = QString::fromUtf8("category"); static QString groupNameString = QString::fromUtf8("group-name"); static QString memberString = QString::fromUtf8("member"); static QString membersString = QString::fromUtf8("members"); static QString memberGroupsString = QString::fromUtf8("member-groups"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == memberGroupsString) { reader->readNextStartOrStopElement(memberGroupsString); while (reader->readNextStartOrStopElement(memberString).isStartToken) { QString category = reader->attribute(categoryString); QString group = reader->attribute(groupNameString); if (reader->hasAttribute(memberString)) { QString member = reader->attribute(memberString); m_db->m_members.addMemberToGroup(category, group, member); } else { QStringList members = reader->attribute(membersString).split(QString::fromLatin1(","), QString::SkipEmptyParts); Q_FOREACH (const QString &memberItem, members) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName(category); if (!catPtr) { // category was not declared in "Categories" qCWarning(XMLDBLog) << "File corruption in index.xml. Inserting missing category: " << category; catPtr = new XMLCategory(category, QString::fromUtf8("dialog-warning"), DB::Category::TreeView, 32, false); m_db->m_categoryCollection.addCategory(catPtr); } XMLCategory *cat = static_cast(catPtr.data()); QString member = cat->nameForId(memberItem.toInt()); if (member.isNull()) continue; m_db->m_members.addMemberToGroup(category, group, member); } if (members.size() == 0) { // Groups are stored even if they are empty, so we also have to read them. // With no members, the above for loop will not be executed. m_db->m_members.addGroup(category, group); } } reader->readEndElement(); } } } /* void XMLDB::FileReader::loadSettings(ReaderPtr reader) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == settingsString) { reader->readNextStartOrStopElement(settingString); while(reader->readNextStartOrStopElement(settingString).isStartToken) { if (reader->hasAttribute(keyString) && reader->hasAttribute(valueString)) { m_db->m_settings.insert(unescape(reader->attribute(keyString)), unescape(reader->attribute(valueString))); } else { qWarning() << "File corruption in index.xml. Setting either lacking a key or a " << "value attribute. Ignoring this entry."; } reader->readEndElement(); } } } */ void XMLDB::FileReader::checkIfImagesAreSorted() { if (m_db->uiDelegate().isDialogDisabled(QString::fromLatin1("checkWhetherImagesAreSorted"))) return; QDateTime last(QDate(1900, 1, 1)); bool wrongOrder = false; for (DB::ImageInfoListIterator it = m_db->m_images.begin(); !wrongOrder && it != m_db->m_images.end(); ++it) { if (last > (*it)->date().start() && (*it)->date().start().isValid()) wrongOrder = true; last = (*it)->date().start(); } if (wrongOrder) { m_db->uiDelegate().information( QString::fromLatin1("Database is not sorted by date."), i18n("

Your images/videos are not sorted, which means that navigating using the date bar " "will only work suboptimally.

" "

In the Maintenance menu, you can find Display Images with Incomplete Dates " "which you can use to find the images that are missing date information.

" "

You can then select the images that you have reason to believe have a correct date " "in either their Exif data or on the file, and execute Maintenance->Read Exif Info " "to reread the information.

" "

Finally, once all images have their dates set, you can execute " "Maintenance->Sort All by Date & Time to sort them in the database.

"), i18n("Images/Videos Are Not Sorted"), QString::fromLatin1("checkWhetherImagesAreSorted")); } } void XMLDB::FileReader::checkIfAllImagesHaveSizeAttributes() { QTime time; time.start(); if (m_db->uiDelegate().isDialogDisabled(QString::fromLatin1("checkWhetherAllImagesIncludesSize"))) return; if (m_db->s_anyImageWithEmptySize) { m_db->uiDelegate().information( QString::fromLatin1("Found image(s) without size information."), i18n("

Not all the images in the database have information about image sizes; this is needed to " "get the best result in the thumbnail view. To fix this, simply go to the Maintenance menu, " "and first choose Remove All Thumbnails, and after that choose Build Thumbnails.

" "

Not doing so will result in extra space around images in the thumbnail view - that is all - so " "there is no urgency in doing it.

"), i18n("Not All Images Have Size Information"), QString::fromLatin1("checkWhetherAllImagesIncludesSize")); } } +void XMLDB::FileReader::repairDB() +{ + if (m_repairTagsWithNullIds) { + // the m_repairTagsWithNullIds is set in loadCategories() + // -> care is taken so that multiple tags with id=0 all end up in the IdMap + // afterwards, loadImages() applies fixes to the affected images + // -> this happens in XMLDB::Database::possibleLoadCompressedCategories() + // i.e. the zero ids still require cleanup: + qCInfo(XMLDBLog) << "Database contained tags with id=0 (possibly related to bug #415415). Assigning new ids for affected categories..."; + for (auto category : m_db->categoryCollection()->categories()) { + XMLCategory *xmlCategory = static_cast(category.data()); + xmlCategory->clearNullIds(); + } + } +} + DB::ImageInfoPtr XMLDB::FileReader::load(const DB::FileName &fileName, ReaderPtr reader) { DB::ImageInfoPtr info = XMLDB::Database::createImageInfo(fileName, reader, m_db); m_nextStackId = qMax(m_nextStackId, info->stackId() + 1); info->createFolderCategoryItem(m_folderCategory, m_db->m_members); return info; } XMLDB::ReaderPtr XMLDB::FileReader::readConfigFile(const QString &configFile) { ReaderPtr reader = ReaderPtr(new XmlReader(m_db->uiDelegate(), configFile)); QFile file(configFile); if (!file.exists()) { // Load a default setup QFile file(QStandardPaths::locate(QStandardPaths::DataLocation, QString::fromLatin1("default-setup"))); if (!file.open(QIODevice::ReadOnly)) { m_db->uiDelegate().information( QString::fromLatin1("default-setup not found in standard paths."), i18n("

KPhotoAlbum was unable to load a default setup, which indicates an installation error

" "

If you have installed KPhotoAlbum yourself, then you must remember to set the environment variable " "KDEDIRS, to point to the topmost installation directory.

" "

If you for example ran cmake with -DCMAKE_INSTALL_PREFIX=/usr/local/kde, then you must use the following " "environment variable setup (this example is for Bash and compatible shells):

" "

export KDEDIRS=/usr/local/kde

" "

In case you already have KDEDIRS set, simply append the string as if you where setting the PATH " "environment variable

"), i18n("No default setup file found")); } else { QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); QString str = stream.readAll(); // Replace the default setup's category and tag names with localized ones str = str.replace(QString::fromUtf8("People"), i18n("People")); str = str.replace(QString::fromUtf8("Places"), i18n("Places")); str = str.replace(QString::fromUtf8("Events"), i18n("Events")); str = str.replace(QString::fromUtf8("untagged"), i18n("untagged")); str = str.replace(QRegExp(QString::fromLatin1("imageDirectory=\"[^\"]*\"")), QString::fromLatin1("")); str = str.replace(QRegExp(QString::fromLatin1("htmlBaseDir=\"[^\"]*\"")), QString::fromLatin1("")); str = str.replace(QRegExp(QString::fromLatin1("htmlBaseURL=\"[^\"]*\"")), QString::fromLatin1("")); reader->addData(str); } } else { if (!file.open(QIODevice::ReadOnly)) { m_db->uiDelegate().error( QString::fromLatin1("Unable to open '%1' for reading").arg(configFile), i18n("Unable to open '%1' for reading", configFile), i18n("Error Running Demo")); exit(-1); } reader->addData(file.readAll()); #if 0 QString errMsg; int errLine; int errCol; if ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol )) { file.close(); // If parsing index.xml fails let's see if we could use a backup instead Utilities::checkForBackupFile( configFile, i18n( "line %1 column %2 in file %3: %4", errLine , errCol , configFile , errMsg ) ); if ( !file.open( QIODevice::ReadOnly ) || ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol ) ) ) { KMessageBox::error( messageParent(), i18n( "Failed to recover the backup: %1", errMsg ) ); exit(-1); } } #endif } // Now read the content of the file. #if 0 QDomElement top = doc.documentElement(); if ( top.isNull() ) { KMessageBox::error( messageParent(), i18n("Error in file %1: No elements found", configFile ) ); exit(-1); } if ( top.tagName().toLower() != QString::fromLatin1( "kphotoalbum" ) && top.tagName().toLower() != QString::fromLatin1( "kimdaba" ) ) { // KimDaBa compatibility KMessageBox::error( messageParent(), i18n("Error in file %1: expected 'KPhotoAlbum' as top element but found '%2'", configFile , top.tagName() ) ); exit(-1); } #endif file.close(); return reader; } /** * @brief Unescape a string used as an XML attribute name. * * @see XMLDB::FileWriter::escape * * @param str the string to be unescaped * @return the unescaped string */ QString XMLDB::FileReader::unescape(const QString &str) { static bool hashUsesCompressedFormat = useCompressedFileFormat(); static QHash s_cache; if (hashUsesCompressedFormat != useCompressedFileFormat()) s_cache.clear(); if (s_cache.contains(str)) return s_cache[str]; QString tmp(str); // Matches encoded characters in attribute names QRegExp rx(QString::fromLatin1("(_.)([0-9A-F]{2})")); int pos = 0; // Unencoding special characters if compressed XML is selected if (useCompressedFileFormat()) { while ((pos = rx.indexIn(tmp, pos)) != -1) { QString before = rx.cap(1) + rx.cap(2); QString after = QString::fromLatin1(QByteArray::fromHex(rx.cap(2).toLocal8Bit())); tmp.replace(pos, before.length(), after); pos += after.length(); } } else tmp.replace(QString::fromLatin1("_"), QString::fromLatin1(" ")); s_cache.insert(str, tmp); return tmp; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileReader.h b/XMLDB/FileReader.h index e5675a78..ed8a5eb6 100644 --- a/XMLDB/FileReader.h +++ b/XMLDB/FileReader.h @@ -1,75 +1,88 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef XMLDB_FILEREADER_H #define XMLDB_FILEREADER_H #include "XmlReader.h" #include #include #include class QXmlStreamReader; namespace XMLDB { class Database; class FileReader { public: FileReader(Database *db) : m_db(db) , m_nextStackId(1) { } void read(const QString &configFile); static QString unescape(const QString &); DB::StackID nextStackId() const { return m_nextStackId; } protected: void loadCategories(ReaderPtr reader); void loadImages(ReaderPtr reader); void loadBlockList(ReaderPtr reader); void loadMemberGroups(ReaderPtr reader); //void loadSettings(ReaderPtr reader); DB::ImageInfoPtr load(const DB::FileName &filename, ReaderPtr reader); ReaderPtr readConfigFile(const QString &configFile); void createSpecialCategories(); void checkIfImagesAreSorted(); void checkIfAllImagesHaveSizeAttributes(); + /** + * @brief Repair the database if an issue was flagged for repair. + * DB repairs that only require local knowledge are usually done in the respective + * load* method. This method is called after the database file was loaded. + * + * Currently, this tries to fix the following issues: + * + * - Bug #415415 - Renaming tag groups can produce tags with id=0 + */ + void repairDB(); + private: Database *const m_db; int m_fileVersion; DB::StackID m_nextStackId; // During profilation I found that it was rather expensive to look this up over and over again (once for each image) DB::CategoryPtr m_folderCategory; + /// Flag indicating that repair is necessary + bool m_repairTagsWithNullIds = false; }; } #endif /* XMLDB_FILEREADER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLCategory.cpp b/XMLDB/XMLCategory.cpp index 1ddc00f3..64bb12ed 100644 --- a/XMLDB/XMLCategory.cpp +++ b/XMLDB/XMLCategory.cpp @@ -1,228 +1,248 @@ -/* Copyright (C) 2003-2018 Jesper K. Pedersen +/* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "XMLCategory.h" +#include "Logging.h" #include #include #include -#include "Logging.h" XMLDB::XMLCategory::XMLCategory(const QString &name, const QString &icon, ViewType type, int thumbnailSize, bool show, bool positionable) : m_name(name) , m_icon(icon) , m_show(show) , m_type(type) , m_thumbnailSize(thumbnailSize) , m_positionable(positionable) , m_categoryType(DB::Category::PlainCategory) , m_shouldSave(true) { } QString XMLDB::XMLCategory::name() const { return m_name; } void XMLDB::XMLCategory::setName(const QString &name) { m_name = name; } void XMLDB::XMLCategory::setPositionable(bool positionable) { if (positionable != m_positionable) { m_positionable = positionable; emit changed(); } } bool XMLDB::XMLCategory::positionable() const { return m_positionable; } QString XMLDB::XMLCategory::iconName() const { return m_icon; } void XMLDB::XMLCategory::setIconName(const QString &name) { m_icon = name; emit changed(); } void XMLDB::XMLCategory::setViewType(ViewType type) { m_type = type; emit changed(); } XMLDB::XMLCategory::ViewType XMLDB::XMLCategory::viewType() const { return m_type; } void XMLDB::XMLCategory::setDoShow(bool b) { m_show = b; emit changed(); } bool XMLDB::XMLCategory::doShow() const { return m_show; } void XMLDB::XMLCategory::setType(DB::Category::CategoryType t) { m_categoryType = t; } DB::Category::CategoryType XMLDB::XMLCategory::type() const { return m_categoryType; } bool XMLDB::XMLCategory::isSpecialCategory() const { return m_categoryType != DB::Category::PlainCategory; } void XMLDB::XMLCategory::addOrReorderItems(const QStringList &items) { m_items = Utilities::mergeListsUniqly(items, m_items); } void XMLDB::XMLCategory::setItems(const QStringList &items) { m_items = items; } void XMLDB::XMLCategory::removeItem(const QString &item) { m_items.removeAll(item); m_nameMap.remove(idForName(item)); m_idMap.remove(item); emit itemRemoved(item); } void XMLDB::XMLCategory::renameItem(const QString &oldValue, const QString &newValue) { int id = idForName(oldValue); m_items.removeAll(oldValue); m_nameMap.remove(id); m_idMap.remove(oldValue); addItem(newValue); - if ( id > 0) + if (id > 0) setIdMapping(newValue, id); emit itemRenamed(oldValue, newValue); } void XMLDB::XMLCategory::addItem(const QString &item) { // for the "SortLastUsed" functionality in ListSelect we remove the item and insert it again: if (m_items.contains(item)) m_items.removeAll(item); m_items.prepend(item); } QStringList XMLDB::XMLCategory::items() const { return m_items; } int XMLDB::XMLCategory::idForName(const QString &name) const { + Q_ASSERT(m_idMap.count(name) <= 1); return m_idMap[name]; } /** * @brief Make sure that the id/name mapping is a full mapping. */ void XMLDB::XMLCategory::initIdMap() { // find maximum id // obviously, this will leave gaps in numbering when tags are deleted // assuming that tags are seldomly removed this should not be a problem int i = 0; if (!m_nameMap.empty()) { i = m_nameMap.lastKey(); } Q_FOREACH (const QString &tag, m_items) { if (!m_idMap.contains(tag)) setIdMapping(tag, ++i); } const QStringList groups = DB::ImageDB::instance()->memberMap().groups(m_name); Q_FOREACH (const QString &group, groups) { if (!m_idMap.contains(group)) setIdMapping(group, ++i); } } -void XMLDB::XMLCategory::setIdMapping(const QString &name, int id) +void XMLDB::XMLCategory::setIdMapping(const QString &name, int id, IdMapping mode) { if (id <= 0) { - qCWarning(XMLDBLog, "XMLDB::XMLCategory::setIdMapping attempting to set id for %s to invalid value %d", qPrintable(name), id); + if (mode == IdMapping::SafeMapping) { + qCWarning(XMLDBLog, "XMLDB::XMLCategory::setIdMapping attempting to set id for %s to invalid value %d", qPrintable(name), id); + } else { + m_nameMap.insertMulti(id, name); + m_idMap.insertMulti(name, id); + } } else { m_nameMap.insert(id, name); m_idMap.insert(name, id); } } QString XMLDB::XMLCategory::nameForId(int id) const { + Q_ASSERT(m_nameMap.count(id) <= 1); return m_nameMap[id]; } +QStringList XMLDB::XMLCategory::namesForId(int id) const +{ + return m_nameMap.values(id); +} + +void XMLDB::XMLCategory::clearNullIds() +{ + for (const auto &tag : namesForId(0)) { + m_idMap.remove(tag); + } + m_nameMap.remove(0); +} + void XMLDB::XMLCategory::setThumbnailSize(int size) { m_thumbnailSize = size; emit changed(); } int XMLDB::XMLCategory::thumbnailSize() const { return m_thumbnailSize; } bool XMLDB::XMLCategory::shouldSave() { return m_shouldSave; } void XMLDB::XMLCategory::setShouldSave(bool b) { m_shouldSave = b; } void XMLDB::XMLCategory::setBirthDate(const QString &item, const QDate &birthDate) { m_birthDates.insert(item, birthDate); } QDate XMLDB::XMLCategory::birthDate(const QString &item) const { return m_birthDates[item]; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLCategory.h b/XMLDB/XMLCategory.h index 373f14b8..f8b455d0 100644 --- a/XMLDB/XMLCategory.h +++ b/XMLDB/XMLCategory.h @@ -1,92 +1,110 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef XMLCATEGORY_H #define XMLCATEGORY_H #include #include #include namespace XMLDB { class XMLCategory : public DB::Category { Q_OBJECT public: XMLCategory(const QString &name, const QString &icon, ViewType type, int thumbnailSize, bool show, bool positionable = false); QString name() const override; void setName(const QString &name) override; void setPositionable(bool) override; bool positionable() const override; QString iconName() const override; void setIconName(const QString &name) override; void setViewType(ViewType type) override; ViewType viewType() const override; void setThumbnailSize(int) override; int thumbnailSize() const override; void setDoShow(bool b) override; bool doShow() const override; void setType(DB::Category::CategoryType t) override; CategoryType type() const override; bool isSpecialCategory() const override; void addOrReorderItems(const QStringList &items) override; void setItems(const QStringList &items) override; void removeItem(const QString &item) override; void renameItem(const QString &oldValue, const QString &newValue) override; void addItem(const QString &item) override; QStringList items() const override; int idForName(const QString &name) const; void initIdMap(); - void setIdMapping(const QString &name, int id); + enum class IdMapping { SafeMapping, + UnsafeMapping }; + void setIdMapping(const QString &name, int id, IdMapping mode = IdMapping::SafeMapping); QString nameForId(int id) const; + /** + * @brief namesForId returns multiple names for an id. + * Obviously, this is not how ids usually work. + * Multiple names for the same id can be forced by using the IdMapping::UnsafeMapping parameter + * when calling setIdMapping(). + * The only place where this makes sense is when reading a damaged index.xml file that is to be repaired. + * After loading the database is complete, the mapping between id and name is always 1:1! + * @param id + * @return + */ + QStringList namesForId(int id) const; + /** + * @brief clearNullIds clears the IdMapping for tags with id=0. + * This can only happen when loading a corrupted database file. + */ + void clearNullIds(); bool shouldSave(); void setShouldSave(bool b); void setBirthDate(const QString &item, const QDate &birthDate) override; QDate birthDate(const QString &item) const override; private: QString m_name; QString m_icon; bool m_show; ViewType m_type; int m_thumbnailSize; bool m_positionable; CategoryType m_categoryType; QStringList m_items; QMap m_idMap; QMap m_nameMap; QMap m_birthDates; bool m_shouldSave; }; } #endif /* XMLCATEGORY_H */ // vi:expandtab:tabstop=4 shiftwidth=4: