File indexing completed on 2024-05-05 05:04:33

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2016-2020 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include "bibliographymodel.h"
0021 
0022 #include <QSettings>
0023 
0024 #include <Entry>
0025 #include <onlinesearch/OnlineSearchAbstract>
0026 
0027 const int SortedBibliographyModel::SortAuthorNewestTitle = 0;
0028 const int SortedBibliographyModel::SortAuthorOldestTitle = 1;
0029 const int SortedBibliographyModel::SortNewestAuthorTitle = 2;
0030 const int SortedBibliographyModel::SortOldestAuthorTitle = 3;
0031 
0032 SortedBibliographyModel::SortedBibliographyModel()
0033         : QSortFilterProxyModel(), m_sortOrder(0), model(new BibliographyModel()) {
0034     const QSettings settings(QStringLiteral("harbour-bibsearch"), QStringLiteral("BibSearch"));
0035     m_sortOrder = settings.value(QStringLiteral("sortOrder"), 0).toInt();
0036 
0037     setSourceModel(model);
0038     setDynamicSortFilter(true);
0039     sort(0);
0040     connect(model, &BibliographyModel::busyChanged, this, &SortedBibliographyModel::busyChanged);
0041     connect(model, &BibliographyModel::progressChanged, this, &SortedBibliographyModel::progressChanged);
0042 }
0043 
0044 SortedBibliographyModel::~SortedBibliographyModel() {
0045     QSettings settings(QStringLiteral("harbour-bibsearch"), QStringLiteral("BibSearch"));
0046     settings.setValue(QStringLiteral("sortOrder"), m_sortOrder);
0047 
0048     delete model;
0049 }
0050 
0051 QHash<int, QByteArray> SortedBibliographyModel::roleNames() const {
0052     QHash<int, QByteArray> roles;
0053     roles[BibTeXIdRole] = "bibtexid";
0054     roles[FoundViaRole] = "foundVia";
0055     roles[TitleRole] = "title";
0056     roles[AuthorRole] = "author";
0057     roles[AuthorShortRole] = "authorShort";
0058     roles[YearRole] = "year";
0059     roles[WherePublishedRole] = "wherePublished";
0060     roles[UrlRole] = "url";
0061     roles[DoiRole] = "doi";
0062     return roles;
0063 }
0064 
0065 bool SortedBibliographyModel::isBusy() const {
0066     if (model != nullptr)
0067         return model->isBusy();
0068     else
0069         return false;
0070 }
0071 
0072 int SortedBibliographyModel::progress() const {
0073     if (model != nullptr)
0074         return model->progress();
0075     else
0076         return -1;
0077 }
0078 
0079 int SortedBibliographyModel::sortOrder() const {
0080     return m_sortOrder;
0081 }
0082 
0083 void SortedBibliographyModel::setSortOrder(int _sortOrder) {
0084     if (_sortOrder != m_sortOrder) {
0085         m_sortOrder = _sortOrder;
0086         invalidate();
0087         emit sortOrderChanged(m_sortOrder);
0088     }
0089 }
0090 
0091 QStringList SortedBibliographyModel::humanReadableSortOrder() const {
0092     static const QStringList result {
0093                                      tr("Last name, newest first"), ///< SortAuthorNewestTitle
0094                                      tr("Last name, oldest first"), ///< SortAuthorOldestTitle
0095                                      tr("Newest first, last name"), ///< SortNewestAuthorTitle
0096                                      tr("Newest first, last name")  ///< SortOldestAuthorTitle
0097                                     };
0098     return result;
0099 }
0100 
0101 void SortedBibliographyModel::startSearch(const QString &freeText, const QString &title, const QString &author) {
0102     if (model != nullptr)
0103         model->startSearch(freeText, title, author);
0104 }
0105 
0106 void SortedBibliographyModel::clear() {
0107     if (model != nullptr)
0108         model->clear();
0109 }
0110 
0111 bool SortedBibliographyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const {
0112     if (model == nullptr)
0113         return source_left.row() < source_right.row();
0114 
0115     const QSharedPointer<const Entry> entryLeft = model->entry(source_left.row());
0116     const QSharedPointer<const Entry> entryRight = model->entry(source_right.row());
0117     if (entryLeft.isNull() || entryRight.isNull())
0118         return source_left.row() < source_right.row();
0119 
0120     SortingTriState sortingTriState = Undecided;
0121     switch (m_sortOrder) {
0122     case SortAuthorNewestTitle:
0123         sortingTriState = compareTitles(entryLeft, entryRight, compareYears(entryLeft, entryRight, MostRecentFirst, compareAuthors(entryLeft, entryRight)));
0124         break;
0125     case SortAuthorOldestTitle:
0126         sortingTriState = compareTitles(entryLeft, entryRight, compareYears(entryLeft, entryRight, LeastRecentFirst, compareAuthors(entryLeft, entryRight)));
0127         break;
0128     case SortNewestAuthorTitle:
0129         sortingTriState = compareTitles(entryLeft, entryRight, compareAuthors(entryLeft, entryRight, compareYears(entryLeft, entryRight, MostRecentFirst)));
0130         break;
0131     case SortOldestAuthorTitle:
0132         sortingTriState = compareTitles(entryLeft, entryRight, compareAuthors(entryLeft, entryRight, compareYears(entryLeft, entryRight, LeastRecentFirst)));
0133         break;
0134     default:
0135         sortingTriState = Undecided;
0136     }
0137 
0138     switch (sortingTriState) {
0139     case True:
0140         return true;
0141     case False:
0142         return false;
0143     default:
0144         return (source_left.row() < source_right.row());
0145     }
0146 }
0147 
0148 SortedBibliographyModel::SortingTriState SortedBibliographyModel::compareAuthors(const QSharedPointer<const Entry> entryLeft, const QSharedPointer<const Entry> entryRight, SortedBibliographyModel::SortingTriState previousDecision) const {
0149     if (previousDecision != Undecided) return previousDecision;
0150 
0151     const Value authorsLeft = entryLeft->operator[](Entry::ftAuthor);
0152     const Value authorsRight = entryRight->operator[](Entry::ftAuthor);
0153     int p = 0;
0154     while (p < authorsLeft.count() && p < authorsRight.count()) {
0155         const QSharedPointer<Person> personLeft = authorsLeft.at(p).dynamicCast<Person>();
0156         const QSharedPointer<Person> personRight = authorsRight.at(p).dynamicCast<Person>();
0157         if (personLeft.isNull() || personRight.isNull())
0158             return Undecided;
0159 
0160         const int cmpLast = removeNobiliaryParticle(personLeft->lastName()).localeAwareCompare(removeNobiliaryParticle(personRight->lastName()));
0161         if (cmpLast < 0) return True;
0162         else if (cmpLast > 0) return False;
0163         const int cmpFirst = personLeft->firstName().left(1).localeAwareCompare(personRight->firstName().left(1));
0164         if (cmpFirst < 0) return True;
0165         else if (cmpFirst > 0) return False;
0166 
0167         ++p;
0168     }
0169     if (authorsLeft.count() < authorsRight.count()) return True;
0170     else if (authorsLeft.count() > authorsRight.count()) return False;
0171 
0172     return Undecided;
0173 }
0174 
0175 SortedBibliographyModel::SortingTriState SortedBibliographyModel::compareYears(const QSharedPointer<const Entry> entryLeft, const QSharedPointer<const Entry> entryRight, AgeSorting ageSorting, SortedBibliographyModel::SortingTriState previousDecision) const {
0176     if (previousDecision != Undecided) return previousDecision;
0177 
0178     bool yearLeftOk = false, yearRightOk = false;
0179     const int yearLeft = BibliographyModel::valueToText(entryLeft->operator[](Entry::ftYear)).toInt(&yearLeftOk);
0180     const int yearRight = BibliographyModel::valueToText(entryRight->operator[](Entry::ftYear)).toInt(&yearRightOk);
0181     if (yearLeftOk && yearRightOk && yearLeft > 1000 && yearRight > 1000 && yearLeft < 3000 && yearRight < 3000) {
0182         if (yearLeft < yearRight) return ageSorting == LeastRecentFirst ? True : False;
0183         else if (yearLeft > yearRight) return ageSorting == LeastRecentFirst ? False : True;
0184     }
0185 
0186     return Undecided;
0187 }
0188 
0189 SortedBibliographyModel::SortingTriState SortedBibliographyModel::compareTitles(const QSharedPointer<const Entry> entryLeft, const QSharedPointer<const Entry> entryRight, SortedBibliographyModel::SortingTriState previousDecision) const {
0190     if (previousDecision != Undecided) return previousDecision;
0191 
0192     const QString titleLeft = BibliographyModel::valueToText(entryLeft->operator[](Entry::ftTitle));
0193     const QString titleRight = BibliographyModel::valueToText(entryRight->operator[](Entry::ftTitle));
0194     const int titleCmp = titleLeft.localeAwareCompare(titleRight);
0195     if (titleCmp < 0) return True;
0196     else if (titleCmp > 0) return False;
0197 
0198     return Undecided;
0199 }
0200 
0201 QString SortedBibliographyModel::removeNobiliaryParticle(const QString &lastname) const {
0202     static const QStringList nobiliaryParticles {QStringLiteral("af "), QStringLiteral("d'"), QStringLiteral("de "), QStringLiteral("di "), QStringLiteral("du "), QStringLiteral("of "), QStringLiteral("van "), QStringLiteral("von "), QStringLiteral("zu ")};
0203     for (QStringList::ConstIterator it = nobiliaryParticles.constBegin(); it != nobiliaryParticles.constEnd(); ++it)
0204         if (lastname.startsWith(*it))
0205             return lastname.mid(it->length());
0206     return lastname;
0207 }
0208 
0209 BibliographyModel::BibliographyModel() {
0210     m_file = new File();
0211     m_runningSearches = 0;
0212 
0213     m_searchEngineList = new SearchEngineList();
0214     connect(m_searchEngineList, &SearchEngineList::foundEntry, this, &BibliographyModel::newEntry);
0215     connect(m_searchEngineList, &SearchEngineList::busyChanged, this, &BibliographyModel::busyChanged);
0216     connect(m_searchEngineList, &SearchEngineList::progressChanged, this, &BibliographyModel::progressChanged);
0217 }
0218 
0219 BibliographyModel::~BibliographyModel() {
0220     delete m_file;
0221     delete m_searchEngineList;
0222 }
0223 
0224 int BibliographyModel::rowCount(const QModelIndex &parent) const {
0225     if (parent == QModelIndex())
0226         return m_file->count();
0227     else
0228         return 0;
0229 }
0230 
0231 QVariant BibliographyModel::data(const QModelIndex &index, int role) const {
0232     if (index.row() < 0 || index.row() >= m_file->count() || index.column() != 0)
0233         return QVariant();
0234 
0235     const QSharedPointer<const Entry> curEntry = entry(index.row());
0236 
0237     if (!curEntry.isNull()) {
0238         QString fieldName;
0239         switch (role) {
0240         case Qt::DisplayRole: /// fall-through on purpose
0241         case TitleRole: fieldName = Entry::ftTitle; break;
0242         case AuthorRole: fieldName = Entry::ftAuthor; break;
0243         case YearRole: fieldName = Entry::ftYear; break;
0244         }
0245         if (!fieldName.isEmpty())
0246             return valueToText(curEntry->operator[](fieldName));
0247 
0248         if (role == BibTeXIdRole) {
0249             return curEntry->id();
0250         } else if (role == FoundViaRole) {
0251             const QString foundVia = valueToText(curEntry->operator[](QStringLiteral("x-fetchedfrom")));
0252             if (!foundVia.isEmpty()) return foundVia;
0253         } else if (role == AuthorRole) {
0254             const QString authors = valueToText(curEntry->operator[](Entry::ftAuthor));
0255             if (!authors.isEmpty()) return authors;
0256             else return valueToText(curEntry->operator[](Entry::ftEditor));
0257         } else if (role == WherePublishedRole) {
0258             const QString journal = valueToText(curEntry->operator[](Entry::ftJournal));
0259             if (!journal.isEmpty()) {
0260                 const QString volume = valueToText(curEntry->operator[](Entry::ftVolume));
0261                 const QString issue = valueToText(curEntry->operator[](Entry::ftNumber));
0262                 if (volume.isEmpty())
0263                     return journal;
0264                 else if (issue.isEmpty()) /// but 'volume' is not empty
0265                     return journal + QStringLiteral(" ") + volume;
0266                 else /// both 'volume' and 'issue' are not empty
0267                     return journal + QStringLiteral(" ") + volume + QStringLiteral(" (") + issue + QStringLiteral(")");
0268             }
0269             const QString bookTitle = valueToText(curEntry->operator[](Entry::ftBookTitle));
0270             if (!bookTitle.isEmpty()) return bookTitle;
0271             const bool isPhdThesis = curEntry->type() == Entry::etPhDThesis;
0272             if (isPhdThesis) {
0273                 const QString school = valueToText(curEntry->operator[](Entry::ftSchool));
0274                 if (school.isEmpty()) {
0275                     return tr("Doctoral dissertation");
0276                 } else {
0277                     return tr("Doctoral dissertation (%1)").arg(school);
0278                 }
0279             }
0280             const QString school = valueToText(curEntry->operator[](Entry::ftSchool));
0281             if (!school.isEmpty()) return school;
0282             const QString publisher = valueToText(curEntry->operator[](Entry::ftPublisher));
0283             if (!publisher.isEmpty()) return publisher;
0284             return QStringLiteral("");
0285         } else if (role == AuthorShortRole) {
0286             const QStringList authors = valueToList(curEntry->operator[](Entry::ftAuthor));
0287             switch (authors.size()) {
0288             case 0: return QString(); ///< empty list of authors
0289             case 1: return authors.first(); ///< single author
0290             case 2:
0291                 return tr("%1 and %2").arg(authors.first()).arg(authors[1]); ///< two authors
0292             default:
0293                 return tr("%1 and %2 more").arg(authors.first()).arg(QString::number(authors.size() - 1)); ///< three or more authors
0294             }
0295         } else if (role == UrlRole) {
0296             const QStringList doiList = valueToList(curEntry->operator[](Entry::ftDOI));
0297             if (!doiList.isEmpty()) return QStringLiteral("https://dx.doi.org/") + doiList.first();
0298             const QStringList urlList = valueToList(curEntry->operator[](Entry::ftUrl));
0299             if (!urlList.isEmpty()) return urlList.first();
0300             const QStringList bibUrlList = valueToList(curEntry->operator[](QStringLiteral("biburl")));
0301             if (!bibUrlList.isEmpty()) return bibUrlList.first();
0302             return QString();
0303         } else if (role == DoiRole) {
0304             const QStringList doiList = valueToList(curEntry->operator[](Entry::ftDOI));
0305             if (!doiList.isEmpty()) return doiList.first();
0306             return QString();
0307         }
0308     }
0309 
0310     return QVariant();
0311 }
0312 
0313 const QSharedPointer<const Entry> BibliographyModel::entry(int row) const {
0314     const QSharedPointer<const Element> element = m_file->at(row);
0315     const QSharedPointer<const Entry> result = element.dynamicCast<const Entry>();
0316     return result;
0317 }
0318 
0319 void BibliographyModel::startSearch(const QString &freeText, const QString &title, const QString &author) {
0320     QMap<OnlineSearchAbstract::QueryKey, QString> query;
0321     query[OnlineSearchAbstract::QueryKey::FreeText] = freeText;
0322     query[OnlineSearchAbstract::QueryKey::Title] = title;
0323     query[OnlineSearchAbstract::QueryKey::Author] = author;
0324 
0325     m_searchEngineList->resetProgress();
0326 
0327     m_runningSearches = 0;
0328     const QSettings settings(QStringLiteral("harbour-bibsearch"), QStringLiteral("BibSearch"));
0329     for (int i = 0; i < m_searchEngineList->size(); ++i) {
0330         OnlineSearchAbstract *osa = m_searchEngineList->at(i);
0331         const bool doSearchThisEngine = isSearchEngineEnabled(settings, osa);
0332         if (!doSearchThisEngine) continue;
0333         osa->startSearch(query, 10 /** TODO make number configurable */);
0334         ++m_runningSearches;
0335     }
0336 }
0337 
0338 void BibliographyModel::clear() {
0339     beginResetModel();
0340     m_file->clear();
0341     endResetModel();
0342 }
0343 
0344 bool BibliographyModel::isBusy() const {
0345     for (QVector<OnlineSearchAbstract *>::ConstIterator it = m_searchEngineList->constBegin(); it != m_searchEngineList->constEnd(); ++it) {
0346         if ((*it)->busy()) return true;
0347     }
0348     return false;
0349 }
0350 
0351 int BibliographyModel::progress() const {
0352     return m_searchEngineList->progress();
0353 }
0354 
0355 void BibliographyModel::searchFinished() {
0356     --m_runningSearches;
0357 }
0358 
0359 void BibliographyModel::newEntry(QSharedPointer<Entry> e) {
0360     const int n = m_file->count();
0361     beginInsertRows(QModelIndex(), n, n);
0362     m_file->insert(n, e);
0363     endInsertRows();
0364 }
0365 
0366 QString BibliographyModel::valueToText(const Value &value) {
0367     return valueToList(value).join(QStringLiteral(", "));
0368 }
0369 
0370 
0371 QStringList BibliographyModel::valueToList(const Value &value) {
0372     if (value.isEmpty()) return QStringList();
0373 
0374     QStringList resultItems;
0375 
0376     const QSharedPointer<const Person> firstPerson = value.first().dynamicCast<const Person>();
0377     if (!firstPerson.isNull()) {
0378         /// First item in value is a Person, assume all other items are Persons as well
0379         for (Value::ConstIterator it = value.constBegin(); it != value.constEnd(); ++it) {
0380             QSharedPointer<const Person> person = (*it).dynamicCast<const Person>();
0381             if (person.isNull()) continue;
0382             const QString name = personToText(person);
0383             if (name.isEmpty()) continue;
0384             resultItems.append(beautifyLaTeX(name));
0385         }
0386     } else {
0387         for (Value::ConstIterator it = value.constBegin(); it != value.constEnd(); ++it) {
0388             const QString valueItem = valueItemToText(*it);
0389             if (valueItem.isEmpty()) continue;
0390             resultItems.append(beautifyLaTeX(valueItem));
0391         }
0392     }
0393 
0394     return resultItems;
0395 }
0396 
0397 QString BibliographyModel::personToText(const QSharedPointer<const Person> &person) {
0398     if (person.isNull()) return QString();
0399     QString name = person->lastName();
0400     if (name.isEmpty()) return QString();
0401     const QString firstName = person->firstName().left(1);
0402     if (!firstName.isEmpty())
0403         name = name.prepend(QStringLiteral(". ")).prepend(firstName);
0404     return name;
0405 }
0406 
0407 QString BibliographyModel::valueItemToText(const QSharedPointer<ValueItem> &valueItem) {
0408     const QSharedPointer<PlainText> plainText = valueItem.dynamicCast<PlainText>();
0409     if (!plainText.isNull())
0410         return plainText->text();
0411     else {
0412         const QSharedPointer<VerbatimText> verbatimText = valueItem.dynamicCast<VerbatimText>();
0413         if (!verbatimText.isNull())
0414             return verbatimText->text();
0415         else {
0416             const QSharedPointer<MacroKey> macroKey = valueItem.dynamicCast<MacroKey>();
0417             if (!macroKey.isNull())
0418                 return macroKey->text();
0419             else {
0420                 // TODO
0421                 return QString();
0422             }
0423         }
0424     }
0425 }
0426 
0427 QString BibliographyModel::beautifyLaTeX(const QString &input) {
0428     QString output = input;
0429     static const QStringList toBeRemoved {QStringLiteral("\\textsuperscript{"), QStringLiteral("\\}"), QStringLiteral("\\{"), QStringLiteral("}"), QStringLiteral("{")};
0430     for (QStringList::ConstIterator it = toBeRemoved.constBegin(); it != toBeRemoved.constEnd(); ++it)
0431         output = output.remove(*it);
0432 
0433     return output;
0434 }