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 if ( col.remoteId().isEmpty() ) // no remote id here means it hasn't been added to the resource yet, so we exclude it from the sync 00115 return 0; 00116 LocalNode *node = new LocalNode( col ); 00117 Q_ASSERT( !localUidMap.contains( col.id() ) ); 00118 localUidMap.insert( node->collection.id(), node ); 00119 if ( !hierarchicalRIDs ) 00120 localRidMap.insert( node->collection.remoteId(), node ); 00121 00122 // add already existing children 00123 if ( localPendingCollections.contains( col.id() ) ) { 00124 QVector<Collection::Id> childIds = localPendingCollections.take( col.id() ); 00125 foreach ( Collection::Id childId, childIds ) { 00126 Q_ASSERT( localUidMap.contains( childId ) ); 00127 LocalNode *childNode = localUidMap.value( childId ); 00128 node->childNodes.append( childNode ); 00129 node->childRidMap.insert( childNode->collection.remoteId(), childNode ); 00130 } 00131 } 00132 00133 // set our parent and add ourselves as child 00134 if ( localUidMap.contains( col.parentCollection().id() ) ) { 00135 LocalNode* parentNode = localUidMap.value( col.parentCollection().id() ); 00136 parentNode->childNodes.append( node ); 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 00185 LocalNode* findMatchingLocalNode( const Collection &collection ) 00186 { 00187 if ( !hierarchicalRIDs ) { 00188 if ( localRidMap.contains( collection.remoteId() ) ) 00189 return localRidMap.value( collection.remoteId() ); 00190 return 0; 00191 } else { 00192 if ( collection.id() == Collection::root().id() || collection.remoteId() == Collection::root().remoteId() ) 00193 return localRoot; 00194 LocalNode *localParent = 0; 00195 if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { 00196 kWarning() << "Remote collection without valid parent found: " << collection; 00197 return 0; 00198 } 00199 if ( collection.parentCollection().id() == Collection::root().id() || collection.parentCollection().remoteId() == Collection::root().remoteId() ) 00200 localParent = localRoot; 00201 else 00202 localParent = findMatchingLocalNode( collection.parentCollection() ); 00203 00204 if ( localParent && localParent->childRidMap.contains( collection.remoteId() ) ) 00205 return localParent->childRidMap.value( collection.remoteId() ); 00206 return 0; 00207 } 00208 } 00209 00215 LocalNode* findBestLocalAncestor( const Collection &collection, bool *exactMatch = 0 ) 00216 { 00217 if ( !hierarchicalRIDs ) 00218 return localRoot; 00219 if ( collection == Collection::root() ) { 00220 if ( exactMatch ) *exactMatch = true; 00221 return localRoot; 00222 } 00223 if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { 00224 kWarning() << "Remote collection without valid parent found: " << collection; 00225 return 0; 00226 } 00227 bool parentIsExact = false; 00228 LocalNode *localParent = findBestLocalAncestor( collection.parentCollection(), &parentIsExact ); 00229 if ( !parentIsExact ) { 00230 if ( exactMatch ) *exactMatch = false; 00231 return localParent; 00232 } 00233 if ( localParent->childRidMap.contains( collection.remoteId() ) ) { 00234 if ( exactMatch ) *exactMatch = true; 00235 return localParent->childRidMap.value( collection.remoteId() ); 00236 } 00237 if ( exactMatch ) *exactMatch = false; 00238 return localParent; 00239 } 00240 00246 void processPendingRemoteNodes( LocalNode *_localRoot ) 00247 { 00248 QList<RemoteNode*> pendingRemoteNodes( _localRoot->pendingRemoteNodes ); 00249 _localRoot->pendingRemoteNodes.clear(); 00250 QHash<LocalNode*, QList<RemoteNode*> > pendingCreations; 00251 foreach ( RemoteNode *remoteNode, pendingRemoteNodes ) { 00252 // step 1: see if we have a matching local node already 00253 LocalNode *localNode = findMatchingLocalNode( remoteNode->collection ); 00254 if ( localNode ) { 00255 Q_ASSERT( !localNode->processed ); 00256 updateLocalCollection( localNode, remoteNode ); 00257 continue; 00258 } 00259 // step 2: check if we have the parent at least, then we can create it 00260 localNode = findMatchingLocalNode( remoteNode->collection.parentCollection() ); 00261 if ( localNode ) { 00262 pendingCreations[localNode].append( remoteNode ); 00263 continue; 00264 } 00265 // step 3: find the best matching ancestor and enqueue it for later processing 00266 localNode = findBestLocalAncestor( remoteNode->collection ); 00267 if ( !localNode ) { 00268 q->setError( Unknown ); 00269 q->setErrorText( i18n( "Remote collection without root-terminated ancestor chain provided, resource is broken." ) ); 00270 q->emitResult(); 00271 return; 00272 } 00273 localNode->pendingRemoteNodes.append( remoteNode ); 00274 } 00275 00276 // process the now possible collection creations 00277 for ( QHash<LocalNode*, QList<RemoteNode*> >::const_iterator it = pendingCreations.constBegin(); 00278 it != pendingCreations.constEnd(); ++it ) 00279 { 00280 createLocalCollections( it.key(), it.value() ); 00281 } 00282 } 00283 00287 void updateLocalCollection( LocalNode *localNode, RemoteNode *remoteNode ) 00288 { 00289 Collection upd( remoteNode->collection ); 00290 upd.setId( localNode->collection.id() ); 00291 { 00292 // ### HACK to work around the implicit move attempts of CollectionModifyJob 00293 // which we do explicitly below 00294 Collection c( upd ); 00295 c.setParentCollection( localNode->collection.parentCollection() ); 00296 ++pendingJobs; 00297 CollectionModifyJob *mod = new CollectionModifyJob( c, q ); 00298 connect( mod, SIGNAL( result( KJob* ) ), q, SLOT( updateLocalCollectionResult( KJob* ) ) ); 00299 } 00300 00301 // detecting moves is only possible with global RIDs 00302 if ( !hierarchicalRIDs ) { 00303 LocalNode *oldParent = localUidMap.value( localNode->collection.parentCollection().id() ); 00304 LocalNode *newParent = findMatchingLocalNode( remoteNode->collection.parentCollection() ); 00305 // TODO: handle the newParent == 0 case correctly, ie. defer the move until the new 00306 // local parent has been created 00307 if ( newParent && oldParent != newParent ) { 00308 ++pendingJobs; 00309 CollectionMoveJob *move = new CollectionMoveJob( upd, newParent->collection, q ); 00310 connect( move, SIGNAL( result( KJob* ) ), q, SLOT( updateLocalCollectionResult( KJob* ) ) ); 00311 } 00312 } 00313 00314 localNode->processed = true; 00315 delete remoteNode; 00316 } 00317 00318 void updateLocalCollectionResult( KJob* job ) 00319 { 00320 --pendingJobs; 00321 if ( job->error() ) 00322 return; // handled by the base class 00323 if ( qobject_cast<CollectionModifyJob*>( job ) ) 00324 ++progress; 00325 checkDone(); 00326 } 00327 00332 void createLocalCollections( LocalNode* localParent, QList<RemoteNode*> remoteNodes ) 00333 { 00334 foreach ( RemoteNode *remoteNode, remoteNodes ) { 00335 ++pendingJobs; 00336 Collection col( remoteNode->collection ); 00337 col.setParentCollection( localParent->collection ); 00338 CollectionCreateJob *create = new CollectionCreateJob( col, q ); 00339 create->setProperty( LOCAL_NODE, QVariant::fromValue( localParent ) ); 00340 create->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); 00341 connect( create, SIGNAL( result( KJob* ) ), q, SLOT( createLocalCollectionResult( KJob* ) ) ); 00342 } 00343 } 00344 00345 void createLocalCollectionResult( KJob* job ) 00346 { 00347 --pendingJobs; 00348 if ( job->error() ) 00349 return; // handled by the base class 00350 00351 const Collection newLocal = static_cast<CollectionCreateJob*>( job )->collection(); 00352 LocalNode* localNode = createLocalNode( newLocal ); 00353 localNode->processed = true; 00354 00355 LocalNode* localParent = job->property( LOCAL_NODE ).value<LocalNode*>(); 00356 Q_ASSERT( localParent->childNodes.contains( localNode ) ); 00357 RemoteNode* remoteNode = job->property( REMOTE_NODE ).value<RemoteNode*>(); 00358 delete remoteNode; 00359 ++progress; 00360 00361 processPendingRemoteNodes( localParent ); 00362 if ( !hierarchicalRIDs ) 00363 processPendingRemoteNodes( localRoot ); 00364 00365 checkDone(); 00366 } 00367 00371 bool hasProcessedChildren( LocalNode *localNode ) const 00372 { 00373 if ( localNode->processed ) 00374 return true; 00375 foreach ( LocalNode *child, localNode->childNodes ) { 00376 if ( hasProcessedChildren( child ) ) 00377 return true; 00378 } 00379 return false; 00380 } 00381 00386 Collection::List findUnprocessedLocalCollections( LocalNode *localNode ) const 00387 { 00388 Collection::List rv; 00389 if ( !localNode->processed && hasProcessedChildren( localNode ) ) { 00390 kWarning() << "Found unprocessed local node with processed children, excluding from deletion"; 00391 kWarning() << localNode->collection; 00392 return rv; 00393 } 00394 if ( !localNode->processed ) { 00395 rv.append( localNode->collection ); 00396 return rv; 00397 } 00398 foreach ( LocalNode *child, localNode->childNodes ) 00399 rv.append( findUnprocessedLocalCollections( child ) ); 00400 return rv; 00401 } 00402 00406 void deleteUnprocessedLocalNodes() 00407 { 00408 if ( incremental ) 00409 return; 00410 const Collection::List cols = findUnprocessedLocalCollections( localRoot ); 00411 deleteLocalCollections( cols ); 00412 } 00413 00418 void deleteLocalCollections( const Collection::List &cols ) 00419 { 00420 q->setTotalAmount( KJob::Bytes, q->totalAmount( KJob::Bytes ) + cols.size() ); 00421 foreach ( const Collection &col, cols ) { 00422 ++pendingJobs; 00423 CollectionDeleteJob *job = new CollectionDeleteJob( col, q ); 00424 connect( job, SIGNAL( result( KJob* ) ), q, SLOT( deleteLocalCollectionsResult( KJob* ) ) ); 00425 00426 // It can happen that the groupware servers report us deleted collections 00427 // twice, in this case this collection delete job will fail on the second try. 00428 // To avoid a rollback of the complete transaction we gracefully allow the job 00429 // to fail :) 00430 q->setIgnoreJobFailure( job ); 00431 } 00432 } 00433 00434 void deleteLocalCollectionsResult( KJob* ) 00435 { 00436 --pendingJobs; 00437 00438 ++progress; 00439 checkDone(); 00440 } 00441 00445 void execute() 00446 { 00447 kDebug() << Q_FUNC_INFO << "localListDone: " << localListDone << " deliveryDone: " << deliveryDone; 00448 if ( !localListDone ) 00449 return; 00450 00451 processPendingRemoteNodes( localRoot ); 00452 00453 if ( !incremental && deliveryDone ) 00454 deleteUnprocessedLocalNodes(); 00455 00456 if ( !hierarchicalRIDs ) { 00457 deleteLocalCollections( removedRemoteCollections ); 00458 } else { 00459 Collection::List localCols; 00460 foreach ( const Collection &c, removedRemoteCollections ) { 00461 LocalNode *node = findMatchingLocalNode( c ); 00462 if ( node ) 00463 localCols.append( node->collection ); 00464 } 00465 deleteLocalCollections( localCols ); 00466 } 00467 removedRemoteCollections.clear(); 00468 00469 checkDone(); 00470 } 00471 00475 QList<RemoteNode*> findPendingRemoteNodes( LocalNode *localNode ) 00476 { 00477 QList<RemoteNode*> rv; 00478 rv.append( localNode->pendingRemoteNodes ); 00479 foreach ( LocalNode *child, localNode->childNodes ) 00480 rv.append( findPendingRemoteNodes( child ) ); 00481 return rv; 00482 } 00483 00488 void checkDone() 00489 { 00490 q->setProcessedAmount( KJob::Bytes, progress ); 00491 00492 // still running jobs or not fully delivered local/remote state 00493 if ( !deliveryDone || pendingJobs > 0 || !localListDone ) 00494 return; 00495 00496 // safety check: there must be no pending remote nodes anymore 00497 QList<RemoteNode*> orphans = findPendingRemoteNodes( localRoot ); 00498 if ( !orphans.isEmpty() ) { 00499 q->setError( Unknown ); 00500 q->setErrorText( i18n( "Found unresolved orphan collections" ) ); 00501 foreach ( RemoteNode* orphan, orphans ) 00502 kDebug() << "found orphan collection:" << orphan->collection; 00503 q->emitResult(); 00504 return; 00505 } 00506 00507 kDebug() << Q_FUNC_INFO << "q->commit()"; 00508 q->commit(); 00509 } 00510 00511 CollectionSync *q; 00512 00513 QString resourceId; 00514 00515 int pendingJobs; 00516 int progress; 00517 00518 LocalNode* localRoot; 00519 QHash<Collection::Id, LocalNode*> localUidMap; 00520 QHash<QString, LocalNode*> localRidMap; 00521 00522 // temporary during build-up of the local node tree, must be empty afterwards 00523 QHash<Collection::Id, QVector<Collection::Id> > localPendingCollections; 00524 00525 // removed remote collections in incremental mode 00526 Collection::List removedRemoteCollections; 00527 00528 bool incremental; 00529 bool streaming; 00530 bool hierarchicalRIDs; 00531 00532 bool localListDone; 00533 bool deliveryDone; 00534 }; 00535 00536 CollectionSync::CollectionSync( const QString &resourceId, QObject *parent ) : 00537 TransactionSequence( parent ), 00538 d( new Private( this ) ) 00539 { 00540 d->resourceId = resourceId; 00541 setTotalAmount( KJob::Bytes, 0 ); 00542 } 00543 00544 CollectionSync::~CollectionSync() 00545 { 00546 delete d; 00547 } 00548 00549 void CollectionSync::setRemoteCollections(const Collection::List & remoteCollections) 00550 { 00551 setTotalAmount( KJob::Bytes, totalAmount( KJob::Bytes ) + remoteCollections.count() ); 00552 foreach ( const Collection &c, remoteCollections ) 00553 d->createRemoteNode( c ); 00554 00555 if ( !d->streaming ) 00556 d->deliveryDone = true; 00557 d->execute(); 00558 } 00559 00560 void CollectionSync::setRemoteCollections(const Collection::List & changedCollections, const Collection::List & removedCollections) 00561 { 00562 setTotalAmount( KJob::Bytes, totalAmount( KJob::Bytes ) + changedCollections.count() ); 00563 d->incremental = true; 00564 foreach ( const Collection &c, changedCollections ) 00565 d->createRemoteNode( c ); 00566 d->removedRemoteCollections += removedCollections; 00567 00568 if ( !d->streaming ) 00569 d->deliveryDone = true; 00570 d->execute(); 00571 } 00572 00573 void CollectionSync::doStart() 00574 { 00575 CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); 00576 job->fetchScope().setResource( d->resourceId ); 00577 job->fetchScope().setIncludeUnsubscribed( true ); 00578 job->fetchScope().setAncestorRetrieval( CollectionFetchScope::Parent ); 00579 connect( job, SIGNAL( collectionsReceived( const Akonadi::Collection::List& ) ), 00580 SLOT( localCollectionsReceived( const Akonadi::Collection::List& ) ) ); 00581 connect( job, SIGNAL( result( KJob* ) ), SLOT( localCollectionFetchResult( KJob* ) ) ); 00582 } 00583 00584 void CollectionSync::setStreamingEnabled( bool streaming ) 00585 { 00586 d->streaming = streaming; 00587 } 00588 00589 void CollectionSync::retrievalDone() 00590 { 00591 d->deliveryDone = true; 00592 d->execute(); 00593 } 00594 00595 void CollectionSync::setHierarchicalRemoteIds( bool hierarchical ) 00596 { 00597 d->hierarchicalRIDs = hierarchical; 00598 } 00599 00600 #include "collectionsync_p.moc"