1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2007-09-19
7  * Description : Access to comments of an item in the database
8  *
9  * Copyright (C) 2007-2013 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
10  * Copyright (C) 2009-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11  *
12  * This program is free software; you can redistribute it
13  * and/or modify it under the terms of the GNU General
14  * Public License as published by the Free Software Foundation;
15  * either version 2, or (at your option)
16  * any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * ============================================================ */
24 
25 #include "itemcomments.h"
26 
27 // Qt includes
28 
29 #include <QLocale>
30 
31 // Local includes
32 
33 #include "coredb.h"
34 
35 namespace Digikam
36 {
37 
38 class Q_DECL_HIDDEN ItemComments::Private : public QSharedData
39 {
40 public:
41 
Private()42     explicit Private()
43       : id    (-1),
44         unique(ItemComments::UniquePerLanguage)
45     {
46     }
47 
init(const CoreDbAccess & access,qlonglong imageId)48     void init(const CoreDbAccess& access, qlonglong imageId)
49     {
50         id    = imageId;
51         infos = access.db()->getItemComments(id);
52 
53         for (int i = 0 ; i < infos.size() ; ++i)
54         {
55             CommentInfo& info = infos[i];
56 
57             if (info.language.isNull())
58             {
59                 info.language = QLatin1String("x-default");
60             }
61         }
62     }
63 
languageMatch(const QString & fullCode,const QString & langCode,int & fullCodeMatch,int & langCodeMatch,int & defaultCodeMatch,int & firstMatch,DatabaseComment::Type type=DatabaseComment::Comment) const64     void languageMatch(const QString& fullCode,
65                        const QString& langCode,
66                        int& fullCodeMatch,
67                        int& langCodeMatch,
68                        int& defaultCodeMatch,
69                        int& firstMatch,
70                        DatabaseComment::Type type = DatabaseComment::Comment) const
71     {
72         // if you change the algorithm, please take a look at ItemCopyright as well
73 
74         fullCodeMatch    = -1;
75         langCodeMatch    = -1;
76         defaultCodeMatch = -1;
77         firstMatch       = -1;
78 
79         if (infos.isEmpty())
80         {
81             return;
82         }
83 
84         // First we search for a full match
85         // Second for a match of the language code
86         // Third for the default code
87         // Fourth we return the first comment
88 
89         QLatin1String defaultCode("x-default");
90 
91         for (int i = 0 ; i < infos.size() ; ++i)
92         {
93             const CommentInfo& info = infos.at(i);
94 
95             if (info.type == type)
96             {
97                 if (firstMatch == -1)
98                 {
99                     firstMatch = i;
100                 }
101 
102                 if      (info.language == fullCode)
103                 {
104                     fullCodeMatch = i;
105                     break;
106                 }
107                 else if (info.language.startsWith(langCode) && langCodeMatch == -1)
108                 {
109                     langCodeMatch = i;
110                 }
111                 else if (info.language == defaultCode)
112                 {
113                     defaultCodeMatch = i;
114                 }
115             }
116         }
117     }
118 
adjustStoredIndexes(QSet<int> & set,int removedIndex)119     void adjustStoredIndexes(QSet<int>& set, int removedIndex)
120     {
121         QSet<int> newSet;
122 
123         foreach (int index, set)
124         {
125             if      (index > removedIndex)
126             {
127                 newSet << index - 1;
128             }
129             else if (index < removedIndex)
130             {
131                 newSet << index;
132             }
133 
134             // drop index == removedIndex
135         }
136 
137         set = newSet;
138     }
139 
adjustStoredIndexes(int removedIndex)140     void adjustStoredIndexes(int removedIndex)
141     {
142         adjustStoredIndexes(dirtyIndices, removedIndex);
143         adjustStoredIndexes(newIndices,   removedIndex);
144     }
145 
146 public:
147 
148     qlonglong                     id;
149     QList<CommentInfo>            infos;
150     QSet<int>                     dirtyIndices;
151     QSet<int>                     newIndices;
152     QSet<int>                     idsToRemove;
153     ItemComments::UniqueBehavior unique;
154 };
155 
ItemComments()156 ItemComments::ItemComments()
157     : d(nullptr)
158 {
159 }
160 
ItemComments(qlonglong imageid)161 ItemComments::ItemComments(qlonglong imageid)
162     : d(new Private)
163 {
164     CoreDbAccess access;
165     d->init(access, imageid);
166 }
167 
ItemComments(const CoreDbAccess & access,qlonglong imageid)168 ItemComments::ItemComments(const CoreDbAccess& access, qlonglong imageid)
169     : d(new Private)
170 {
171     d->init(access, imageid);
172 }
173 
ItemComments(const ItemComments & other)174 ItemComments::ItemComments(const ItemComments& other)
175     : d(other.d)
176 {
177 }
178 
~ItemComments()179 ItemComments::~ItemComments()
180 {
181     apply();
182 }
183 
operator =(const ItemComments & other)184 ItemComments& ItemComments::operator=(const ItemComments& other)
185 {
186     d = other.d;
187 
188     return *this;
189 }
190 
isNull() const191 bool ItemComments::isNull() const
192 {
193     return !d;
194 }
195 
defaultComment(DatabaseComment::Type type) const196 QString ItemComments::defaultComment(DatabaseComment::Type type) const
197 {
198     return defaultComment(nullptr, type);
199 }
200 
defaultComment(int * const index,DatabaseComment::Type type) const201 QString ItemComments::defaultComment(int* const index, DatabaseComment::Type type) const
202 {
203     if (!d)
204     {
205         return QString();
206     }
207 
208     QString spec     = QLocale().name().toLower();
209     QString langCode = spec.left(spec.indexOf(QLatin1Char('_'))) + QLatin1Char('-');
210     QString fullCode = spec.replace(QLatin1Char('_'), QLatin1Char('-'));
211 
212     int fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch;
213 
214     d->languageMatch(fullCode, langCode, fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch, type);
215 
216     int chosen = fullCodeMatch;
217 
218     if (chosen == -1)
219     {
220         chosen = langCodeMatch;
221     }
222 
223     if (chosen == -1)
224     {
225         chosen = defaultCodeMatch;
226     }
227 
228     if (chosen == -1)
229     {
230         chosen = firstMatch;
231     }
232 
233     if (index)
234     {
235         *index = chosen;
236     }
237 
238     if (chosen == -1)
239     {
240         return QString();
241     }
242     else
243     {
244         return d->infos.at(chosen).comment;
245     }
246 }
247 
commentForLanguage(const QString & languageCode,int * const index,LanguageChoiceBehavior behavior) const248 QString ItemComments::commentForLanguage(const QString& languageCode,
249                                          int* const index,
250                                          LanguageChoiceBehavior behavior) const
251 {
252     if (!d)
253     {
254         return QString();
255     }
256 
257     int fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch;
258 
259     // en-us => en-
260 
261     QString firstPart;
262 
263     if (languageCode == QLatin1String("x-default"))
264     {
265         firstPart = languageCode;
266     }
267     else
268     {
269         firstPart = languageCode.section(QLatin1Char('-'), 0, 0, QString::SectionIncludeTrailingSep);
270     }
271 
272     d->languageMatch(languageCode, firstPart, fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch);
273 
274     int chosen = fullCodeMatch;
275 
276     if (chosen == -1)
277     {
278         chosen = langCodeMatch;
279     }
280 
281     if ((chosen == -1) && (behavior > ReturnMatchingLanguageOnly))
282     {
283         chosen = defaultCodeMatch;
284 
285         if ((chosen == -1) && (behavior == ReturnMatchingDefaultOrFirstLanguage))
286         {
287             chosen = firstMatch;
288         }
289     }
290 
291     if (index)
292     {
293         *index = chosen;
294     }
295 
296     if (chosen == -1)
297     {
298         return QString();
299     }
300     else
301     {
302         return d->infos.at(chosen).comment;
303     }
304 }
305 
numberOfComments() const306 int ItemComments::numberOfComments() const
307 {
308     if (!d)
309     {
310         return 0;
311     }
312 
313     return d->infos.size();
314 }
315 
type(int index) const316 DatabaseComment::Type ItemComments::type(int index) const
317 {
318     if (!d)
319     {
320         return DatabaseComment::UndefinedType;
321     }
322 
323     return d->infos.at(index).type;
324 }
325 
language(int index) const326 QString ItemComments::language(int index) const
327 {
328     if (!d)
329     {
330         return QString();
331     }
332 
333     return d->infos.at(index).language;
334 }
335 
author(int index) const336 QString ItemComments::author(int index) const
337 {
338     if (!d)
339     {
340         return QString();
341     }
342 
343     return d->infos.at(index).author;
344 }
345 
date(int index) const346 QDateTime ItemComments::date(int index) const
347 {
348     if (!d)
349     {
350         return QDateTime();
351     }
352 
353     return d->infos.at(index).date;
354 }
355 
comment(int index) const356 QString ItemComments::comment(int index) const
357 {
358     if (!d)
359     {
360         return QString();
361     }
362 
363     return d->infos.at(index).comment;
364 }
365 
setUniqueBehavior(UniqueBehavior behavior)366 void ItemComments::setUniqueBehavior(UniqueBehavior behavior)
367 {
368     if (!d)
369     {
370         return;
371     }
372 
373     d->unique = behavior;
374 }
375 
addComment(const QString & comment,const QString & lang,const QString & author_,const QDateTime & date,DatabaseComment::Type type)376 void ItemComments::addComment(const QString& comment,
377                               const QString& lang,
378                               const QString& author_,
379                               const QDateTime& date,
380                               DatabaseComment::Type type)
381 {
382     if (!d)
383     {
384         return;
385     }
386 
387     bool multipleCommentsPerLanguage = (d->unique == UniquePerLanguageAndAuthor);
388     QString language                 = lang;
389 
390     if (language.isEmpty())
391     {
392         language = QLatin1String("x-default");
393     }
394 
395     QString author = author_;
396 
397     /// @todo This makes no sense - is another variable supposed to be used instead? - Michael Hansen
398 
399     if (author.isEmpty())
400     {
401         author = QString();
402     }
403 
404     for (int i = 0 ; i < d->infos.size() ; ++i)
405     {
406         CommentInfo& info = d->infos[i];
407 
408         // some extra considerations on replacing
409 
410         if ((info.type == type) && (info.type == DatabaseComment::Comment) && (info.language == language))
411         {
412             if (!multipleCommentsPerLanguage || (info.author == author))
413             {
414                 info.comment = comment;
415                 info.date    = date;
416                 info.author  = author;
417                 d->dirtyIndices << i;
418                 return;
419             }
420         }
421 
422         // simulate unique restrictions of db.
423         // There is a problem that a NULL value is never unique, see #189080
424 
425         if ((info.type == type)         &&
426             (info.language == language) &&
427             ((info.author == author) || (info.author.isEmpty() && author.isEmpty())))
428         {
429             info.comment = comment;
430             info.date    = date;
431             d->dirtyIndices << i;
432             return;
433         }
434     }
435 
436     addCommentDirectly(comment, language, author, type, date);
437 }
438 
addHeadline(const QString & headline,const QString & lang,const QString & author,const QDateTime & date)439 void ItemComments::addHeadline(const QString& headline, const QString& lang,
440                                const QString& author, const QDateTime& date)
441 {
442     addComment(headline, lang, author, date, DatabaseComment::Headline);
443 }
444 
addTitle(const QString & title,const QString & lang,const QString & author,const QDateTime & date)445 void ItemComments::addTitle(const QString& title, const QString& lang,
446                             const QString& author, const QDateTime& date)
447 {
448     addComment(title, lang, author, date, DatabaseComment::Title);
449 }
450 
replaceComments(const CaptionsMap & map,DatabaseComment::Type type)451 void ItemComments::replaceComments(const CaptionsMap& map, DatabaseComment::Type type)
452 {
453     if (!d)
454     {
455         return;
456     }
457 
458     d->dirtyIndices.clear();
459 
460     for (CaptionsMap::const_iterator it = map.constBegin() ; it != map.constEnd() ; ++it)
461     {
462         CaptionValues val = it.value();
463         addComment(val.caption, it.key(), val.author, val.date, type);
464     }
465 
466     // remove all comments of this type that have not been touched above
467 
468     for (int i = 0 ; i < d->infos.size() /* changing! */ ; )
469     {
470         if (!d->dirtyIndices.contains(i) && !d->newIndices.contains(i) && (d->infos[i].type == type))
471         {
472             remove(i);
473         }
474         else
475         {
476             ++i;
477         }
478     }
479 }
480 
replaceFrom(const ItemComments & source)481 void ItemComments::replaceFrom(const ItemComments& source)
482 {
483     if (!d)
484     {
485         return;
486     }
487 
488     if (!source.d)
489     {
490         removeAll();
491         return;
492     }
493 
494     foreach (const CommentInfo& info, source.d->infos)
495     {
496         addComment(info.comment, info.language, info.author, info.date, info.type);
497     }
498 
499     // remove all that have not been touched above
500 
501     for (int i = 0 ; i < d->infos.size() /* changing! */ ; )
502     {
503         if (!d->dirtyIndices.contains(i) && !d->newIndices.contains(i))
504         {
505             remove(i);
506         }
507         else
508         {
509             ++i;
510         }
511     }
512 }
513 
addCommentDirectly(const QString & comment,const QString & language,const QString & author,DatabaseComment::Type type,const QDateTime & date)514 void ItemComments::addCommentDirectly(const QString& comment,
515                                       const QString& language,
516                                       const QString& author,
517                                       DatabaseComment::Type type,
518                                       const QDateTime& date)
519 {
520     CommentInfo info;
521     info.comment  = comment;
522     info.language = language;
523     info.author   = author;
524     info.type     = type;
525     info.date     = date;
526 
527     d->newIndices << d->infos.size();
528     d->infos      << info;
529 }
530 
remove(int index)531 void ItemComments::remove(int index)
532 {
533     if (!d)
534     {
535         return;
536     }
537 
538     d->idsToRemove << d->infos.at(index).id;
539     d->infos.removeAt(index);
540     d->adjustStoredIndexes(index);
541 }
542 
removeAll(DatabaseComment::Type type)543 void ItemComments::removeAll(DatabaseComment::Type type)
544 {
545     if (!d)
546     {
547         return;
548     }
549 
550     for (int i = 0 ; i < d->infos.size() /* changing! */ ; )
551     {
552         if (d->infos.at(i).type == type)
553         {
554             remove(i);
555         }
556         else
557         {
558             ++i;
559         }
560     }
561 }
562 
removeAllComments()563 void ItemComments::removeAllComments()
564 {
565     removeAll(DatabaseComment::Comment);
566 }
567 
removeAll()568 void ItemComments::removeAll()
569 {
570     if (!d)
571     {
572         return;
573     }
574 
575     foreach (const CommentInfo& info, d->infos)
576     {
577         d->idsToRemove << info.id;
578     }
579 
580     d->infos.clear();
581     d->dirtyIndices.clear();
582     d->newIndices.clear();
583 }
584 
changeComment(int index,const QString & comment)585 void ItemComments::changeComment(int index, const QString& comment)
586 {
587     if (!d)
588     {
589         return;
590     }
591 
592     d->infos[index].comment = comment;
593     d->dirtyIndices << index;
594 }
595 
changeLanguage(int index,const QString & language)596 void ItemComments::changeLanguage(int index, const QString& language)
597 {
598     if (!d)
599     {
600         return;
601     }
602 
603     d->infos[index].language = language;
604     d->dirtyIndices << index;
605 }
606 
changeAuthor(int index,const QString & author)607 void ItemComments::changeAuthor(int index, const QString& author)
608 {
609     if (!d)
610     {
611         return;
612     }
613 
614     d->infos[index].author = author;
615     d->dirtyIndices << index;
616 }
617 
changeDate(int index,const QDateTime & date)618 void ItemComments::changeDate(int index, const QDateTime& date)
619 {
620     if (!d)
621     {
622         return;
623     }
624 
625     d->infos[index].date = date;
626     d->dirtyIndices << index;
627 }
628 
changeType(int index,DatabaseComment::Type type)629 void ItemComments::changeType(int index, DatabaseComment::Type type)
630 {
631     if (!d)
632     {
633         return;
634     }
635 
636     d->infos[index].type = type;
637     d->dirtyIndices << index;
638 }
639 
apply()640 void ItemComments::apply()
641 {
642     if (!d)
643     {
644         return;
645     }
646 
647     CoreDbAccess access;
648     apply(access);
649 }
650 
apply(CoreDbAccess & access)651 void ItemComments::apply(CoreDbAccess& access)
652 {
653     if (!d)
654     {
655         return;
656     }
657 
658     foreach (int commentId, d->idsToRemove)
659     {
660         access.db()->removeImageComment(commentId, d->id);
661     }
662 
663     d->idsToRemove.clear();
664 
665     foreach (int index, d->newIndices)
666     {
667         CommentInfo& info = d->infos[index];
668         info.id           = access.db()->setImageComment(d->id, info.comment, info.type, info.language, info.author, info.date);
669     }
670 
671     d->dirtyIndices.subtract(d->newIndices);
672     d->newIndices.clear();
673 
674     foreach (int index, d->dirtyIndices)
675     {
676         QVariantList values;
677         CommentInfo& info = d->infos[index];
678         values << (int)info.type << info.language << info.author << info.date << info.comment;
679         access.db()->changeImageComment(info.id, d->id, values);
680     }
681 
682     d->dirtyIndices.clear();
683 }
684 
toCaptionsMap(DatabaseComment::Type type) const685 CaptionsMap ItemComments::toCaptionsMap(DatabaseComment::Type type) const
686 {
687     CaptionsMap map;
688 
689     if (d)
690     {
691         foreach (const CommentInfo& info, d->infos)
692         {
693             if (info.type == type)
694             {
695                 CaptionValues val;
696                 val.caption        = info.comment;
697                 val.author         = info.author;
698                 val.date           = info.date;
699                 map[info.language] = val;
700             }
701         }
702     }
703 
704     return map;
705 }
706 
707 } // namespace Digikam
708