File indexing completed on 2025-01-19 03:53:36

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2007-09-19
0007  * Description : Access to comments of an item in the database
0008  *
0009  * SPDX-FileCopyrightText: 2007-2013 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0010  * SPDX-FileCopyrightText: 2009-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "itemcomments.h"
0017 
0018 // Qt includes
0019 
0020 #include <QLocale>
0021 
0022 // Local includes
0023 
0024 #include "coredb.h"
0025 
0026 namespace Digikam
0027 {
0028 
0029 class Q_DECL_HIDDEN ItemComments::Private : public QSharedData
0030 {
0031 public:
0032 
0033     explicit Private()
0034       : id    (-1),
0035         unique(ItemComments::UniquePerLanguage)
0036     {
0037     }
0038 
0039     void init(const CoreDbAccess& access, qlonglong imageId)
0040     {
0041         id    = imageId;
0042         infos = access.db()->getItemComments(id);
0043 
0044         for (int i = 0 ; i < infos.size() ; ++i)
0045         {
0046             CommentInfo& info = infos[i];
0047 
0048             if (info.language.isNull())
0049             {
0050                 info.language = QLatin1String("x-default");
0051             }
0052         }
0053     }
0054 
0055     void languageMatch(const QString& fullCode,
0056                        const QString& langCode,
0057                        int& fullCodeMatch,
0058                        int& langCodeMatch,
0059                        int& defaultCodeMatch,
0060                        int& firstMatch,
0061                        DatabaseComment::Type type = DatabaseComment::Comment) const
0062     {
0063         // if you change the algorithm, please take a look at ItemCopyright as well
0064 
0065         fullCodeMatch    = -1;
0066         langCodeMatch    = -1;
0067         defaultCodeMatch = -1;
0068         firstMatch       = -1;
0069 
0070         if (infos.isEmpty())
0071         {
0072             return;
0073         }
0074 
0075         // First we search for a full match
0076         // Second for a match of the language code
0077         // Third for the default code
0078         // Fourth we return the first comment
0079 
0080         QLatin1String defaultCode("x-default");
0081 
0082         for (int i = 0 ; i < infos.size() ; ++i)
0083         {
0084             const CommentInfo& info = infos.at(i);
0085 
0086             if (info.type == type)
0087             {
0088                 if (firstMatch == -1)
0089                 {
0090                     firstMatch = i;
0091                 }
0092 
0093                 if      (info.language == fullCode)
0094                 {
0095                     fullCodeMatch = i;
0096                     break;
0097                 }
0098                 else if (info.language.startsWith(langCode) && langCodeMatch == -1)
0099                 {
0100                     langCodeMatch = i;
0101                 }
0102                 else if (info.language == defaultCode)
0103                 {
0104                     defaultCodeMatch = i;
0105                 }
0106             }
0107         }
0108     }
0109 
0110     void adjustStoredIndexes(QSet<int>& set, int removedIndex)
0111     {
0112         QSet<int> newSet;
0113 
0114         Q_FOREACH (int index, set)
0115         {
0116             if      (index > removedIndex)
0117             {
0118                 newSet << index - 1;
0119             }
0120             else if (index < removedIndex)
0121             {
0122                 newSet << index;
0123             }
0124 
0125             // drop index == removedIndex
0126         }
0127 
0128         set = newSet;
0129     }
0130 
0131     void adjustStoredIndexes(int removedIndex)
0132     {
0133         adjustStoredIndexes(dirtyIndices, removedIndex);
0134         adjustStoredIndexes(newIndices,   removedIndex);
0135     }
0136 
0137 public:
0138 
0139     qlonglong                     id;
0140     QList<CommentInfo>            infos;
0141     QSet<int>                     dirtyIndices;
0142     QSet<int>                     newIndices;
0143     QSet<int>                     idsToRemove;
0144     ItemComments::UniqueBehavior unique;
0145 };
0146 
0147 ItemComments::ItemComments()
0148     : d(nullptr)
0149 {
0150 }
0151 
0152 ItemComments::ItemComments(qlonglong imageid)
0153     : d(new Private)
0154 {
0155     CoreDbAccess access;
0156     d->init(access, imageid);
0157 }
0158 
0159 ItemComments::ItemComments(const CoreDbAccess& access, qlonglong imageid)
0160     : d(new Private)
0161 {
0162     d->init(access, imageid);
0163 }
0164 
0165 ItemComments::ItemComments(const ItemComments& other)
0166     : d(other.d)
0167 {
0168 }
0169 
0170 ItemComments::~ItemComments()
0171 {
0172     apply();
0173 }
0174 
0175 ItemComments& ItemComments::operator=(const ItemComments& other)
0176 {
0177     d = other.d;
0178 
0179     return *this;
0180 }
0181 
0182 bool ItemComments::isNull() const
0183 {
0184     return !d;
0185 }
0186 
0187 QString ItemComments::defaultComment(DatabaseComment::Type type) const
0188 {
0189     return defaultComment(nullptr, type);
0190 }
0191 
0192 QString ItemComments::defaultComment(int* const index, DatabaseComment::Type type) const
0193 {
0194     if (!d)
0195     {
0196         return QString();
0197     }
0198 
0199     QString spec     = QLocale().name().toLower();
0200     QString langCode = spec.left(spec.indexOf(QLatin1Char('_'))) + QLatin1Char('-');
0201     QString fullCode = spec.replace(QLatin1Char('_'), QLatin1Char('-'));
0202 
0203     int fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch;
0204 
0205     d->languageMatch(fullCode, langCode, fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch, type);
0206 
0207     int chosen = fullCodeMatch;
0208 
0209     if (chosen == -1)
0210     {
0211         chosen = langCodeMatch;
0212     }
0213 
0214     if (chosen == -1)
0215     {
0216         chosen = defaultCodeMatch;
0217     }
0218 
0219     if (chosen == -1)
0220     {
0221         chosen = firstMatch;
0222     }
0223 
0224     if (index)
0225     {
0226         *index = chosen;
0227     }
0228 
0229     if (chosen == -1)
0230     {
0231         return QString();
0232     }
0233     else
0234     {
0235         return d->infos.at(chosen).comment;
0236     }
0237 }
0238 
0239 QString ItemComments::commentForLanguage(const QString& languageCode,
0240                                          int* const index,
0241                                          LanguageChoiceBehavior behavior) const
0242 {
0243     if (!d)
0244     {
0245         return QString();
0246     }
0247 
0248     int fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch;
0249 
0250     // en-us => en-
0251 
0252     QString firstPart;
0253 
0254     if (languageCode == QLatin1String("x-default"))
0255     {
0256         firstPart = languageCode;
0257     }
0258     else
0259     {
0260         firstPart = languageCode.section(QLatin1Char('-'), 0, 0, QString::SectionIncludeTrailingSep);
0261     }
0262 
0263     d->languageMatch(languageCode, firstPart, fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch);
0264 
0265     int chosen = fullCodeMatch;
0266 
0267     if (chosen == -1)
0268     {
0269         chosen = langCodeMatch;
0270     }
0271 
0272     if ((chosen == -1) && (behavior > ReturnMatchingLanguageOnly))
0273     {
0274         chosen = defaultCodeMatch;
0275 
0276         if ((chosen == -1) && (behavior == ReturnMatchingDefaultOrFirstLanguage))
0277         {
0278             chosen = firstMatch;
0279         }
0280     }
0281 
0282     if (index)
0283     {
0284         *index = chosen;
0285     }
0286 
0287     if (chosen == -1)
0288     {
0289         return QString();
0290     }
0291     else
0292     {
0293         return d->infos.at(chosen).comment;
0294     }
0295 }
0296 
0297 int ItemComments::numberOfComments() const
0298 {
0299     if (!d)
0300     {
0301         return 0;
0302     }
0303 
0304     return d->infos.size();
0305 }
0306 
0307 DatabaseComment::Type ItemComments::type(int index) const
0308 {
0309     if (!d)
0310     {
0311         return DatabaseComment::UndefinedType;
0312     }
0313 
0314     return d->infos.at(index).type;
0315 }
0316 
0317 QString ItemComments::language(int index) const
0318 {
0319     if (!d)
0320     {
0321         return QString();
0322     }
0323 
0324     return d->infos.at(index).language;
0325 }
0326 
0327 QString ItemComments::author(int index) const
0328 {
0329     if (!d)
0330     {
0331         return QString();
0332     }
0333 
0334     return d->infos.at(index).author;
0335 }
0336 
0337 QDateTime ItemComments::date(int index) const
0338 {
0339     if (!d)
0340     {
0341         return QDateTime();
0342     }
0343 
0344     return d->infos.at(index).date;
0345 }
0346 
0347 QString ItemComments::comment(int index) const
0348 {
0349     if (!d)
0350     {
0351         return QString();
0352     }
0353 
0354     return d->infos.at(index).comment;
0355 }
0356 
0357 void ItemComments::setUniqueBehavior(UniqueBehavior behavior)
0358 {
0359     if (!d)
0360     {
0361         return;
0362     }
0363 
0364     d->unique = behavior;
0365 }
0366 
0367 void ItemComments::addComment(const QString& comment,
0368                               const QString& lang,
0369                               const QString& author_,
0370                               const QDateTime& date,
0371                               DatabaseComment::Type type)
0372 {
0373     if (!d)
0374     {
0375         return;
0376     }
0377 
0378     bool multipleCommentsPerLanguage = (d->unique == UniquePerLanguageAndAuthor);
0379     QString language                 = lang;
0380 
0381     if (language.isEmpty())
0382     {
0383         language = QLatin1String("x-default");
0384     }
0385 
0386     QString author = author_;
0387 
0388     /// @todo This makes no sense - is another variable supposed to be used instead? - Michael Hansen
0389 
0390     if (author.isEmpty())
0391     {
0392         author = QString();
0393     }
0394 
0395     for (int i = 0 ; i < d->infos.size() ; ++i)
0396     {
0397         CommentInfo& info = d->infos[i];
0398 
0399         // some extra considerations on replacing
0400 
0401         if ((info.type == type) && (info.type == DatabaseComment::Comment) && (info.language == language))
0402         {
0403             if (!multipleCommentsPerLanguage || (info.author == author))
0404             {
0405                 info.comment = comment;
0406                 info.date    = date;
0407                 info.author  = author;
0408                 d->dirtyIndices << i;
0409                 return;
0410             }
0411         }
0412 
0413         // simulate unique restrictions of db.
0414         // There is a problem that a NULL value is never unique, see #189080
0415 
0416         if ((info.type == type)         &&
0417             (info.language == language) &&
0418             ((info.author == author) || (info.author.isEmpty() && author.isEmpty())))
0419         {
0420             info.comment = comment;
0421             info.date    = date;
0422             d->dirtyIndices << i;
0423             return;
0424         }
0425     }
0426 
0427     addCommentDirectly(comment, language, author, type, date);
0428 }
0429 
0430 void ItemComments::addHeadline(const QString& headline, const QString& lang,
0431                                const QString& author, const QDateTime& date)
0432 {
0433     addComment(headline, lang, author, date, DatabaseComment::Headline);
0434 }
0435 
0436 void ItemComments::addTitle(const QString& title, const QString& lang,
0437                             const QString& author, const QDateTime& date)
0438 {
0439     addComment(title, lang, author, date, DatabaseComment::Title);
0440 }
0441 
0442 void ItemComments::replaceComments(const CaptionsMap& map, DatabaseComment::Type type)
0443 {
0444     if (!d)
0445     {
0446         return;
0447     }
0448 
0449     d->dirtyIndices.clear();
0450 
0451     for (CaptionsMap::const_iterator it = map.constBegin() ; it != map.constEnd() ; ++it)
0452     {
0453         CaptionValues val = it.value();
0454         addComment(val.caption, it.key(), val.author, val.date, type);
0455     }
0456 
0457     // remove all comments of this type that have not been touched above
0458 
0459     for (int i = 0 ; i < d->infos.size() /* changing! */ ; )
0460     {
0461         if (!d->dirtyIndices.contains(i) && !d->newIndices.contains(i) && (d->infos[i].type == type))
0462         {
0463             remove(i);
0464         }
0465         else
0466         {
0467             ++i;
0468         }
0469     }
0470 }
0471 
0472 void ItemComments::replaceFrom(const ItemComments& source)
0473 {
0474     if (!d)
0475     {
0476         return;
0477     }
0478 
0479     if (!source.d)
0480     {
0481         removeAll();
0482         return;
0483     }
0484 
0485     Q_FOREACH (const CommentInfo& info, source.d->infos)
0486     {
0487         addComment(info.comment, info.language, info.author, info.date, info.type);
0488     }
0489 
0490     // remove all that have not been touched above
0491 
0492     for (int i = 0 ; i < d->infos.size() /* changing! */ ; )
0493     {
0494         if (!d->dirtyIndices.contains(i) && !d->newIndices.contains(i))
0495         {
0496             remove(i);
0497         }
0498         else
0499         {
0500             ++i;
0501         }
0502     }
0503 }
0504 
0505 void ItemComments::addCommentDirectly(const QString& comment,
0506                                       const QString& language,
0507                                       const QString& author,
0508                                       DatabaseComment::Type type,
0509                                       const QDateTime& date)
0510 {
0511     CommentInfo info;
0512     info.comment  = comment;
0513     info.language = language;
0514     info.author   = author;
0515     info.type     = type;
0516     info.date     = date;
0517 
0518     d->newIndices << d->infos.size();
0519     d->infos      << info;
0520 }
0521 
0522 void ItemComments::remove(int index)
0523 {
0524     if (!d)
0525     {
0526         return;
0527     }
0528 
0529     d->idsToRemove << d->infos.at(index).id;
0530     d->infos.removeAt(index);
0531     d->adjustStoredIndexes(index);
0532 }
0533 
0534 void ItemComments::removeAll(DatabaseComment::Type type)
0535 {
0536     if (!d)
0537     {
0538         return;
0539     }
0540 
0541     for (int i = 0 ; i < d->infos.size() /* changing! */ ; )
0542     {
0543         if (d->infos.at(i).type == type)
0544         {
0545             remove(i);
0546         }
0547         else
0548         {
0549             ++i;
0550         }
0551     }
0552 }
0553 
0554 void ItemComments::removeAllComments()
0555 {
0556     removeAll(DatabaseComment::Comment);
0557 }
0558 
0559 void ItemComments::removeAll()
0560 {
0561     if (!d)
0562     {
0563         return;
0564     }
0565 
0566     Q_FOREACH (const CommentInfo& info, d->infos)
0567     {
0568         d->idsToRemove << info.id;
0569     }
0570 
0571     d->infos.clear();
0572     d->dirtyIndices.clear();
0573     d->newIndices.clear();
0574 }
0575 
0576 void ItemComments::changeComment(int index, const QString& comment)
0577 {
0578     if (!d)
0579     {
0580         return;
0581     }
0582 
0583     d->infos[index].comment = comment;
0584     d->dirtyIndices << index;
0585 }
0586 
0587 void ItemComments::changeLanguage(int index, const QString& language)
0588 {
0589     if (!d)
0590     {
0591         return;
0592     }
0593 
0594     d->infos[index].language = language;
0595     d->dirtyIndices << index;
0596 }
0597 
0598 void ItemComments::changeAuthor(int index, const QString& author)
0599 {
0600     if (!d)
0601     {
0602         return;
0603     }
0604 
0605     d->infos[index].author = author;
0606     d->dirtyIndices << index;
0607 }
0608 
0609 void ItemComments::changeDate(int index, const QDateTime& date)
0610 {
0611     if (!d)
0612     {
0613         return;
0614     }
0615 
0616     d->infos[index].date = date;
0617     d->dirtyIndices << index;
0618 }
0619 
0620 void ItemComments::changeType(int index, DatabaseComment::Type type)
0621 {
0622     if (!d)
0623     {
0624         return;
0625     }
0626 
0627     d->infos[index].type = type;
0628     d->dirtyIndices << index;
0629 }
0630 
0631 void ItemComments::apply()
0632 {
0633     if (!d)
0634     {
0635         return;
0636     }
0637 
0638     CoreDbAccess access;
0639     apply(access);
0640 }
0641 
0642 void ItemComments::apply(CoreDbAccess& access)
0643 {
0644     if (!d)
0645     {
0646         return;
0647     }
0648 
0649     Q_FOREACH (int commentId, d->idsToRemove)
0650     {
0651         access.db()->removeImageComment(commentId, d->id);
0652     }
0653 
0654     d->idsToRemove.clear();
0655 
0656     Q_FOREACH (int index, d->newIndices)
0657     {
0658         CommentInfo& info = d->infos[index];
0659         info.id           = access.db()->setImageComment(d->id, info.comment, info.type, info.language, info.author, info.date);
0660     }
0661 
0662     d->dirtyIndices.subtract(d->newIndices);
0663     d->newIndices.clear();
0664 
0665     Q_FOREACH (int index, d->dirtyIndices)
0666     {
0667         QVariantList values;
0668         CommentInfo& info = d->infos[index];
0669         values << (int)info.type << info.language << info.author << info.date << info.comment;
0670         access.db()->changeImageComment(info.id, d->id, values);
0671     }
0672 
0673     d->dirtyIndices.clear();
0674 }
0675 
0676 CaptionsMap ItemComments::toCaptionsMap(DatabaseComment::Type type) const
0677 {
0678     CaptionsMap map;
0679 
0680     if (d)
0681     {
0682         Q_FOREACH (const CommentInfo& info, d->infos)
0683         {
0684             if (info.type == type)
0685             {
0686                 CaptionValues val;
0687                 val.caption        = info.comment;
0688                 val.author         = info.author;
0689                 val.date           = info.date;
0690                 map[info.language] = val;
0691             }
0692         }
0693     }
0694 
0695     return map;
0696 }
0697 
0698 } // namespace Digikam