1 | /* |
2 | Copyright (c) 2007, 2009 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 "collectionsync_p.h" |
21 | #include "collection.h" |
22 | |
23 | #include "collectioncreatejob.h" |
24 | #include "collectiondeletejob.h" |
25 | #include "collectionfetchjob.h" |
26 | #include "collectionmodifyjob.h" |
27 | #include "collectionfetchscope.h" |
28 | #include "collectionmovejob.h" |
29 | |
30 | #include "cachepolicy.h" |
31 | |
32 | #include <kdebug.h> |
33 | #include <KLocalizedString> |
34 | #include <QtCore/QVariant> |
35 | |
36 | using namespace Akonadi; |
37 | |
38 | struct RemoteNode; |
39 | |
40 | /** |
41 | LocalNode is used to build a tree structure of all our locally existing collections. |
42 | */ |
43 | struct LocalNode |
44 | { |
45 | LocalNode(const Collection &col) |
46 | : collection(col) |
47 | , processed(false) |
48 | {} |
49 | |
50 | ~LocalNode() |
51 | { |
52 | qDeleteAll(childNodes); |
53 | qDeleteAll(pendingRemoteNodes); |
54 | } |
55 | |
56 | Collection collection; |
57 | QList<LocalNode *> childNodes; |
58 | QHash<QString, LocalNode *> childRidMap; |
59 | /** When using hierarchical RIDs we attach a list of not yet processable remote nodes to |
60 | the closest already existing local ancestor node. They will be re-evaluated once a new |
61 | child node is added. */ |
62 | QList<RemoteNode *> pendingRemoteNodes; |
63 | bool processed; |
64 | }; |
65 | |
66 | Q_DECLARE_METATYPE(LocalNode *) |
67 | static const char LOCAL_NODE[] = "LocalNode" ; |
68 | |
69 | /** |
70 | RemoteNode is used as a container for remote collections which typically don't have a UID set |
71 | and thus cannot easily be compared or put into maps etc. |
72 | */ |
73 | struct RemoteNode |
74 | { |
75 | RemoteNode(const Collection &col) |
76 | : collection(col) |
77 | {} |
78 | |
79 | Collection collection; |
80 | }; |
81 | |
82 | Q_DECLARE_METATYPE(RemoteNode *) |
83 | static const char REMOTE_NODE[] = "RemoteNode" ; |
84 | |
85 | static const char CONTENTMIMETYPES[] = "CONTENTMIMETYPES" ; |
86 | |
87 | /** |
88 | * @internal |
89 | */ |
90 | class CollectionSync::Private |
91 | { |
92 | public: |
93 | Private(CollectionSync *parent) |
94 | : q(parent) |
95 | , pendingJobs(0) |
96 | , progress(0) |
97 | , localRoot(0) |
98 | , currentTransaction(0) |
99 | , knownLocalCollections(0) |
100 | , incremental(false) |
101 | , streaming(false) |
102 | , hierarchicalRIDs(false) |
103 | , localListDone(false) |
104 | , deliveryDone(false) |
105 | { |
106 | } |
107 | |
108 | ~Private() |
109 | { |
110 | delete localRoot; |
111 | qDeleteAll(rootRemoteNodes); |
112 | } |
113 | |
114 | /** Utility method to reset the node tree. */ |
115 | void resetNodeTree() |
116 | { |
117 | delete localRoot; |
118 | localRoot = new LocalNode(Collection::root()); |
119 | localRoot->processed = true; // never try to delete that one |
120 | if (currentTransaction) { |
121 | // we are running the update transaction, initialize pending remote nodes |
122 | localRoot->pendingRemoteNodes.swap(rootRemoteNodes); |
123 | } |
124 | |
125 | localUidMap.clear(); |
126 | localRidMap.clear(); |
127 | localUidMap.insert(localRoot->collection.id(), localRoot); |
128 | if (!hierarchicalRIDs) { |
129 | localRidMap.insert(QString(), localRoot); |
130 | } |
131 | } |
132 | |
133 | /** Create a local node from the given local collection and integrate it into the local tree structure. */ |
134 | LocalNode *createLocalNode(const Collection &col) |
135 | { |
136 | LocalNode *node = new LocalNode(col); |
137 | Q_ASSERT(!localUidMap.contains(col.id())); |
138 | localUidMap.insert(node->collection.id(), node); |
139 | if (!hierarchicalRIDs && !col.remoteId().isEmpty()) { |
140 | localRidMap.insert(node->collection.remoteId(), node); |
141 | } |
142 | |
143 | // add already existing children |
144 | if (localPendingCollections.contains(col.id())) { |
145 | QVector<Collection::Id> childIds = localPendingCollections.take(col.id()); |
146 | foreach (Collection::Id childId, childIds) { |
147 | Q_ASSERT(localUidMap.contains(childId)); |
148 | LocalNode *childNode = localUidMap.value(childId); |
149 | node->childNodes.append(childNode); |
150 | if (!childNode->collection.remoteId().isEmpty()) { |
151 | node->childRidMap.insert(childNode->collection.remoteId(), childNode); |
152 | } |
153 | } |
154 | } |
155 | |
156 | // set our parent and add ourselves as child |
157 | if (localUidMap.contains(col.parentCollection().id())) { |
158 | LocalNode *parentNode = localUidMap.value(col.parentCollection().id()); |
159 | parentNode->childNodes.append(node); |
160 | if (!node->collection.remoteId().isEmpty()) { |
161 | parentNode->childRidMap.insert(node->collection.remoteId(), node); |
162 | } |
163 | } else { |
164 | localPendingCollections[col.parentCollection().id()].append(col.id()); |
165 | } |
166 | |
167 | return node; |
168 | } |
169 | |
170 | /** Same as createLocalNode() for remote collections. */ |
171 | void createRemoteNode(const Collection &col) |
172 | { |
173 | if (col.remoteId().isEmpty()) { |
174 | kWarning() << "Collection '" << col.name() << "' does not have a remote identifier - skipping" ; |
175 | return; |
176 | } |
177 | RemoteNode *node = new RemoteNode(col); |
178 | rootRemoteNodes.append(node); |
179 | } |
180 | |
181 | /** Create local nodes as we receive the local listing from the Akonadi server. */ |
182 | void localCollectionsReceived(const Akonadi::Collection::List &localCols) |
183 | { |
184 | foreach (const Collection &c, localCols) { |
185 | createLocalNode(c); |
186 | knownLocalCollections++; |
187 | } |
188 | } |
189 | |
190 | /** Once the local collection listing finished we can continue with the interesting stuff. */ |
191 | void localCollectionFetchResult(KJob *job) |
192 | { |
193 | if (job->error()) { |
194 | return; // handled by the base class |
195 | } |
196 | |
197 | // safety check: the local tree has to be connected |
198 | if (!localPendingCollections.isEmpty()) { |
199 | q->setError(Unknown); |
200 | q->setErrorText(i18n("Inconsistent local collection tree detected." )); |
201 | q->emitResult(); |
202 | return; |
203 | } |
204 | |
205 | localListDone = true; |
206 | execute(); |
207 | } |
208 | |
209 | /** |
210 | * Find a child node with matching collection name. |
211 | * @note This is used as a fallback if the resource lost the RID update somehow. |
212 | * This can be used because the Akonadi server enforces unique child collection names inside the hierarchy |
213 | */ |
214 | LocalNode *findLocalChildNodeByName(LocalNode *localParentNode, const QString &name) const |
215 | { |
216 | if (name.isEmpty()) { // shouldn't happen... |
217 | return 0; |
218 | } |
219 | |
220 | if (localParentNode == localRoot) { // possibly non-unique names on top-level |
221 | return 0; |
222 | } |
223 | |
224 | foreach (LocalNode *childNode, localParentNode->childNodes) { |
225 | // the restriction on empty RIDs can possibly removed, but for now I only understand the implication for this case |
226 | if (childNode->collection.name() == name && childNode->collection.remoteId().isEmpty()) { |
227 | return childNode; |
228 | } |
229 | } |
230 | return 0; |
231 | } |
232 | |
233 | /** |
234 | Find the local node that matches the given remote collection, returns 0 |
235 | if that doesn't exist (yet). |
236 | */ |
237 | LocalNode *findMatchingLocalNode(const Collection &collection) const |
238 | { |
239 | if (!hierarchicalRIDs) { |
240 | if (localRidMap.contains(collection.remoteId())) { |
241 | return localRidMap.value(collection.remoteId()); |
242 | } |
243 | return 0; |
244 | } else { |
245 | if (collection.id() == Collection::root().id() || collection.remoteId() == Collection::root().remoteId()) { |
246 | return localRoot; |
247 | } |
248 | LocalNode *localParent = 0; |
249 | if (collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty()) { |
250 | kWarning() << "Remote collection without valid parent found: " << collection; |
251 | return 0; |
252 | } |
253 | if (collection.parentCollection().id() == Collection::root().id() || collection.parentCollection().remoteId() == Collection::root().remoteId()) { |
254 | localParent = localRoot; |
255 | } else { |
256 | localParent = findMatchingLocalNode(collection.parentCollection()); |
257 | } |
258 | |
259 | if (localParent) { |
260 | if (localParent->childRidMap.contains(collection.remoteId())) { |
261 | return localParent->childRidMap.value(collection.remoteId()); |
262 | } |
263 | // check if we have a local folder with a matching name and no RID, if so let's use that one |
264 | // we would get an error if we don't do this anyway, as we'd try to create two sibling nodes with the same name |
265 | if (LocalNode *recoveredLocalNode = findLocalChildNodeByName(localParent, collection.name())) { |
266 | kDebug() << "Recovering collection with lost RID:" << collection << recoveredLocalNode->collection; |
267 | return recoveredLocalNode; |
268 | } |
269 | } |
270 | return 0; |
271 | } |
272 | } |
273 | |
274 | /** |
275 | Find the local node that is the nearest ancestor of the given remote collection |
276 | (when using hierarchical RIDs only, otherwise it's always the local root node). |
277 | Never returns 0. |
278 | */ |
279 | LocalNode *findBestLocalAncestor(const Collection &collection, bool *exactMatch = 0) |
280 | { |
281 | if (!hierarchicalRIDs) { |
282 | return localRoot; |
283 | } |
284 | if (collection == Collection::root()) { |
285 | if (exactMatch) { |
286 | *exactMatch = true; |
287 | } |
288 | return localRoot; |
289 | } |
290 | if (collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty()) { |
291 | kWarning() << "Remote collection without valid parent found: " << collection; |
292 | return 0; |
293 | } |
294 | bool parentIsExact = false; |
295 | LocalNode *localParent = findBestLocalAncestor(collection.parentCollection(), &parentIsExact); |
296 | if (!parentIsExact) { |
297 | if (exactMatch) { |
298 | *exactMatch = false; |
299 | } |
300 | return localParent; |
301 | } |
302 | if (localParent->childRidMap.contains(collection.remoteId())) { |
303 | if (exactMatch) { |
304 | *exactMatch = true; |
305 | } |
306 | return localParent->childRidMap.value(collection.remoteId()); |
307 | } |
308 | if (exactMatch) { |
309 | *exactMatch = false; |
310 | } |
311 | return localParent; |
312 | } |
313 | |
314 | /** |
315 | Checks if any of the remote nodes is not equal to the current local one. If so return true. |
316 | */ |
317 | bool checkPendingRemoteNodes() const |
318 | { |
319 | if (rootRemoteNodes.size() != knownLocalCollections) { |
320 | return true; |
321 | } |
322 | |
323 | foreach (RemoteNode *remoteNode, rootRemoteNodes) { |
324 | // every remote note should have a local node already |
325 | LocalNode *localNode = findMatchingLocalNode(remoteNode->collection); |
326 | if (localNode) { |
327 | if (checkLocalCollection(localNode, remoteNode)) { |
328 | return true; |
329 | } |
330 | } else { |
331 | return true; |
332 | } |
333 | } |
334 | return false; |
335 | } |
336 | |
337 | /** |
338 | Checks the pending remote nodes attached to the given local root node |
339 | to see if any of them can be processed by now. If not, they are moved to |
340 | the closest ancestor available. |
341 | */ |
342 | void processPendingRemoteNodes(LocalNode *_localRoot) |
343 | { |
344 | QList<RemoteNode *> pendingRemoteNodes(_localRoot->pendingRemoteNodes); |
345 | _localRoot->pendingRemoteNodes.clear(); |
346 | QHash<LocalNode *, QList<RemoteNode *> > pendingCreations; |
347 | foreach (RemoteNode *remoteNode, pendingRemoteNodes) { |
348 | // step 1: see if we have a matching local node already |
349 | LocalNode *localNode = findMatchingLocalNode(remoteNode->collection); |
350 | if (localNode) { |
351 | Q_ASSERT(!localNode->processed); |
352 | updateLocalCollection(localNode, remoteNode); |
353 | continue; |
354 | } |
355 | // step 2: check if we have the parent at least, then we can create it |
356 | localNode = findMatchingLocalNode(remoteNode->collection.parentCollection()); |
357 | if (localNode) { |
358 | pendingCreations[localNode].append(remoteNode); |
359 | continue; |
360 | } |
361 | // step 3: find the best matching ancestor and enqueue it for later processing |
362 | localNode = findBestLocalAncestor(remoteNode->collection); |
363 | if (!localNode) { |
364 | q->setError(Unknown); |
365 | q->setErrorText(i18n("Remote collection without root-terminated ancestor chain provided, resource is broken." )); |
366 | q->emitResult(); |
367 | return; |
368 | } |
369 | localNode->pendingRemoteNodes.append(remoteNode); |
370 | } |
371 | |
372 | // process the now possible collection creations |
373 | for (QHash<LocalNode *, QList<RemoteNode *> >::const_iterator it = pendingCreations.constBegin(); |
374 | it != pendingCreations.constEnd(); ++it) { |
375 | createLocalCollections(it.key(), it.value()); |
376 | } |
377 | } |
378 | |
379 | /** |
380 | Checks if the given localNode and remoteNode are different |
381 | */ |
382 | bool checkLocalCollection(LocalNode *localNode, RemoteNode *remoteNode) const |
383 | { |
384 | const Collection &localCollection = localNode->collection; |
385 | const Collection &remoteCollection = remoteNode->collection; |
386 | |
387 | if (!keepLocalChanges.contains(CONTENTMIMETYPES)) { |
388 | if (localCollection.contentMimeTypes().size() != remoteCollection.contentMimeTypes().size()) { |
389 | return true; |
390 | } else { |
391 | for (int i = 0; i < remoteCollection.contentMimeTypes().size(); i++) { |
392 | const QString &m = remoteCollection.contentMimeTypes().at(i); |
393 | if (!localCollection.contentMimeTypes().contains(m)) { |
394 | return true; |
395 | } |
396 | } |
397 | } |
398 | } |
399 | |
400 | if (localCollection.parentCollection().remoteId() != remoteCollection.parentCollection().remoteId()) { |
401 | return true; |
402 | } |
403 | if (localCollection.name() != remoteCollection.name()) { |
404 | return true; |
405 | } |
406 | if (localCollection.remoteId() != remoteCollection.remoteId()) { |
407 | return true; |
408 | } |
409 | if (localCollection.remoteRevision() != remoteCollection.remoteRevision()) { |
410 | return true; |
411 | } |
412 | if (!(localCollection.cachePolicy() == remoteCollection.cachePolicy())) { |
413 | return true; |
414 | } |
415 | |
416 | // CollectionModifyJob adds the remote attributes to the local collection |
417 | foreach (const Attribute *attr, remoteCollection.attributes()) { |
418 | const Attribute *localAttr = localCollection.attribute(attr->type()); |
419 | if (localAttr && keepLocalChanges.contains(attr->type())) { |
420 | continue; |
421 | } |
422 | // The attribute must both exist and have equal contents |
423 | if (!localAttr || localAttr->serialized() != attr->serialized()) { |
424 | return true; |
425 | } |
426 | } |
427 | |
428 | return false; |
429 | } |
430 | |
431 | /** |
432 | Performs a local update for the given node pair. |
433 | */ |
434 | void updateLocalCollection(LocalNode *localNode, RemoteNode *remoteNode) |
435 | { |
436 | Collection upd(remoteNode->collection); |
437 | Q_ASSERT(!upd.remoteId().isEmpty()); |
438 | Q_ASSERT(currentTransaction); |
439 | upd.setId(localNode->collection.id()); |
440 | if (keepLocalChanges.contains(CONTENTMIMETYPES)) { |
441 | upd.setContentMimeTypes(localNode->collection.contentMimeTypes()); |
442 | } |
443 | foreach (Attribute *remoteAttr, upd.attributes()) { |
444 | if (keepLocalChanges.contains(remoteAttr->type()) && localNode->collection.hasAttribute(remoteAttr->type())) { |
445 | //We don't want to overwrite the attribute changes with the defaults provided by the resource. |
446 | Attribute *localAttr = localNode->collection.attribute(remoteAttr->type()); |
447 | upd.removeAttribute(localAttr->type()); |
448 | upd.addAttribute(localAttr->clone()); |
449 | } |
450 | } |
451 | |
452 | { |
453 | // ### HACK to work around the implicit move attempts of CollectionModifyJob |
454 | // which we do explicitly below |
455 | Collection c(upd); |
456 | c.setParentCollection(localNode->collection.parentCollection()); |
457 | ++pendingJobs; |
458 | CollectionModifyJob *mod = new CollectionModifyJob(c, currentTransaction); |
459 | connect(mod, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*))); |
460 | } |
461 | |
462 | // detecting moves is only possible with global RIDs |
463 | if (!hierarchicalRIDs) { |
464 | LocalNode *oldParent = localUidMap.value(localNode->collection.parentCollection().id()); |
465 | LocalNode *newParent = findMatchingLocalNode(remoteNode->collection.parentCollection()); |
466 | // TODO: handle the newParent == 0 case correctly, ie. defer the move until the new |
467 | // local parent has been created |
468 | if (newParent && oldParent != newParent) { |
469 | ++pendingJobs; |
470 | CollectionMoveJob *move = new CollectionMoveJob(upd, newParent->collection, currentTransaction); |
471 | connect(move, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*))); |
472 | } |
473 | } |
474 | |
475 | localNode->processed = true; |
476 | delete remoteNode; |
477 | } |
478 | |
479 | void updateLocalCollectionResult(KJob *job) |
480 | { |
481 | --pendingJobs; |
482 | if (job->error()) { |
483 | return; // handled by the base class |
484 | } |
485 | if (qobject_cast<CollectionModifyJob *>(job)) { |
486 | ++progress; |
487 | } |
488 | checkDone(); |
489 | } |
490 | |
491 | /** |
492 | Creates local folders for the given local parent and remote nodes. |
493 | @todo group CollectionCreateJobs into a single one once it supports that |
494 | */ |
495 | void createLocalCollections(LocalNode *localParent, QList<RemoteNode *> remoteNodes) |
496 | { |
497 | foreach (RemoteNode *remoteNode, remoteNodes) { |
498 | ++pendingJobs; |
499 | Collection col(remoteNode->collection); |
500 | Q_ASSERT(!col.remoteId().isEmpty()); |
501 | col.setParentCollection(localParent->collection); |
502 | CollectionCreateJob *create = new CollectionCreateJob(col, currentTransaction); |
503 | create->setProperty(LOCAL_NODE, QVariant::fromValue(localParent)); |
504 | create->setProperty(REMOTE_NODE, QVariant::fromValue(remoteNode)); |
505 | connect(create, SIGNAL(result(KJob*)), q, SLOT(createLocalCollectionResult(KJob*))); |
506 | } |
507 | } |
508 | |
509 | void createLocalCollectionResult(KJob *job) |
510 | { |
511 | --pendingJobs; |
512 | if (job->error()) { |
513 | return; // handled by the base class |
514 | } |
515 | |
516 | const Collection newLocal = static_cast<CollectionCreateJob *>(job)->collection(); |
517 | LocalNode *localNode = createLocalNode(newLocal); |
518 | localNode->processed = true; |
519 | |
520 | LocalNode *localParent = job->property(LOCAL_NODE).value<LocalNode *>(); |
521 | Q_ASSERT(localParent->childNodes.contains(localNode)); |
522 | RemoteNode *remoteNode = job->property(REMOTE_NODE).value<RemoteNode *>(); |
523 | delete remoteNode; |
524 | ++progress; |
525 | |
526 | processPendingRemoteNodes(localParent); |
527 | if (!hierarchicalRIDs) { |
528 | processPendingRemoteNodes(localRoot); |
529 | } |
530 | |
531 | checkDone(); |
532 | } |
533 | |
534 | /** |
535 | Checks if the given local node has processed child nodes. |
536 | */ |
537 | bool hasProcessedChildren(LocalNode *localNode) const |
538 | { |
539 | if (localNode->processed) { |
540 | return true; |
541 | } |
542 | foreach (LocalNode *child, localNode->childNodes) { |
543 | if (hasProcessedChildren(child)) { |
544 | return true; |
545 | } |
546 | } |
547 | return false; |
548 | } |
549 | |
550 | /** |
551 | Find all local nodes that are not marked as processed and have no children that |
552 | are marked as processed. |
553 | */ |
554 | Collection::List findUnprocessedLocalCollections(LocalNode *localNode) const |
555 | { |
556 | Collection::List rv; |
557 | if (!localNode->processed) { |
558 | if (hasProcessedChildren(localNode)) { |
559 | kWarning() << "Found unprocessed local node with processed children, excluding from deletion" ; |
560 | kWarning() << localNode->collection; |
561 | return rv; |
562 | } |
563 | if (localNode->collection.remoteId().isEmpty()) { |
564 | kWarning() << "Found unprocessed local node without remoteId, excluding from deletion" ; |
565 | kWarning() << localNode->collection; |
566 | return rv; |
567 | } |
568 | rv.append(localNode->collection); |
569 | return rv; |
570 | } |
571 | |
572 | foreach (LocalNode *child, localNode->childNodes) { |
573 | rv.append(findUnprocessedLocalCollections(child)); |
574 | } |
575 | return rv; |
576 | } |
577 | |
578 | /** |
579 | Deletes unprocessed local nodes, in non-incremental mode. |
580 | */ |
581 | void deleteUnprocessedLocalNodes() |
582 | { |
583 | if (incremental) { |
584 | return; |
585 | } |
586 | const Collection::List cols = findUnprocessedLocalCollections(localRoot); |
587 | deleteLocalCollections(cols); |
588 | } |
589 | |
590 | /** |
591 | Deletes the given collection list. |
592 | @todo optimize delete job to support batch operations |
593 | */ |
594 | void deleteLocalCollections(const Collection::List &cols) |
595 | { |
596 | q->setTotalAmount(KJob::Bytes, q->totalAmount(KJob::Bytes) + cols.size()); |
597 | foreach (const Collection &col, cols) { |
598 | Q_ASSERT(!col.remoteId().isEmpty()); // empty RID -> stuff we haven't even written to the remote side yet |
599 | |
600 | ++pendingJobs; |
601 | Q_ASSERT(currentTransaction); |
602 | CollectionDeleteJob *job = new CollectionDeleteJob(col, currentTransaction); |
603 | connect(job, SIGNAL(result(KJob*)), q, SLOT(deleteLocalCollectionsResult(KJob*))); |
604 | |
605 | // It can happen that the groupware servers report us deleted collections |
606 | // twice, in this case this collection delete job will fail on the second try. |
607 | // To avoid a rollback of the complete transaction we gracefully allow the job |
608 | // to fail :) |
609 | currentTransaction->setIgnoreJobFailure(job); |
610 | } |
611 | } |
612 | |
613 | void deleteLocalCollectionsResult(KJob *) |
614 | { |
615 | --pendingJobs; |
616 | |
617 | ++progress; |
618 | checkDone(); |
619 | } |
620 | |
621 | /** |
622 | Check update necessity. |
623 | */ |
624 | void checkUpdateNecessity() |
625 | { |
626 | bool updateNeeded = checkPendingRemoteNodes(); |
627 | if (!updateNeeded) { |
628 | // We can end right now |
629 | q->emitResult(); |
630 | return; |
631 | } |
632 | |
633 | // Since there are differences with the remote collections we need to sync. Start a transaction here. |
634 | Q_ASSERT(!currentTransaction); |
635 | currentTransaction = new TransactionSequence(q); |
636 | currentTransaction->setAutomaticCommittingEnabled(false); |
637 | q->connect(currentTransaction, SIGNAL(result(KJob*)), SLOT(transactionSequenceResult(KJob*))); |
638 | |
639 | // Now that a transaction is started we need to fetch local collections again and do the update |
640 | q->doStart(); |
641 | } |
642 | |
643 | /** After the transaction has finished report we're done as well. */ |
644 | void transactionSequenceResult(KJob *job) |
645 | { |
646 | if (job->error()) { |
647 | return; // handled by the base class |
648 | } |
649 | |
650 | q->emitResult(); |
651 | } |
652 | |
653 | /** |
654 | Process what's currently available. |
655 | */ |
656 | void execute() |
657 | { |
658 | kDebug() << Q_FUNC_INFO << "localListDone: " << localListDone << " deliveryDone: " << deliveryDone; |
659 | if (!localListDone || !deliveryDone) { |
660 | return; |
661 | } |
662 | |
663 | // If a transaction is not started yet we are still checking if the update is |
664 | // actually needed. |
665 | if (!currentTransaction) { |
666 | checkUpdateNecessity(); |
667 | return; |
668 | } |
669 | |
670 | // Since the transaction is already running we need to execute the update. |
671 | processPendingRemoteNodes(localRoot); |
672 | |
673 | if (!incremental && deliveryDone) { |
674 | deleteUnprocessedLocalNodes(); |
675 | } |
676 | |
677 | if (!hierarchicalRIDs) { |
678 | deleteLocalCollections(removedRemoteCollections); |
679 | } else { |
680 | Collection::List localCols; |
681 | foreach (const Collection &c, removedRemoteCollections) { |
682 | LocalNode *node = findMatchingLocalNode(c); |
683 | if (node) { |
684 | localCols.append(node->collection); |
685 | } |
686 | } |
687 | deleteLocalCollections(localCols); |
688 | } |
689 | removedRemoteCollections.clear(); |
690 | |
691 | checkDone(); |
692 | } |
693 | |
694 | /** |
695 | Finds pending remote nodes, which at the end of the day should be an empty set. |
696 | */ |
697 | QList<RemoteNode *> findPendingRemoteNodes(LocalNode *localNode) |
698 | { |
699 | QList<RemoteNode *> rv; |
700 | rv.append(localNode->pendingRemoteNodes); |
701 | foreach (LocalNode *child, localNode->childNodes) { |
702 | rv.append(findPendingRemoteNodes(child)); |
703 | } |
704 | return rv; |
705 | } |
706 | |
707 | /** |
708 | Are we there yet?? |
709 | @todo progress reporting |
710 | */ |
711 | void checkDone() |
712 | { |
713 | q->setProcessedAmount(KJob::Bytes, progress); |
714 | |
715 | // still running jobs or not fully delivered local/remote state |
716 | if (!deliveryDone || pendingJobs > 0 || !localListDone) { |
717 | return; |
718 | } |
719 | |
720 | // safety check: there must be no pending remote nodes anymore |
721 | QList<RemoteNode *> orphans = findPendingRemoteNodes(localRoot); |
722 | if (!orphans.isEmpty()) { |
723 | q->setError(Unknown); |
724 | q->setErrorText(i18n("Found unresolved orphan collections" )); |
725 | foreach (RemoteNode *orphan, orphans) { |
726 | kDebug() << "found orphan collection:" << orphan->collection; |
727 | } |
728 | q->emitResult(); |
729 | return; |
730 | } |
731 | |
732 | kDebug() << Q_FUNC_INFO << "q->commit()" ; |
733 | Q_ASSERT(currentTransaction); |
734 | currentTransaction->commit(); |
735 | } |
736 | |
737 | CollectionSync *q; |
738 | |
739 | QString resourceId; |
740 | |
741 | int pendingJobs; |
742 | int progress; |
743 | |
744 | LocalNode *localRoot; |
745 | TransactionSequence *currentTransaction; |
746 | QHash<Collection::Id, LocalNode *> localUidMap; |
747 | QHash<QString, LocalNode *> localRidMap; |
748 | |
749 | // temporary during build-up of the local node tree, must be empty afterwards |
750 | QHash<Collection::Id, QVector<Collection::Id> > localPendingCollections; |
751 | |
752 | // removed remote collections in incremental mode |
753 | Collection::List removedRemoteCollections; |
754 | |
755 | // used to store the list of remote nodes passed by the user |
756 | QList<RemoteNode *> rootRemoteNodes; |
757 | |
758 | // keep track of the total number of local collections that are known |
759 | // only used during the preliminary check to see if updating is needed |
760 | int knownLocalCollections; |
761 | |
762 | bool incremental; |
763 | bool streaming; |
764 | bool hierarchicalRIDs; |
765 | |
766 | bool localListDone; |
767 | bool deliveryDone; |
768 | |
769 | // List of parts where local changes should not be overwritten |
770 | QSet<QByteArray> keepLocalChanges; |
771 | }; |
772 | |
773 | CollectionSync::CollectionSync(const QString &resourceId, QObject *parent) |
774 | : Job(parent) |
775 | , d(new Private(this)) |
776 | { |
777 | d->resourceId = resourceId; |
778 | setTotalAmount(KJob::Bytes, 0); |
779 | } |
780 | |
781 | CollectionSync::~CollectionSync() |
782 | { |
783 | delete d; |
784 | } |
785 | |
786 | void CollectionSync::setRemoteCollections(const Collection::List &remoteCollections) |
787 | { |
788 | setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + remoteCollections.count()); |
789 | foreach (const Collection &c, remoteCollections) { |
790 | d->createRemoteNode(c); |
791 | } |
792 | |
793 | if (!d->streaming) { |
794 | d->deliveryDone = true; |
795 | } |
796 | d->execute(); |
797 | } |
798 | |
799 | void CollectionSync::setRemoteCollections(const Collection::List &changedCollections, const Collection::List &removedCollections) |
800 | { |
801 | setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + changedCollections.count()); |
802 | d->incremental = true; |
803 | foreach (const Collection &c, changedCollections) { |
804 | d->createRemoteNode(c); |
805 | } |
806 | d->removedRemoteCollections += removedCollections; |
807 | |
808 | if (!d->streaming) { |
809 | d->deliveryDone = true; |
810 | } |
811 | d->execute(); |
812 | } |
813 | |
814 | void CollectionSync::doStart() |
815 | { |
816 | d->resetNodeTree(); |
817 | Job *parent = (d->currentTransaction ? static_cast<Job *>(d->currentTransaction) : static_cast<Job *>(this)); |
818 | CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, parent); |
819 | job->fetchScope().setResource(d->resourceId); |
820 | job->fetchScope().setIncludeUnsubscribed(true); |
821 | job->fetchScope().setAncestorRetrieval(CollectionFetchScope::Parent); |
822 | connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), |
823 | SLOT(localCollectionsReceived(Akonadi::Collection::List))); |
824 | connect(job, SIGNAL(result(KJob*)), SLOT(localCollectionFetchResult(KJob*))); |
825 | } |
826 | |
827 | void CollectionSync::setStreamingEnabled(bool streaming) |
828 | { |
829 | d->streaming = streaming; |
830 | } |
831 | |
832 | void CollectionSync::retrievalDone() |
833 | { |
834 | d->deliveryDone = true; |
835 | d->execute(); |
836 | } |
837 | |
838 | void CollectionSync::setHierarchicalRemoteIds(bool hierarchical) |
839 | { |
840 | d->hierarchicalRIDs = hierarchical; |
841 | } |
842 | |
843 | void CollectionSync::rollback() |
844 | { |
845 | if (d->currentTransaction) { |
846 | d->currentTransaction->rollback(); |
847 | } |
848 | } |
849 | |
850 | void CollectionSync::setKeepLocalChanges(const QSet<QByteArray> &parts) |
851 | { |
852 | d->keepLocalChanges = parts; |
853 | } |
854 | |
855 | #include "moc_collectionsync_p.cpp" |
856 | |