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 | |
37 | using namespace Akonadi; |
38 | |
39 | class Akonadi::CollectionFetchJobPrivate : public JobPrivate |
40 | { |
41 | public: |
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 | |
108 | CollectionFetchJob::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 | |
118 | CollectionFetchJob::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 | |
133 | CollectionFetchJob::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 | |
148 | CollectionFetchJob::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 | |
165 | CollectionFetchJob::~CollectionFetchJob() |
166 | { |
167 | } |
168 | |
169 | Akonadi::Collection::List CollectionFetchJob::collections() const |
170 | { |
171 | Q_D(const CollectionFetchJob); |
172 | |
173 | return d->mCollections; |
174 | } |
175 | |
176 | void 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 | |
310 | void 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 | |
336 | void CollectionFetchJob::setResource(const QString &resource) |
337 | { |
338 | Q_D(CollectionFetchJob); |
339 | |
340 | d->mScope.setResource(resource); |
341 | } |
342 | |
343 | static 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 | |
382 | void 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 | |
419 | void CollectionFetchJob::includeUnsubscribed(bool include) |
420 | { |
421 | Q_D(CollectionFetchJob); |
422 | |
423 | d->mScope.setIncludeUnsubscribed(include); |
424 | } |
425 | |
426 | void CollectionFetchJob::includeStatistics(bool include) |
427 | { |
428 | Q_D(CollectionFetchJob); |
429 | |
430 | d->mScope.setIncludeStatistics(include); |
431 | } |
432 | |
433 | void CollectionFetchJob::setFetchScope(const CollectionFetchScope &scope) |
434 | { |
435 | Q_D(CollectionFetchJob); |
436 | d->mScope = scope; |
437 | } |
438 | |
439 | CollectionFetchScope &CollectionFetchJob::fetchScope() |
440 | { |
441 | Q_D(CollectionFetchJob); |
442 | return d->mScope; |
443 | } |
444 | |
445 | #include "moc_collectionfetchjob.cpp" |
446 | |