1 /***************************************************************************
2 * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, see <https://www.gnu.org/licenses/>. *
16 ***************************************************************************/
17
18 #include "collection.h"
19
20 #include <QHash>
21 #include <QQueue>
22 #include <QVector>
23 #include <QNetworkReply>
24 #include <QXmlStreamReader>
25 #include <QTimer>
26
27 #include <KLocalizedString>
28
29 #include "api.h"
30 #include "internalnetworkaccessmanager.h"
31 #include "logging_networking.h"
32
33 using namespace Zotero;
34
35 class Zotero::Collection::Private
36 {
37 private:
38 Zotero::Collection *p;
39
40 public:
41 QSharedPointer<Zotero::API> api;
42
43 static const QString top;
44
Private(QSharedPointer<Zotero::API> a,Zotero::Collection * parent)45 Private(QSharedPointer<Zotero::API> a, Zotero::Collection *parent)
46 : p(parent), api(a) {
47 initialized = false;
48 busy = false;
49 }
50
51 bool initialized, busy;
52
53 QQueue<QString> downloadQueue;
54
55 QHash<QString, QString> collectionToLabel;
56 QHash<QString, QString> collectionToParent;
57 QHash<QString, QVector<QString> > collectionToChildren;
58
requestZoteroUrl(const QUrl & url)59 QNetworkReply *requestZoteroUrl(const QUrl &url) {
60 busy = true;
61 QUrl internalUrl = url;
62 api->addLimitToUrl(internalUrl);
63 QNetworkRequest request = api->request(internalUrl);
64 QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request);
65 connect(reply, &QNetworkReply::finished, p, &Zotero::Collection::finishedFetchingCollection);
66 return reply;
67 }
68
runNextInDownloadQueue()69 void runNextInDownloadQueue() {
70 if (!downloadQueue.isEmpty()) {
71 const QString head = downloadQueue.dequeue();
72 QUrl url = api->baseUrl();
73 url = url.adjusted(QUrl::StripTrailingSlash);
74 url.setPath(url.path() + QString(QStringLiteral("/collections/%1/collections")).arg(head));
75 if (api->inBackoffMode())
76 /// If Zotero asked to 'back off', wait until this period is over before issuing the next request
77 QTimer::singleShot((api->backoffSecondsLeft() + 1) * 1000, p, [ = ]() {
78 requestZoteroUrl(url);
79 });
80 else
81 requestZoteroUrl(url);
82 } else {
83 initialized = true;
84 p->emitFinishedLoading();
85 }
86 }
87 };
88
89 const QString Zotero::Collection::Private::top = QStringLiteral("top");
90
Collection(QSharedPointer<Zotero::API> api,QObject * parent)91 Collection::Collection(QSharedPointer<Zotero::API> api, QObject *parent)
92 : QObject(parent), d(new Zotero::Collection::Private(api, this))
93 {
94 d->collectionToLabel[Private::top] = i18n("Library");
95
96 QUrl url = api->baseUrl();
97 url = url.adjusted(QUrl::StripTrailingSlash);
98 url.setPath(url.path() + QStringLiteral("/collections/top"));
99 if (api->inBackoffMode())
100 /// If Zotero asked to 'back off', wait until this period is over before issuing the next request
101 QTimer::singleShot((api->backoffSecondsLeft() + 1) * 1000, this, [ = ]() {
102 d->requestZoteroUrl(url);
103 });
104 else
105 d->requestZoteroUrl(url);
106 }
107
~Collection()108 Collection::~Collection()
109 {
110 delete d;
111 }
112
initialized() const113 bool Collection::initialized() const
114 {
115 return d->initialized;
116 }
117
busy() const118 bool Collection::busy() const
119 {
120 return d->busy;
121 }
122
collectionLabel(const QString & collectionId) const123 QString Collection::collectionLabel(const QString &collectionId) const
124 {
125 if (!d->initialized) return QString();
126
127 return d->collectionToLabel[collectionId];
128 }
129
collectionParent(const QString & collectionId) const130 QString Collection::collectionParent(const QString &collectionId) const
131 {
132 if (!d->initialized) return QString();
133
134 return d->collectionToParent[collectionId];
135 }
136
collectionChildren(const QString & collectionId) const137 QVector<QString> Collection::collectionChildren(const QString &collectionId) const
138 {
139 if (!d->initialized) return QVector<QString>();
140
141 return QVector<QString>(d->collectionToChildren[collectionId]);
142 }
143
collectionNumericId(const QString & collectionId) const144 uint Collection::collectionNumericId(const QString &collectionId) const
145 {
146 if (!d->initialized) return 0;
147
148 if (collectionId == Private::top) /// root node
149 return 0;
150
151 return qHash(collectionId);
152 }
153
collectionFromNumericId(uint numericId) const154 QString Collection::collectionFromNumericId(uint numericId) const
155 {
156 if (numericId == 0) /// root node
157 return Private::top;
158
159 // TODO make those resolutions more efficient
160 const QList<QString> keys = d->collectionToLabel.keys();
161 for (const QString &key : keys) {
162 if (numericId == qHash(key))
163 return key;
164 }
165 return QString();
166 }
167
finishedFetchingCollection()168 void Collection::finishedFetchingCollection()
169 {
170 QNetworkReply *reply = static_cast<QNetworkReply *>(sender());
171 QString parentId = Private::top;
172
173 if (reply->hasRawHeader("Backoff")) {
174 bool ok = false;
175 int time = QString::fromLatin1(reply->rawHeader("Backoff").constData()).toInt(&ok);
176 if (!ok) time = 10; ///< parsing argument of raw header 'Backoff' failed? 10 seconds is fallback
177 d->api->startBackoff(time);
178 } else if (reply->hasRawHeader("Retry-After")) {
179 bool ok = false;
180 int time = QString::fromLatin1(reply->rawHeader("Retry-After").constData()).toInt(&ok);
181 if (!ok) time = 10; ///< parsing argument of raw header 'Retry-After' failed? 10 seconds is fallback
182 d->api->startBackoff(time);
183 }
184
185 if (reply->error() == QNetworkReply::NoError) {
186 QString nextPage;
187 QXmlStreamReader xmlReader(reply);
188 while (!xmlReader.atEnd() && !xmlReader.hasError()) {
189 const QXmlStreamReader::TokenType tt = xmlReader.readNext();
190 if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("title")) {
191 /// Not perfect: guess author name from collection's title
192 const QStringList titleFragments = xmlReader.readElementText(QXmlStreamReader::IncludeChildElements).split(QStringLiteral(" / "));
193 if (titleFragments.count() == 3)
194 d->collectionToLabel[Private::top] = i18n("%1's Library", titleFragments[1]);
195 } else if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("entry")) {
196 QString title, key;
197 while (!xmlReader.atEnd() && !xmlReader.hasError()) {
198 const QXmlStreamReader::TokenType tt = xmlReader.readNext();
199 if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("title"))
200 title = xmlReader.readElementText(QXmlStreamReader::IncludeChildElements);
201 else if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("key"))
202 key = xmlReader.readElementText(QXmlStreamReader::IncludeChildElements);
203 else if (tt == QXmlStreamReader::EndElement && xmlReader.name() == QStringLiteral("entry"))
204 break;
205 }
206
207 if (!key.isEmpty() && !title.isEmpty()) {
208 d->downloadQueue.enqueue(key);
209 d->collectionToLabel.insert(key, title);
210 d->collectionToParent.insert(key, parentId);
211 QVector<QString> vec = d->collectionToChildren[parentId];
212 vec.append(key);
213 d->collectionToChildren[parentId] = vec;
214 }
215 } else if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("link")) {
216 const QXmlStreamAttributes attrs = xmlReader.attributes();
217 if (attrs.hasAttribute(QStringLiteral("rel")) && attrs.hasAttribute(QStringLiteral("href")) && attrs.value(QStringLiteral("rel")) == QStringLiteral("next"))
218 nextPage = attrs.value(QStringLiteral("href")).toString();
219 else if (attrs.hasAttribute(QStringLiteral("rel")) && attrs.hasAttribute(QStringLiteral("href")) && attrs.value(QStringLiteral("rel")) == QStringLiteral("self")) {
220 const QString text = attrs.value(QStringLiteral("href")).toString();
221 const int p1 = text.indexOf(QStringLiteral("/collections/"));
222 const int p2 = text.indexOf(QStringLiteral("/"), p1 + 14);
223 if (p1 > 0 && p2 > p1 + 14)
224 parentId = text.mid(p1 + 13, p2 - p1 - 13);
225 }
226 } else if (tt == QXmlStreamReader::EndElement && xmlReader.name() == QStringLiteral("feed"))
227 break;
228 }
229
230 if (!nextPage.isEmpty()) {
231 if (d->api->inBackoffMode())
232 /// If Zotero asked to 'back off', wait until this period is over before issuing the next request
233 QTimer::singleShot((d->api->backoffSecondsLeft() + 1) * 1000, this, [ = ]() {
234 d->requestZoteroUrl(nextPage);
235 });
236 else
237 d->requestZoteroUrl(nextPage);
238 } else
239 d->runNextInDownloadQueue();
240 } else {
241 qCWarning(LOG_KBIBTEX_NETWORKING) << reply->errorString(); ///< something went wrong
242 d->initialized = false;
243 emitFinishedLoading();
244 }
245 }
246
emitFinishedLoading()247 void Collection::emitFinishedLoading()
248 {
249 d->busy = false;
250 emit finishedLoading();
251 }
252