1/*
2 Copyright (c) 2006 - 2007 Volker Krause <vkrause@kde.org>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "collectionfetchjob.h"
21
22#include "imapparser_p.h"
23#include "job_p.h"
24#include "protocol_p.h"
25#include "protocolhelper_p.h"
26#include "entity_p.h"
27#include "collectionfetchscope.h"
28#include "collectionutils_p.h"
29
30#include <kdebug.h>
31#include <KLocalizedString>
32
33#include <QtCore/QHash>
34#include <QtCore/QStringList>
35#include <QtCore/QTimer>
36
37using namespace Akonadi;
38
39class Akonadi::CollectionFetchJobPrivate : public JobPrivate
40{
41public:
42 CollectionFetchJobPrivate(CollectionFetchJob *parent)
43 : JobPrivate(parent)
44 , mEmitTimer(0)
45 , mBasePrefetch(false)
46 {
47
48 }
49
50 void init()
51 {
52 mEmitTimer = new QTimer(q_ptr);
53 mEmitTimer->setSingleShot(true);
54 mEmitTimer->setInterval(100);
55 q_ptr->connect(mEmitTimer, SIGNAL(timeout()), q_ptr, SLOT(timeout()));
56 }
57
58 Q_DECLARE_PUBLIC(CollectionFetchJob)
59
60 CollectionFetchJob::Type mType;
61 Collection mBase;
62 Collection::List mBaseList;
63 Collection::List mCollections;
64 CollectionFetchScope mScope;
65 Collection::List mPendingCollections;
66 QTimer *mEmitTimer;
67 bool mBasePrefetch;
68 Collection::List mPrefetchList;
69
70 void aboutToFinish()
71 {
72 timeout();
73 }
74
75 void timeout()
76 {
77 Q_Q(CollectionFetchJob);
78
79 mEmitTimer->stop(); // in case we are called by result()
80 if (!mPendingCollections.isEmpty()) {
81 if (!q->error()) {
82 emit q->collectionsReceived(mPendingCollections);
83 }
84 mPendingCollections.clear();
85 }
86 }
87
88 void subJobCollectionReceived(const Akonadi::Collection::List &collections)
89 {
90 mPendingCollections += collections;
91 if (!mEmitTimer->isActive()) {
92 mEmitTimer->start();
93 }
94 }
95
96 QString jobDebuggingString() const
97 {
98 if (mBase.isValid()) {
99 return QString::fromLatin1("Collection Id %1").arg(mBase.id());
100 } else if (CollectionUtils::hasValidHierarchicalRID(mBase)) {
101 return QString::fromUtf8(QByteArray(QByteArray("(") + ProtocolHelper::hierarchicalRidToByteArray(mBase) + QByteArray(")")));
102 } else {
103 return QString::fromLatin1("Collection RemoteId %1").arg(mBase.remoteId());
104 }
105 }
106};
107
108CollectionFetchJob::CollectionFetchJob(const Collection &collection, Type type, QObject *parent)
109 : Job(new CollectionFetchJobPrivate(this), parent)
110{
111 Q_D(CollectionFetchJob);
112 d->init();
113
114 d->mBase = collection;
115 d->mType = type;
116}
117
118CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, QObject *parent)
119 : Job(new CollectionFetchJobPrivate(this), parent)
120{
121 Q_D(CollectionFetchJob);
122 d->init();
123
124 Q_ASSERT(!cols.isEmpty());
125 if (cols.size() == 1) {
126 d->mBase = cols.first();
127 } else {
128 d->mBaseList = cols;
129 }
130 d->mType = CollectionFetchJob::Base;
131}
132
133CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, Type type, QObject *parent)
134 : Job(new CollectionFetchJobPrivate(this), parent)
135{
136 Q_D(CollectionFetchJob);
137 d->init();
138
139 Q_ASSERT(!cols.isEmpty());
140 if (cols.size() == 1) {
141 d->mBase = cols.first();
142 } else {
143 d->mBaseList = cols;
144 }
145 d->mType = type;
146}
147
148CollectionFetchJob::CollectionFetchJob(const QList<Collection::Id> &cols, Type type, QObject *parent)
149 : Job(new CollectionFetchJobPrivate(this), parent)
150{
151 Q_D(CollectionFetchJob);
152 d->init();
153
154 Q_ASSERT(!cols.isEmpty());
155 if (cols.size() == 1) {
156 d->mBase = Collection(cols.first());
157 } else {
158 foreach (Collection::Id id, cols) {
159 d->mBaseList.append(Collection(id));
160 }
161 }
162 d->mType = type;
163}
164
165CollectionFetchJob::~CollectionFetchJob()
166{
167}
168
169Akonadi::Collection::List CollectionFetchJob::collections() const
170{
171 Q_D(const CollectionFetchJob);
172
173 return d->mCollections;
174}
175
176void CollectionFetchJob::doStart()
177{
178 Q_D(CollectionFetchJob);
179
180 if (!d->mBaseList.isEmpty()) {
181 if (d->mType == Recursive) {
182 // Because doStart starts several subjobs and @p cols could contain descendants of
183 // other elements in the list, if type is Recusrive, we could end up with duplicates in the result.
184 // To fix this we require an initial fetch of @p cols with Base and RetrieveAncestors,
185 // Iterate over that result removing intersections and then perform the Recursive fetch on
186 // the remainder.
187 d->mBasePrefetch = true;
188 // No need to connect to the collectionsReceived signal here. This job is internal. The
189 // result needs to be filtered through filterDescendants before it is useful.
190 new CollectionFetchJob(d->mBaseList, NonOverlappingRoots, this);
191 } else if (d->mType == NonOverlappingRoots) {
192 foreach (const Collection &col, d->mBaseList) {
193 // No need to connect to the collectionsReceived signal here. This job is internal. The (aggregated)
194 // result needs to be filtered through filterDescendants before it is useful.
195 CollectionFetchJob *subJob = new CollectionFetchJob(col, Base, this);
196 subJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
197 }
198 } else {
199 foreach (const Collection &col, d->mBaseList) {
200 CollectionFetchJob *subJob = new CollectionFetchJob(col, d->mType, this);
201 connect(subJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(subJobCollectionReceived(Akonadi::Collection::List)));
202 subJob->setFetchScope(fetchScope());
203 }
204 }
205 return;
206 }
207
208 if (!d->mBase.isValid() && d->mBase.remoteId().isEmpty()) {
209 setError(Unknown);
210 setErrorText(i18n("Invalid collection given."));
211 emitResult();
212 return;
213 }
214
215 QByteArray command = d->newTag();
216 if (!d->mBase.isValid()) {
217 if (CollectionUtils::hasValidHierarchicalRID(d->mBase)) {
218 command += " HRID";
219 } else {
220 command += " " AKONADI_CMD_RID;
221 }
222 }
223 command += " LIST ";
224
225 if (d->mBase.isValid()) {
226 command += QByteArray::number(d->mBase.id());
227 } else if (CollectionUtils::hasValidHierarchicalRID(d->mBase)) {
228 command += '(' + ProtocolHelper::hierarchicalRidToByteArray(d->mBase) + ')';
229 } else {
230 command += ImapParser::quote(d->mBase.remoteId().toUtf8());
231 }
232
233 command += ' ';
234 switch (d->mType) {
235 case Base:
236 command += "0 (";
237 break;
238 case FirstLevel:
239 command += "1 (";
240 break;
241 case Recursive:
242 command += "INF (";
243 break;
244 default:
245 Q_ASSERT(false);
246 }
247
248 QList<QByteArray> filter;
249 if (!d->mScope.resource().isEmpty()) {
250 filter.append("RESOURCE");
251 // FIXME: Does this need to be quoted??
252 filter.append(d->mScope.resource().toUtf8());
253 }
254
255 if (!d->mScope.contentMimeTypes().isEmpty()) {
256 filter.append("MIMETYPE");
257 QList<QByteArray> mts;
258 foreach (const QString &mt, d->mScope.contentMimeTypes()) {
259 // FIXME: Does this need to be quoted??
260 mts.append(mt.toUtf8());
261 }
262 filter.append('(' + ImapParser::join(mts, " ") + ')');
263 }
264
265 switch (d->mScope.listFilter()) {
266 case CollectionFetchScope::Display:
267 filter.append("DISPLAY TRUE");
268 break;
269 case CollectionFetchScope::Sync:
270 filter.append("SYNC TRUE");
271 break;
272 case CollectionFetchScope::Index:
273 filter.append("INDEX TRUE");
274 break;
275 case CollectionFetchScope::Enabled:
276 filter.append("ENABLED TRUE");
277 break;
278 case CollectionFetchScope::NoFilter:
279 break;
280 default:
281 Q_ASSERT(false);
282 }
283
284 QList<QByteArray> options;
285 if (d->mScope.includeStatistics()) {
286 options.append("STATISTICS");
287 options.append("true");
288 }
289 if (d->mScope.ancestorRetrieval() != CollectionFetchScope::None) {
290 options.append("ANCESTORS");
291 switch (d->mScope.ancestorRetrieval()) {
292 case CollectionFetchScope::None:
293 options.append("0");
294 break;
295 case CollectionFetchScope::Parent:
296 options.append("1");
297 break;
298 case CollectionFetchScope::All:
299 options.append("INF");
300 break;
301 default:
302 Q_ASSERT(false);
303 }
304 }
305
306 command += ImapParser::join(filter, " ") + ") (" + ImapParser::join(options, " ") + ")\n";
307 d->writeData(command);
308}
309
310void CollectionFetchJob::doHandleResponse(const QByteArray &tag, const QByteArray &data)
311{
312 Q_D(CollectionFetchJob);
313
314 if (d->mBasePrefetch || d->mType == NonOverlappingRoots) {
315 return;
316 }
317
318 if (tag == "*") {
319 Collection collection;
320 ProtocolHelper::parseCollection(data, collection);
321 if (!collection.isValid()) {
322 return;
323 }
324
325 collection.d_ptr->resetChangeLog();
326 d->mCollections.append(collection);
327 d->mPendingCollections.append(collection);
328 if (!d->mEmitTimer->isActive()) {
329 d->mEmitTimer->start();
330 }
331 return;
332 }
333 kDebug() << "Unhandled server response" << tag << data;
334}
335
336void CollectionFetchJob::setResource(const QString &resource)
337{
338 Q_D(CollectionFetchJob);
339
340 d->mScope.setResource(resource);
341}
342
343static Collection::List filterDescendants(const Collection::List &list)
344{
345 Collection::List result;
346
347 QVector<QList<Collection::Id> > ids;
348 foreach (const Collection &collection, list) {
349 QList<Collection::Id> ancestors;
350 Collection parent = collection.parentCollection();
351 ancestors << parent.id();
352 if (parent != Collection::root()) {
353 while (parent.parentCollection() != Collection::root()) {
354 parent = parent.parentCollection();
355 QList<Collection::Id>::iterator i = qLowerBound(ancestors.begin(), ancestors.end(), parent.id());
356 ancestors.insert(i, parent.id());
357 }
358 }
359 ids << ancestors;
360 }
361
362 QSet<Collection::Id> excludeList;
363 foreach (const Collection &collection, list) {
364 int i = 0;
365 foreach (const QList<Collection::Id> &ancestors, ids) {
366 if (qBinaryFind(ancestors, collection.id()) != ancestors.end()) {
367 excludeList.insert(list.at(i).id());
368 }
369 ++i;
370 }
371 }
372
373 foreach (const Collection &collection, list) {
374 if (!excludeList.contains(collection.id())) {
375 result.append(collection);
376 }
377 }
378
379 return result;
380}
381
382void CollectionFetchJob::slotResult(KJob *job)
383{
384 Q_D(CollectionFetchJob);
385
386 CollectionFetchJob *list = qobject_cast<CollectionFetchJob *>(job);
387 Q_ASSERT(job);
388 if (d->mBasePrefetch) {
389 d->mBasePrefetch = false;
390 const Collection::List roots = list->collections();
391 Job::slotResult(job);
392 Q_ASSERT(!hasSubjobs());
393 if (!job->error()) {
394 foreach (const Collection &col, roots) {
395 CollectionFetchJob *subJob = new CollectionFetchJob(col, d->mType, this);
396 connect(subJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(subJobCollectionReceived(Akonadi::Collection::List)));
397 subJob->setFetchScope(fetchScope());
398 }
399 }
400 // No result yet.
401 } else if (d->mType == NonOverlappingRoots) {
402 d->mPrefetchList += list->collections();
403 Job::slotResult(job);
404 if (!job->error() && !hasSubjobs()) {
405 const Collection::List result = filterDescendants(d->mPrefetchList);
406 d->mPendingCollections += result;
407 d->mCollections = result;
408 d->delayedEmitResult();
409 }
410 } else {
411 d->mCollections += list->collections();
412 Job::slotResult(job);
413 if (!job->error() && !hasSubjobs()) {
414 d->delayedEmitResult();
415 }
416 }
417}
418
419void CollectionFetchJob::includeUnsubscribed(bool include)
420{
421 Q_D(CollectionFetchJob);
422
423 d->mScope.setIncludeUnsubscribed(include);
424}
425
426void CollectionFetchJob::includeStatistics(bool include)
427{
428 Q_D(CollectionFetchJob);
429
430 d->mScope.setIncludeStatistics(include);
431}
432
433void CollectionFetchJob::setFetchScope(const CollectionFetchScope &scope)
434{
435 Q_D(CollectionFetchJob);
436 d->mScope = scope;
437}
438
439CollectionFetchScope &CollectionFetchJob::fetchScope()
440{
441 Q_D(CollectionFetchJob);
442 return d->mScope;
443}
444
445#include "moc_collectionfetchjob.cpp"
446