akonadi
collectionsync.cpp
00001 /* 00002 Copyright (c) 2007, 2009 Volker Krause <vkrause@kde.org> 00003 00004 This library is free software; you can redistribute it and/or modify it 00005 under the terms of the GNU Library General Public License as published by 00006 the Free Software Foundation; either version 2 of the License, or (at your 00007 option) any later version. 00008 00009 This library is distributed in the hope that it will be useful, but WITHOUT 00010 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 00011 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 00012 License for more details. 00013 00014 You should have received a copy of the GNU Library General Public License 00015 along with this library; see the file COPYING.LIB. If not, write to the 00016 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 00017 02110-1301, USA. 00018 */ 00019 00020 #include "collectionsync_p.h" 00021 #include "collection.h" 00022 00023 #include "collectioncreatejob.h" 00024 #include "collectiondeletejob.h" 00025 #include "collectionfetchjob.h" 00026 #include "collectionmodifyjob.h" 00027 #include "collectionfetchscope.h" 00028 #include "collectionmovejob.h" 00029 00030 #include <kdebug.h> 00031 #include <KLocale> 00032 #include <QtCore/QVariant> 00033 00034 using namespace Akonadi; 00035 00036 struct RemoteNode; 00037 00041 struct LocalNode 00042 { 00043 LocalNode( const Collection &col ) : 00044 collection( col ), 00045 processed( false ) 00046 {} 00047 00048 ~LocalNode() 00049 { 00050 qDeleteAll( childNodes ); 00051 qDeleteAll( pendingRemoteNodes ); 00052 } 00053 00054 Collection collection; 00055 QList<LocalNode*> childNodes; 00056 QHash<QString, LocalNode*> childRidMap; 00060 QList<RemoteNode*> pendingRemoteNodes; 00061 bool processed; 00062 }; 00063 00064 Q_DECLARE_METATYPE( LocalNode* ) 00065 static const char LOCAL_NODE[] = "LocalNode"; 00066 00071 struct RemoteNode 00072 { 00073 RemoteNode( const Collection &col ) : 00074 collection( col ) 00075 {} 00076 00077 Collection collection; 00078 }; 00079 00080 Q_DECLARE_METATYPE( RemoteNode* ) 00081 static const char REMOTE_NODE[] = "RemoteNode"; 00082 00086 class CollectionSync::Private 00087 { 00088 public: 00089 Private( CollectionSync *parent ) : 00090 q( parent ), 00091 pendingJobs( 0 ), 00092 progress( 0 ), 00093 incremental( false ), 00094 streaming( false ), 00095 hierarchicalRIDs( false ), 00096 localListDone( false ), 00097 deliveryDone( false ) 00098 { 00099 localRoot = new LocalNode( Collection::root() ); 00100 localRoot->processed = true; // never try to delete that one 00101 localUidMap.insert( localRoot->collection.id(), localRoot ); 00102 if ( !hierarchicalRIDs ) 00103 localRidMap.insert( QString(), localRoot ); 00104 } 00105 00106 ~Private() 00107 { 00108 delete localRoot; 00109 } 00110 00112 LocalNode* createLocalNode( const Collection &col ) 00113 { 00114 LocalNode *node = new LocalNode( col ); 00115 Q_ASSERT( !localUidMap.contains( col.id() ) ); 00116 localUidMap.insert( node->collection.id(), node ); 00117 if ( !hierarchicalRIDs && !col.remoteId().isEmpty() ) 00118 localRidMap.insert( node->collection.remoteId(), node ); 00119 00120 // add already existing children 00121 if ( localPendingCollections.contains( col.id() ) ) { 00122 QVector<Collection::Id> childIds = localPendingCollections.take( col.id() ); 00123 foreach ( Collection::Id childId, childIds ) { 00124 Q_ASSERT( localUidMap.contains( childId ) ); 00125 LocalNode *childNode = localUidMap.value( childId ); 00126 node->childNodes.append( childNode ); 00127 if ( !childNode->collection.remoteId().isEmpty() ) 00128 node->childRidMap.insert( childNode->collection.remoteId(), childNode ); 00129 } 00130 } 00131 00132 // set our parent and add ourselves as child 00133 if ( localUidMap.contains( col.parentCollection().id() ) ) { 00134 LocalNode* parentNode = localUidMap.value( col.parentCollection().id() ); 00135 parentNode->childNodes.append( node ); 00136 if ( !node->collection.remoteId().isEmpty() ) 00137 parentNode->childRidMap.insert( node->collection.remoteId(), node ); 00138 } else { 00139 localPendingCollections[ col.parentCollection().id() ].append( col.id() ); 00140 } 00141 00142 return node; 00143 } 00144 00146 void createRemoteNode( const Collection &col ) 00147 { 00148 if ( col.remoteId().isEmpty() ) { 00149 kWarning() << "Collection '" << col.name() << "' does not have a remote identifier - skipping"; 00150 return; 00151 } 00152 RemoteNode *node = new RemoteNode( col ); 00153 localRoot->pendingRemoteNodes.append( node ); 00154 } 00155 00157 void localCollectionsReceived( const Akonadi::Collection::List &localCols ) 00158 { 00159 foreach ( const Collection &c, localCols ) 00160 createLocalNode( c ); 00161 } 00162 00164 void localCollectionFetchResult( KJob *job ) 00165 { 00166 if ( job->error() ) 00167 return; // handled by the base class 00168 00169 // safety check: the local tree has to be connected 00170 if ( !localPendingCollections.isEmpty() ) { 00171 q->setError( Unknown ); 00172 q->setErrorText( i18n( "Inconsistent local collection tree detected." ) ); 00173 q->emitResult(); 00174 return; 00175 } 00176 00177 localListDone = true; 00178 execute(); 00179 } 00180 00186 LocalNode* findLocalChildNodeByName( LocalNode *localParentNode, const QString &name ) 00187 { 00188 if ( name.isEmpty() ) // shouldn't happen... 00189 return 0; 00190 00191 if ( localParentNode == localRoot ) // possibly non-unique names on top-level 00192 return 0; 00193 00194 foreach ( LocalNode *childNode, localParentNode->childNodes ) { 00195 // the restriction on empty RIDs can possibly removed, but for now I only understand the implication for this case 00196 if ( childNode->collection.name() == name && childNode->collection.remoteId().isEmpty() ) 00197 return childNode; 00198 } 00199 return 0; 00200 } 00201 00206 LocalNode* findMatchingLocalNode( const Collection &collection ) 00207 { 00208 if ( !hierarchicalRIDs ) { 00209 if ( localRidMap.contains( collection.remoteId() ) ) 00210 return localRidMap.value( collection.remoteId() ); 00211 return 0; 00212 } else { 00213 if ( collection.id() == Collection::root().id() || collection.remoteId() == Collection::root().remoteId() ) 00214 return localRoot; 00215 LocalNode *localParent = 0; 00216 if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { 00217 kWarning() << "Remote collection without valid parent found: " << collection; 00218 return 0; 00219 } 00220 if ( collection.parentCollection().id() == Collection::root().id() || collection.parentCollection().remoteId() == Collection::root().remoteId() ) 00221 localParent = localRoot; 00222 else 00223 localParent = findMatchingLocalNode( collection.parentCollection() ); 00224 00225 if ( localParent ) { 00226 if ( localParent->childRidMap.contains( collection.remoteId() ) ) 00227 return localParent->childRidMap.value( collection.remoteId() ); 00228 // check if we have a local folder with a matching name and no RID, if so let's use that one 00229 // 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 00230 if ( LocalNode *recoveredLocalNode = findLocalChildNodeByName( localParent, collection.name() ) ) { 00231 kDebug() << "Recovering collection with lost RID:" << collection << recoveredLocalNode->collection; 00232 return recoveredLocalNode; 00233 } 00234 } 00235 return 0; 00236 } 00237 } 00238 00244 LocalNode* findBestLocalAncestor( const Collection &collection, bool *exactMatch = 0 ) 00245 { 00246 if ( !hierarchicalRIDs ) 00247 return localRoot; 00248 if ( collection == Collection::root() ) { 00249 if ( exactMatch ) *exactMatch = true; 00250 return localRoot; 00251 } 00252 if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { 00253 kWarning() << "Remote collection without valid parent found: " << collection; 00254 return 0; 00255 } 00256 bool parentIsExact = false; 00257 LocalNode *localParent = findBestLocalAncestor( collection.parentCollection(), &parentIsExact ); 00258 if ( !parentIsExact ) { 00259 if ( exactMatch ) *exactMatch = false; 00260 return localParent; 00261 } 00262 if ( localParent->childRidMap.contains( collection.remoteId() ) ) { 00263 if ( exactMatch ) *exactMatch = true; 00264 return localParent->childRidMap.value( collection.remoteId() ); 00265 } 00266 if ( exactMatch ) *exactMatch = false; 00267 return localParent; 00268 } 00269 00275 void processPendingRemoteNodes( LocalNode *_localRoot ) 00276 { 00277 QList<RemoteNode*> pendingRemoteNodes( _localRoot->pendingRemoteNodes ); 00278 _localRoot->pendingRemoteNodes.clear(); 00279 QHash<LocalNode*, QList<RemoteNode*> > pendingCreations; 00280 foreach ( RemoteNode *remoteNode, pendingRemoteNodes ) { 00281 // step 1: see if we have a matching local node already 00282 LocalNode *localNode = findMatchingLocalNode( remoteNode->collection ); 00283 if ( localNode ) { 00284 Q_ASSERT( !localNode->processed ); 00285 updateLocalCollection( localNode, remoteNode ); 00286 continue; 00287 } 00288 // step 2: check if we have the parent at least, then we can create it 00289 localNode = findMatchingLocalNode( remoteNode->collection.parentCollection() ); 00290 if ( localNode ) { 00291 pendingCreations[localNode].append( remoteNode ); 00292 continue; 00293 } 00294 // step 3: find the best matching ancestor and enqueue it for later processing 00295 localNode = findBestLocalAncestor( remoteNode->collection ); 00296 if ( !localNode ) { 00297 q->setError( Unknown ); 00298 q->setErrorText( i18n( "Remote collection without root-terminated ancestor chain provided, resource is broken." ) ); 00299 q->emitResult(); 00300 return; 00301 } 00302 localNode->pendingRemoteNodes.append( remoteNode ); 00303 } 00304 00305 // process the now possible collection creations 00306 for ( QHash<LocalNode*, QList<RemoteNode*> >::const_iterator it = pendingCreations.constBegin(); 00307 it != pendingCreations.constEnd(); ++it ) 00308 { 00309 createLocalCollections( it.key(), it.value() ); 00310 } 00311 } 00312 00316 void updateLocalCollection( LocalNode *localNode, RemoteNode *remoteNode ) 00317 { 00318 Collection upd( remoteNode->collection ); 00319 Q_ASSERT( !upd.remoteId().isEmpty() ); 00320 upd.setId( localNode->collection.id() ); 00321 { 00322 // ### HACK to work around the implicit move attempts of CollectionModifyJob 00323 // which we do explicitly below 00324 Collection c( upd ); 00325 c.setParentCollection( localNode->collection.parentCollection() ); 00326 ++pendingJobs; 00327 CollectionModifyJob *mod = new CollectionModifyJob( c, q ); 00328 connect( mod, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*)) ); 00329 } 00330 00331 // detecting moves is only possible with global RIDs 00332 if ( !hierarchicalRIDs ) { 00333 LocalNode *oldParent = localUidMap.value( localNode->collection.parentCollection().id() ); 00334 LocalNode *newParent = findMatchingLocalNode( remoteNode->collection.parentCollection() ); 00335 // TODO: handle the newParent == 0 case correctly, ie. defer the move until the new 00336 // local parent has been created 00337 if ( newParent && oldParent != newParent ) { 00338 ++pendingJobs; 00339 CollectionMoveJob *move = new CollectionMoveJob( upd, newParent->collection, q ); 00340 connect( move, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*)) ); 00341 } 00342 } 00343 00344 localNode->processed = true; 00345 delete remoteNode; 00346 } 00347 00348 void updateLocalCollectionResult( KJob* job ) 00349 { 00350 --pendingJobs; 00351 if ( job->error() ) 00352 return; // handled by the base class 00353 if ( qobject_cast<CollectionModifyJob*>( job ) ) 00354 ++progress; 00355 checkDone(); 00356 } 00357 00362 void createLocalCollections( LocalNode* localParent, QList<RemoteNode*> remoteNodes ) 00363 { 00364 foreach ( RemoteNode *remoteNode, remoteNodes ) { 00365 ++pendingJobs; 00366 Collection col( remoteNode->collection ); 00367 Q_ASSERT( !col.remoteId().isEmpty() ); 00368 col.setParentCollection( localParent->collection ); 00369 CollectionCreateJob *create = new CollectionCreateJob( col, q ); 00370 create->setProperty( LOCAL_NODE, QVariant::fromValue( localParent ) ); 00371 create->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); 00372 connect( create, SIGNAL(result(KJob*)), q, SLOT(createLocalCollectionResult(KJob*)) ); 00373 } 00374 } 00375 00376 void createLocalCollectionResult( KJob* job ) 00377 { 00378 --pendingJobs; 00379 if ( job->error() ) 00380 return; // handled by the base class 00381 00382 const Collection newLocal = static_cast<CollectionCreateJob*>( job )->collection(); 00383 LocalNode* localNode = createLocalNode( newLocal ); 00384 localNode->processed = true; 00385 00386 LocalNode* localParent = job->property( LOCAL_NODE ).value<LocalNode*>(); 00387 Q_ASSERT( localParent->childNodes.contains( localNode ) ); 00388 RemoteNode* remoteNode = job->property( REMOTE_NODE ).value<RemoteNode*>(); 00389 delete remoteNode; 00390 ++progress; 00391 00392 processPendingRemoteNodes( localParent ); 00393 if ( !hierarchicalRIDs ) 00394 processPendingRemoteNodes( localRoot ); 00395 00396 checkDone(); 00397 } 00398 00402 bool hasProcessedChildren( LocalNode *localNode ) const 00403 { 00404 if ( localNode->processed ) 00405 return true; 00406 foreach ( LocalNode *child, localNode->childNodes ) { 00407 if ( hasProcessedChildren( child ) ) 00408 return true; 00409 } 00410 return false; 00411 } 00412 00417 Collection::List findUnprocessedLocalCollections( LocalNode *localNode ) const 00418 { 00419 Collection::List rv; 00420 if ( !localNode->processed ) { 00421 if ( hasProcessedChildren( localNode ) ) { 00422 kWarning() << "Found unprocessed local node with processed children, excluding from deletion"; 00423 kWarning() << localNode->collection; 00424 return rv; 00425 } 00426 if ( localNode->collection.remoteId().isEmpty() ) { 00427 kWarning() << "Found unprocessed local node without remoteId, excluding from deletion"; 00428 kWarning() << localNode->collection; 00429 return rv; 00430 } 00431 rv.append( localNode->collection ); 00432 return rv; 00433 } 00434 00435 foreach ( LocalNode *child, localNode->childNodes ) 00436 rv.append( findUnprocessedLocalCollections( child ) ); 00437 return rv; 00438 } 00439 00443 void deleteUnprocessedLocalNodes() 00444 { 00445 if ( incremental ) 00446 return; 00447 const Collection::List cols = findUnprocessedLocalCollections( localRoot ); 00448 deleteLocalCollections( cols ); 00449 } 00450 00455 void deleteLocalCollections( const Collection::List &cols ) 00456 { 00457 q->setTotalAmount( KJob::Bytes, q->totalAmount( KJob::Bytes ) + cols.size() ); 00458 foreach ( const Collection &col, cols ) { 00459 Q_ASSERT( !col.remoteId().isEmpty() ); // empty RID -> stuff we haven't even written to the remote side yet 00460 00461 ++pendingJobs; 00462 CollectionDeleteJob *job = new CollectionDeleteJob( col, q ); 00463 connect( job, SIGNAL(result(KJob*)), q, SLOT(deleteLocalCollectionsResult(KJob*)) ); 00464 00465 // It can happen that the groupware servers report us deleted collections 00466 // twice, in this case this collection delete job will fail on the second try. 00467 // To avoid a rollback of the complete transaction we gracefully allow the job 00468 // to fail :) 00469 q->setIgnoreJobFailure( job ); 00470 } 00471 } 00472 00473 void deleteLocalCollectionsResult( KJob* ) 00474 { 00475 --pendingJobs; 00476 00477 ++progress; 00478 checkDone(); 00479 } 00480 00484 void execute() 00485 { 00486 kDebug() << Q_FUNC_INFO << "localListDone: " << localListDone << " deliveryDone: " << deliveryDone; 00487 if ( !localListDone ) 00488 return; 00489 00490 processPendingRemoteNodes( localRoot ); 00491 00492 if ( !incremental && deliveryDone ) 00493 deleteUnprocessedLocalNodes(); 00494 00495 if ( !hierarchicalRIDs ) { 00496 deleteLocalCollections( removedRemoteCollections ); 00497 } else { 00498 Collection::List localCols; 00499 foreach ( const Collection &c, removedRemoteCollections ) { 00500 LocalNode *node = findMatchingLocalNode( c ); 00501 if ( node ) 00502 localCols.append( node->collection ); 00503 } 00504 deleteLocalCollections( localCols ); 00505 } 00506 removedRemoteCollections.clear(); 00507 00508 checkDone(); 00509 } 00510 00514 QList<RemoteNode*> findPendingRemoteNodes( LocalNode *localNode ) 00515 { 00516 QList<RemoteNode*> rv; 00517 rv.append( localNode->pendingRemoteNodes ); 00518 foreach ( LocalNode *child, localNode->childNodes ) 00519 rv.append( findPendingRemoteNodes( child ) ); 00520 return rv; 00521 } 00522 00527 void checkDone() 00528 { 00529 q->setProcessedAmount( KJob::Bytes, progress ); 00530 00531 // still running jobs or not fully delivered local/remote state 00532 if ( !deliveryDone || pendingJobs > 0 || !localListDone ) 00533 return; 00534 00535 // safety check: there must be no pending remote nodes anymore 00536 QList<RemoteNode*> orphans = findPendingRemoteNodes( localRoot ); 00537 if ( !orphans.isEmpty() ) { 00538 q->setError( Unknown ); 00539 q->setErrorText( i18n( "Found unresolved orphan collections" ) ); 00540 foreach ( RemoteNode* orphan, orphans ) 00541 kDebug() << "found orphan collection:" << orphan->collection; 00542 q->emitResult(); 00543 return; 00544 } 00545 00546 kDebug() << Q_FUNC_INFO << "q->commit()"; 00547 q->commit(); 00548 } 00549 00550 CollectionSync *q; 00551 00552 QString resourceId; 00553 00554 int pendingJobs; 00555 int progress; 00556 00557 LocalNode* localRoot; 00558 QHash<Collection::Id, LocalNode*> localUidMap; 00559 QHash<QString, LocalNode*> localRidMap; 00560 00561 // temporary during build-up of the local node tree, must be empty afterwards 00562 QHash<Collection::Id, QVector<Collection::Id> > localPendingCollections; 00563 00564 // removed remote collections in incremental mode 00565 Collection::List removedRemoteCollections; 00566 00567 bool incremental; 00568 bool streaming; 00569 bool hierarchicalRIDs; 00570 00571 bool localListDone; 00572 bool deliveryDone; 00573 }; 00574 00575 CollectionSync::CollectionSync( const QString &resourceId, QObject *parent ) : 00576 TransactionSequence( parent ), 00577 d( new Private( this ) ) 00578 { 00579 d->resourceId = resourceId; 00580 setTotalAmount( KJob::Bytes, 0 ); 00581 } 00582 00583 CollectionSync::~CollectionSync() 00584 { 00585 delete d; 00586 } 00587 00588 void CollectionSync::setRemoteCollections(const Collection::List & remoteCollections) 00589 { 00590 setTotalAmount( KJob::Bytes, totalAmount( KJob::Bytes ) + remoteCollections.count() ); 00591 foreach ( const Collection &c, remoteCollections ) 00592 d->createRemoteNode( c ); 00593 00594 if ( !d->streaming ) 00595 d->deliveryDone = true; 00596 d->execute(); 00597 } 00598 00599 void CollectionSync::setRemoteCollections(const Collection::List & changedCollections, const Collection::List & removedCollections) 00600 { 00601 setTotalAmount( KJob::Bytes, totalAmount( KJob::Bytes ) + changedCollections.count() ); 00602 d->incremental = true; 00603 foreach ( const Collection &c, changedCollections ) 00604 d->createRemoteNode( c ); 00605 d->removedRemoteCollections += removedCollections; 00606 00607 if ( !d->streaming ) 00608 d->deliveryDone = true; 00609 d->execute(); 00610 } 00611 00612 void CollectionSync::doStart() 00613 { 00614 CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); 00615 job->fetchScope().setResource( d->resourceId ); 00616 job->fetchScope().setIncludeUnsubscribed( true ); 00617 job->fetchScope().setAncestorRetrieval( CollectionFetchScope::Parent ); 00618 connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), 00619 SLOT(localCollectionsReceived(Akonadi::Collection::List)) ); 00620 connect( job, SIGNAL(result(KJob*)), SLOT(localCollectionFetchResult(KJob*)) ); 00621 } 00622 00623 void CollectionSync::setStreamingEnabled( bool streaming ) 00624 { 00625 d->streaming = streaming; 00626 } 00627 00628 void CollectionSync::retrievalDone() 00629 { 00630 d->deliveryDone = true; 00631 d->execute(); 00632 } 00633 00634 void CollectionSync::setHierarchicalRemoteIds( bool hierarchical ) 00635 { 00636 d->hierarchicalRIDs = hierarchical; 00637 } 00638 00639 #include "collectionsync_p.moc"
This file is part of the KDE documentation.
Documentation copyright © 1996-2012 The KDE developers.
Generated on Mon Apr 30 2012 21:49:15 by doxygen 1.8.0 written by Dimitri van Heesch, © 1997-2006
Documentation copyright © 1996-2012 The KDE developers.
Generated on Mon Apr 30 2012 21:49:15 by doxygen 1.8.0 written by Dimitri van Heesch, © 1997-2006
KDE's Doxygen guidelines are available online.