diff --git a/akonadi/collectionfetchjob.cpp b/akonadi/collectionfetchjob.cpp index bf8958ef2..972644a0b 100644 --- a/akonadi/collectionfetchjob.cpp +++ b/akonadi/collectionfetchjob.cpp @@ -1,245 +1,262 @@ /* Copyright (c) 2006 - 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionfetchjob.h" #include "imapparser_p.h" #include "job_p.h" #include "protocol_p.h" #include "protocolhelper_p.h" #include "entity_p.h" #include "collectionfetchscope.h" #include #include #include #include using namespace Akonadi; class Akonadi::CollectionFetchJobPrivate : public JobPrivate { public: CollectionFetchJobPrivate( CollectionFetchJob *parent ) : JobPrivate( parent ) { } Q_DECLARE_PUBLIC( CollectionFetchJob ) CollectionFetchJob::Type mType; Collection mBase; Collection::List mBaseList; Collection::List mCollections; CollectionFetchScope mScope; Collection::List mPendingCollections; QTimer *mEmitTimer; void timeout() { Q_Q( CollectionFetchJob ); mEmitTimer->stop(); // in case we are called by result() if ( !mPendingCollections.isEmpty() ) { emit q->collectionsReceived( mPendingCollections ); mPendingCollections.clear(); } } }; CollectionFetchJob::CollectionFetchJob( const Collection &collection, Type type, QObject *parent ) : Job( new CollectionFetchJobPrivate( this ), parent ) { Q_D( CollectionFetchJob ); d->mBase = collection; d->mType = type; d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); } CollectionFetchJob::CollectionFetchJob( const Collection::List & cols, QObject * parent ) : Job( new CollectionFetchJobPrivate( this ), parent ) { Q_D( CollectionFetchJob ); Q_ASSERT( !cols.isEmpty() ); if ( cols.size() == 1 ) { d->mBase = cols.first(); d->mType = CollectionFetchJob::Base; } else { d->mBaseList = cols; } d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); } CollectionFetchJob::~CollectionFetchJob() { } Collection::List CollectionFetchJob::collections() const { Q_D( const CollectionFetchJob ); return d->mCollections; } void CollectionFetchJob::doStart() { Q_D( CollectionFetchJob ); if ( !d->mBaseList.isEmpty() ) { foreach ( const Collection &col, d->mBaseList ) { new CollectionFetchJob( col, CollectionFetchJob::Base, this ); } return; } if ( !d->mBase.isValid() && d->mBase.remoteId().isEmpty() ) { setError( Unknown ); setErrorText( QLatin1String( "Invalid collection given." ) ); emitResult(); return; } QByteArray command = d->newTag(); if ( !d->mBase.isValid() ) command += " " AKONADI_CMD_RID; if ( d->mScope.includeUnubscribed() ) command += " X-AKLIST "; else command += " X-AKLSUB "; if ( d->mBase.isValid() ) command += QByteArray::number( d->mBase.id() ); else command += ImapParser::quote( d->mBase.remoteId().toUtf8() ); command += ' '; switch ( d->mType ) { case Base: command += "0 ("; break; case FirstLevel: command += "1 ("; break; case Recursive: command += "INF ("; break; default: Q_ASSERT( false ); } + QList filter; if ( !d->mScope.resource().isEmpty() ) { - command += "RESOURCE \""; - command += d->mScope.resource().toUtf8(); - command += '"'; + filter.append( "RESOURCE" ); + filter.append( d->mScope.resource().toUtf8() ); } if ( !d->mScope.contentMimeTypes().isEmpty() ) { - command += " MIMETYPE ("; + filter.append( "MIMETYPE" ); QList mts; foreach ( const QString &mt, d->mScope.contentMimeTypes() ) mts.append( mt.toUtf8() ); - command += ImapParser::join( mts, " " ); - command += ')'; + filter.append( "(" + ImapParser::join( mts, " " ) + ")" ); } + QList options; if ( d->mScope.includeStatistics() ) { - command += ") (STATISTICS true"; + options.append( "STATISTICS" ); + options.append( "true" ); + } + if ( d->mScope.ancestorRetrieval() != CollectionFetchScope::None ) { + options.append( "ANCESTORS" ); + switch ( d->mScope.ancestorRetrieval() ) { + case CollectionFetchScope::None: + options.append( "0" ); + break; + case CollectionFetchScope::Parent: + options.append( "1" ); + break; + case CollectionFetchScope::All: + options.append( "INF" ); + break; + default: + Q_ASSERT( false ); + } } - command += ")\n"; + command += ImapParser::join( filter, " " ) + ") (" + ImapParser::join( options, " " ) + ")\n"; d->writeData( command ); } void CollectionFetchJob::doHandleResponse( const QByteArray & tag, const QByteArray & data ) { Q_D( CollectionFetchJob ); if ( tag == "*" ) { Collection collection; ProtocolHelper::parseCollection( data, collection ); if ( !collection.isValid() ) return; collection.d_ptr->resetChangeLog(); d->mCollections.append( collection ); d->mPendingCollections.append( collection ); if ( !d->mEmitTimer->isActive() ) d->mEmitTimer->start(); return; } kDebug() << "Unhandled server response" << tag << data; } void CollectionFetchJob::setResource(const QString & resource) { Q_D( CollectionFetchJob ); d->mScope.setResource( resource ); } void CollectionFetchJob::slotResult(KJob * job) { Q_D( CollectionFetchJob ); CollectionFetchJob *list = dynamic_cast( job ); Q_ASSERT( job ); d->mCollections += list->collections(); Job::slotResult( job ); if ( !job->error() && !hasSubjobs() ) emitResult(); } void CollectionFetchJob::includeUnsubscribed(bool include) { Q_D( CollectionFetchJob ); d->mScope.setIncludeUnsubscribed( include ); } void CollectionFetchJob::includeStatistics(bool include) { Q_D( CollectionFetchJob ); d->mScope.setIncludeStatistics( include ); } void CollectionFetchJob::setFetchScope( const CollectionFetchScope &scope ) { Q_D( CollectionFetchJob ); d->mScope = scope; } CollectionFetchScope& CollectionFetchJob::fetchScope() { Q_D( CollectionFetchJob ); return d->mScope; } #include "collectionfetchjob.moc" diff --git a/akonadi/collectionfetchjob.h b/akonadi/collectionfetchjob.h index 9e99635d7..c0301f660 100644 --- a/akonadi/collectionfetchjob.h +++ b/akonadi/collectionfetchjob.h @@ -1,180 +1,177 @@ /* Copyright (c) 2006 - 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_COLLECTIONFETCHJOB_H #define AKONADI_COLLECTIONFETCHJOB_H #include "akonadi_export.h" #include #include namespace Akonadi { class CollectionFetchScope; class CollectionFetchJobPrivate; /** * @short Job that fetches collections from the Akonadi storage. * * This class can be used to retrieve the complete or partial collection tree * from the Akonadi storage. * * @code * * using namespace Akonadi; * - * // fetching all collections recursive, starting at the root collection - * CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); - * if ( job->exec() ) { - * Collection::List collections = job->collections(); - * foreach( const Collection &collection, collections ) { - * qDebug() << "Name:" << collection.name(); - * } - * } + * // fetching all collections containing emails recursively, starting at the root collection + * CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); + * job->fetchScope().setContentMimeTypes( QStringList() << "message/rfc822" ); + * connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List), this, SLOT(myCollectionsReceived(Akonadi::Collection::List)) ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(collectionFetchResult(KJob*)) ); * * @endcode * * @author Volker Krause */ class AKONADI_EXPORT CollectionFetchJob : public Job { Q_OBJECT public: /** * Describes the type of fetch depth. */ enum Type { Base, ///< Only fetch the base collection. FirstLevel, ///< Only list direct sub-collections of the base collection. Recursive ///< List all sub-collections. }; /** * Creates a new collection fetch job. If the given base collection * has a unique identifier, this is used to identify the collection in the * Akonadi server. If only a remote identifier is avaiable the collection * is identified using that, given a resource search context has been * specified. There two ways of doing that: by calling setResource() * or globally using Akonadi::ResourceSelectJob. * * @param collection The base collection for the listing. * @param type The type of fetch depth. * @param parent The parent object. */ explicit CollectionFetchJob( const Collection &collection, Type type = FirstLevel, QObject *parent = 0 ); /** * Creates a new collection fetch job to retrieve a list of collections. * The same rules for identifiers apply as noted in the constructor above. * * @param collections A list of collections to fetch. Must not be empty. * @param parent The parent object. */ explicit CollectionFetchJob( const Collection::List &collections, QObject *parent = 0 ); /** * Destroys the collection fetch job. */ virtual ~CollectionFetchJob(); /** * Returns the list of fetched collection. */ Collection::List collections() const; /** * Sets a resource identifier to limit collection listing to one resource. * * @param resource The resource identifier. * @deprecated Use CollectionFetchScope instead. */ KDE_DEPRECATED void setResource( const QString &resource ); /** * Include also unsubscribed collections. * @deprecated Use CollectionFetchScope instead. */ KDE_DEPRECATED void includeUnsubscribed( bool include = true ); /** * Include also statistics about the collections. * * @since 4.3 * @deprecated Use CollectionFetchScope instead. */ KDE_DEPRECATED void includeStatistics( bool include = true ); /** * Sets the collection fetch scope. * * The CollectionFetchScope controls how much of a collection's data is fetched * from the server as well as filter to select which collections to fetch. * * @param fetchScope The new scope for collection fetch operations. * * @see fetchScope() * @since 4.4 */ void setFetchScope( const CollectionFetchScope &fetchScope ); /** * Returns the collection fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the CollectionFetchScope documentation * for an example. * * @return a reference to the current collection fetch scope * * @see setFetchScope() for replacing the current collection fetch scope * @since 4.4 */ CollectionFetchScope &fetchScope(); Q_SIGNALS: /** * This signal is emitted whenever the job has received collections. * * @param collections The received collections. */ void collectionsReceived( const Akonadi::Collection::List &collections ); protected: virtual void doStart(); virtual void doHandleResponse( const QByteArray &tag, const QByteArray &data ); protected Q_SLOTS: //@cond PRIVATE void slotResult( KJob* job ); //@endcond private: Q_DECLARE_PRIVATE( CollectionFetchJob ) //@cond PRIVATE Q_PRIVATE_SLOT( d_func(), void timeout() ) //@endcond }; } #endif diff --git a/akonadi/collectionsync.cpp b/akonadi/collectionsync.cpp index 62a3f05d9..964c9dd50 100644 --- a/akonadi/collectionsync.cpp +++ b/akonadi/collectionsync.cpp @@ -1,532 +1,536 @@ /* Copyright (c) 2007, 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionsync_p.h" #include "collection.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" #include "collectionfetchscope.h" #include #include using namespace Akonadi; struct RemoteNode; /** LocalNode is used to build a tree structure of all our locally existing collections. */ struct LocalNode { LocalNode( const Collection &col ) : collection( col ), parentNode( 0 ), processed( false ) {} ~LocalNode() { qDeleteAll( childNodes ); qDeleteAll( pendingRemoteNodes ); } Collection collection; LocalNode *parentNode; // ### really needed? QList childNodes; QHash childRidMap; /** When using hierarchical RIDs we attach a list of not yet processable remote nodes to the closest already existing local ancestor node. They will be re-evaluated once a new child node is added. */ QList pendingRemoteNodes; bool processed; }; Q_DECLARE_METATYPE( LocalNode* ) static const char LOCAL_NODE[] = "LocalNode"; /** RemoteNode is used as a container for remote collections which typically don't have a UID set and thus cannot easily be compared or put into maps etc. */ struct RemoteNode { RemoteNode( const Collection &col ) : collection( col ) {} Collection collection; }; Q_DECLARE_METATYPE( RemoteNode* ) static const char REMOTE_NODE[] = "RemoteNode"; /** * @internal */ class CollectionSync::Private { public: Private( CollectionSync *parent ) : q( parent ), pendingJobs( 0 ), incremental( false ), streaming( false ), hierarchicalRIDs( false ), localListDone( false ), deliveryDone( false ) { localRoot = new LocalNode( Collection::root() ); localUidMap.insert( localRoot->collection.id(), localRoot ); if ( !hierarchicalRIDs ) localRidMap.insert( QString(), localRoot ); } ~Private() { delete localRoot; } /** Create a local node from the given local collection and integrate it into the local tree structure. */ LocalNode* createLocalNode( const Collection &col ) { LocalNode *node = new LocalNode( col ); Q_ASSERT( !localUidMap.contains( col.id() ) ); localUidMap.insert( node->collection.id(), node ); if ( !hierarchicalRIDs ) localRidMap.insert( node->collection.remoteId(), node ); // add already existing children if ( localPendingCollections.contains( col.id() ) ) { QList childIds = localPendingCollections.take( col.id() ); foreach ( Collection::Id childId, childIds ) { Q_ASSERT( localUidMap.contains( childId ) ); LocalNode *childNode = localUidMap.value( childId ); node->childNodes.append( childNode ); node->childRidMap.insert( childNode->collection.remoteId(), childNode ); } } // set our parent and add ourselves as child if ( localUidMap.contains( col.parentCollection().id() ) ) { node->parentNode = localUidMap.value( col.parentCollection().id() ); node->parentNode->childNodes.append( node ); + node->parentNode->childRidMap.insert( node->collection.remoteId(), node ); } else { localPendingCollections[ col.parentCollection().id() ].append( col.id() ); } return node; } /** Same as createLocalNode() for remote collections. */ void createRemoteNode( const Collection &col ) { if ( col.remoteId().isEmpty() ) { kWarning() << "Collection '" << col.name() << "' does not have a remote identifier - skipping"; return; } RemoteNode *node = new RemoteNode( col ); localRoot->pendingRemoteNodes.append( node ); } /** Create local nodes as we receive the local listing from the Akonadi server. */ void localCollectionsReceived( const Akonadi::Collection::List &localCols ) { foreach ( const Collection &c, localCols ) createLocalNode( c ); } /** Once the local collection listing finished we can continue with the interesting stuff. */ void localCollectionFetchResult( KJob *job ) { if ( job->error() ) return; // handled by the base class // safety check: the local tree has to be connected if ( !localPendingCollections.isEmpty() ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Inconsistent local collection tree detected! OMG, what have you done to my database?!?!" ) ); q->emitResult(); return; } localListDone = true; execute(); } /** Find the local node that matches the given remote collection, returns 0 if that doesn't exist (yet). */ LocalNode* findMatchingLocalNode( const Collection &collection ) { if ( !hierarchicalRIDs ) { if ( localRidMap.contains( collection.remoteId() ) ) return localRidMap.value( collection.remoteId() ); return 0; } else { + if ( collection.id() == Collection::root().id() || collection.remoteId() == Collection::root().remoteId() ) + return localRoot; LocalNode *localParent = 0; if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { kWarning() << "Remote collection without valid parent found: " << collection; return 0; } - if ( collection.parentCollection() == Collection::root() ) + if ( collection.parentCollection().id() == Collection::root().id() || collection.parentCollection().remoteId() == Collection::root().remoteId() ) localParent = localRoot; else localParent = findMatchingLocalNode( collection.parentCollection() ); if ( localParent && localParent->childRidMap.contains( collection.remoteId() ) ) return localParent->childRidMap.value( collection.remoteId() ); return 0; } } /** Find the local node that is the nearest ancestor of the given remote collection (when using hierarchical RIDs only, otherwise it's always the local root node). Never returns 0. */ LocalNode* findBestLocalAncestor( const Collection &collection, bool *exactMatch = 0 ) { if ( !hierarchicalRIDs ) return localRoot; if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { kWarning() << "Remote collection without valid parent found: " << collection; return 0; } if ( collection.parentCollection() == Collection::root() ) { if ( exactMatch ) *exactMatch = true; return localRoot; } bool parentIsExact = false; LocalNode *localParent = findBestLocalAncestor( collection.parentCollection(), &parentIsExact ); if ( !parentIsExact ) { if ( exactMatch ) *exactMatch = false; return localParent; } if ( localParent->childRidMap.contains( collection.remoteId() ) ) { if ( exactMatch ) *exactMatch = true; return localParent->childRidMap.value( collection.remoteId() ); } if ( exactMatch ) *exactMatch = false; return localParent; } /** Checks the pending remote nodes attached to the given local root node to see if any of them can be processed by now. If not, they are moved to the closest ancestor available. */ void processPendingRemoteNodes( LocalNode *localRoot ) { QList pendingRemoteNodes( localRoot->pendingRemoteNodes ); localRoot->pendingRemoteNodes.clear(); QHash > pendingCreations; foreach ( RemoteNode *remoteNode, pendingRemoteNodes ) { // step 1: see if we have a matching local node already LocalNode *localNode = findMatchingLocalNode( remoteNode->collection ); if ( localNode ) { Q_ASSERT( !localNode->processed ); // TODO: moving when using global RIDs updateLocalCollection( localNode, remoteNode ); continue; } // step 2: check if we have the parent at least, then we can create it localNode = findMatchingLocalNode( remoteNode->collection.parentCollection() ); if ( localNode ) { pendingCreations[localNode].append( remoteNode ); continue; } // step 3: find the best matching ancestor and enqueue it for later processing localNode = findBestLocalAncestor( remoteNode->collection ); if ( !localNode ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Remote collection without root-terminated ancestor chain provided, fix your resource dude!" ) ); q->emitResult(); return; } localNode->pendingRemoteNodes.append( remoteNode ); } // process the now possible collection creations for ( QHash >::const_iterator it = pendingCreations.constBegin(); it != pendingCreations.constEnd(); ++it ) { createLocalCollections( it.key(), it.value() ); } } /** Performs a local update for the given node pair. */ void updateLocalCollection( LocalNode *localNode, RemoteNode *remoteNode ) { ++pendingJobs; Collection upd( remoteNode->collection ); upd.setId( localNode->collection.id() ); CollectionModifyJob *mod = new CollectionModifyJob( upd, q ); mod->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); connect( mod, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*)) ); localNode->processed = true; } void updateLocalCollectionResult( KJob* job ) { --pendingJobs; if ( job->error() ) return; // handled by the base class RemoteNode* remoteNode = job->property( REMOTE_NODE ).value(); delete remoteNode; checkDone(); } /** Creates local folders for the given local parent and remote nodes. @todo group CollectionCreateJobs into a single one once it supports that */ void createLocalCollections( LocalNode* localParent, QList remoteNodes ) { foreach ( RemoteNode *remoteNode, remoteNodes ) { ++pendingJobs; Collection col( remoteNode->collection ); col.setParentCollection( localParent->collection ); CollectionCreateJob *create = new CollectionCreateJob( col, q ); create->setProperty( LOCAL_NODE, QVariant::fromValue( localParent ) ); create->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); connect( create, SIGNAL(result(KJob*)), q, SLOT(createLocalCollectionResult(KJob*)) ); } } void createLocalCollectionResult( KJob* job ) { --pendingJobs; if ( job->error() ) return; // handled by the base class const Collection newLocal = static_cast( job )->collection(); LocalNode* localNode = createLocalNode( newLocal ); localNode->processed = true; LocalNode* localParent = job->property( LOCAL_NODE ).value(); Q_ASSERT( localParent->childNodes.contains( localNode ) ); RemoteNode* remoteNode = job->property( REMOTE_NODE ).value(); delete remoteNode; processPendingRemoteNodes( localParent ); if ( !hierarchicalRIDs ) processPendingRemoteNodes( localRoot ); checkDone(); } /** Find all local nodes that are not marked as processed. */ Collection::List findUnprocessedLocalCollections( LocalNode *localNode ) { Collection::List rv; if ( !localNode->processed ) { rv.append( localNode->collection ); return rv; } foreach ( LocalNode *child, localNode->childNodes ) rv.append( findUnprocessedLocalCollections( child ) ); return rv; } /** Deletes unprocessed local nodes, in non-incremental mode. */ void deleteUnprocessedLocalNodes() { if ( incremental ) return; Collection::List cols = findUnprocessedLocalCollections( localRoot ); + deleteLocalCollections( cols ); } /** Deletes the given collection list. @todo optimite delete job to support batch operations */ void deleteLocalCollections( const Collection::List &cols ) { foreach ( const Collection &col, cols ) { ++pendingJobs; CollectionDeleteJob *job = new CollectionDeleteJob( col, q ); connect( job, SIGNAL(result(KJob*)), q, SLOT(deleteLocalCollectionsResult(KJob*)) ); } } void deleteLocalCollectionsResult( KJob *job ) { if ( job->error() ) return; // handled by the base class --pendingJobs; checkDone(); } /** Process what's currently available. */ void execute() { if ( !localListDone ) return; processPendingRemoteNodes( localRoot ); if ( !incremental && deliveryDone ) deleteUnprocessedLocalNodes(); if ( !hierarchicalRIDs ) { deleteLocalCollections( removedRemoteCollections ); } else { Collection::List localCols; foreach ( const Collection &c, removedRemoteCollections ) { LocalNode *node = findMatchingLocalNode( c ); if ( node ) localCols.append( node->collection ); } deleteLocalCollections( localCols ); } removedRemoteCollections.clear(); checkDone(); } /** Finds pending remote nodes, which at the end of the day should be an empty set. */ QList findPendingRemoteNodes( LocalNode *localNode ) { QList rv; rv.append( localNode->pendingRemoteNodes ); foreach ( LocalNode *child, localNode->childNodes ) rv.append( findPendingRemoteNodes( child ) ); return rv; } /** Are we there yet?? @todo progress reporting */ void checkDone() { // still running jobs or not fully delivered local/remote state if ( !deliveryDone || pendingJobs > 0 || !localListDone ) return; // safety check: there must be no pending remote nodes anymore QList orphans = findPendingRemoteNodes( localRoot ); if ( !orphans.isEmpty() ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Found unresolved orphan collections" ) ); foreach ( RemoteNode* orphan, orphans ) kDebug() << "found orphan collection:" << orphan->collection; } q->commit(); } CollectionSync *q; QString resourceId; int pendingJobs; LocalNode* localRoot; QHash localUidMap; QHash localRidMap; // temporary during build-up of the local node tree, must be empty afterwards QHash > localPendingCollections; // removed remote collections in incremental mode Collection::List removedRemoteCollections; bool incremental; bool streaming; bool hierarchicalRIDs; bool localListDone; bool deliveryDone; }; CollectionSync::CollectionSync( const QString &resourceId, QObject *parent ) : TransactionSequence( parent ), d( new Private( this ) ) { d->resourceId = resourceId; } CollectionSync::~CollectionSync() { delete d; } void CollectionSync::setRemoteCollections(const Collection::List & remoteCollections) { foreach ( const Collection &c, remoteCollections ) d->createRemoteNode( c ); if ( !d->streaming ) d->deliveryDone = true; d->execute(); } void CollectionSync::setRemoteCollections(const Collection::List & changedCollections, const Collection::List & removedCollections) { d->incremental = true; foreach ( const Collection &c, changedCollections ) d->createRemoteNode( c ); d->removedRemoteCollections += removedCollections; if ( !d->streaming ) d->deliveryDone = true; d->execute(); } void CollectionSync::doStart() { CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); job->fetchScope().setResource( d->resourceId ); connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(localCollectionsReceived(Akonadi::Collection::List)) ); connect( job, SIGNAL(result(KJob*)), SLOT(localCollectionFetchResult(KJob*)) ); } void CollectionSync::setStreamingEnabled( bool streaming ) { d->streaming = streaming; } void CollectionSync::retrievalDone() { d->deliveryDone = true; d->execute(); } void CollectionSync::setHierarchicalRemoteIds( bool hierarchical ) { d->hierarchicalRIDs = hierarchical; } #include "collectionsync_p.moc" diff --git a/akonadi/protocolhelper.cpp b/akonadi/protocolhelper.cpp index 99a7b9b3c..cdd334b0e 100644 --- a/akonadi/protocolhelper.cpp +++ b/akonadi/protocolhelper.cpp @@ -1,229 +1,248 @@ /* Copyright (c) 2008 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "protocolhelper_p.h" #include "attributefactory.h" #include "collectionstatistics.h" #include "exception.h" #include #include #include #include #include #include #include using namespace Akonadi; int ProtocolHelper::parseCachePolicy(const QByteArray & data, CachePolicy & policy, int start) { QVarLengthArray params; int end = Akonadi::ImapParser::parseParenthesizedList( data, params, start ); for ( int i = 0; i < params.count() - 1; i += 2 ) { const QByteArray key = params[i]; const QByteArray value = params[i + 1]; if ( key == "INHERIT" ) policy.setInheritFromParent( value == "true" ); else if ( key == "INTERVAL" ) policy.setIntervalCheckTime( value.toInt() ); else if ( key == "CACHETIMEOUT" ) policy.setCacheTimeout( value.toInt() ); else if ( key == "SYNCONDEMAND" ) policy.setSyncOnDemand( value == "true" ); else if ( key == "LOCALPARTS" ) { QVarLengthArray tmp; QStringList parts; Akonadi::ImapParser::parseParenthesizedList( value, tmp ); for ( int j=0; j attributes; pos = ImapParser::parseParenthesizedList( data, attributes, pos ); for ( int i = 0; i < attributes.count() - 1; i += 2 ) { const QByteArray key = attributes[i]; const QByteArray value = attributes[i + 1]; if ( key == "NAME" ) { collection.setName( QString::fromUtf8( value ) ); } else if ( key == "REMOTEID" ) { collection.setRemoteId( QString::fromUtf8( value ) ); } else if ( key == "RESOURCE" ) { collection.setResource( QString::fromUtf8( value ) ); } else if ( key == "MIMETYPE" ) { QVarLengthArray ct; ImapParser::parseParenthesizedList( value, ct ); QStringList ct2; for ( int j = 0; j < ct.size(); j++ ) ct2 << QString::fromLatin1( ct[j] ); collection.setContentMimeTypes( ct2 ); } else if ( key == "MESSAGES" ) { CollectionStatistics s = collection.statistics(); s.setCount( value.toLongLong() ); collection.setStatistics( s ); } else if ( key == "UNSEEN" ) { CollectionStatistics s = collection.statistics(); s.setUnreadCount( value.toLongLong() ); collection.setStatistics( s ); } else if ( key == "SIZE" ) { CollectionStatistics s = collection.statistics(); s.setSize( value.toLongLong() ); collection.setStatistics( s ); } else if ( key == "CACHEPOLICY" ) { CachePolicy policy; ProtocolHelper::parseCachePolicy( value, policy ); collection.setCachePolicy( policy ); + } else if ( key == "ANCESTORS" ) { + QList ancestors; + ImapParser::parseParenthesizedList( value, ancestors ); + Collection* currentCol = &collection; + foreach ( const QByteArray uidRidPair, ancestors ) { + QList parentIds; + ImapParser::parseParenthesizedList( uidRidPair, parentIds ); + if ( parentIds.size() != 2 ) + break; + const Collection::Id uid = parentIds.at( 0 ).toLongLong(); + const QString rid = QString::fromUtf8( parentIds.at( 1 ) ); + if ( uid == Collection::root().id() ) { + currentCol->setParentCollection( Collection::root() ); + break; + } + currentCol->parentCollection().setId( uid ); + currentCol->parentCollection().setRemoteId( rid ); + currentCol = ¤tCol->parentCollection(); + } } else { Attribute* attr = AttributeFactory::createAttribute( key ); Q_ASSERT( attr ); attr->deserialize( value ); collection.addAttribute( attr ); } } return pos; } QByteArray ProtocolHelper::attributesToByteArray(const Entity & entity, bool ns ) { QList l; foreach ( const Attribute *attr, entity.attributes() ) { l << encodePartIdentifier( ns ? PartAttribute : PartGlobal, attr->type() ); l << ImapParser::quote( attr->serialized() ); } return ImapParser::join( l, " " ); } QByteArray ProtocolHelper::encodePartIdentifier(PartNamespace ns, const QByteArray & label, int version ) { const QByteArray versionString( version != 0 ? '[' + QByteArray::number( version ) + ']' : "" ); switch ( ns ) { case PartGlobal: return label + versionString; case PartPayload: return "PLD:" + label + versionString; case PartAttribute: return "ATR:" + label + versionString; default: Q_ASSERT( false ); } return QByteArray(); } QByteArray ProtocolHelper::decodePartIdentifier( const QByteArray &data, PartNamespace & ns ) { if ( data.startsWith( "PLD:" ) ) { //krazy:exclude=strings ns = PartPayload; return data.mid( 4 ); } else if ( data.startsWith( "ATR:" ) ) { //krazy:exclude=strings ns = PartAttribute; return data.mid( 4 ); } else { ns = PartGlobal; return data; } } QByteArray ProtocolHelper::itemSetToByteArray( const Item::List &_items, const QByteArray &command ) { if ( _items.isEmpty() ) throw Exception( "No items specified" ); Item::List items( _items ); QByteArray rv; std::sort( items.begin(), items.end(), boost::bind( &Item::id, _1 ) < boost::bind( &Item::id, _2 ) ); if ( items.first().isValid() ) { // all items have a uid set rv += " " AKONADI_CMD_UID " "; rv += command; rv += ' '; QList uids; foreach ( const Item &item, items ) uids << item.id(); ImapSet set; set.add( uids ); rv += set.toImapSequenceSet(); } else { // check if all items have a remote id QList rids; foreach ( const Item &item, items ) { if ( item.remoteId().isEmpty() ) throw Exception( i18n( "No remote identifier specified" ) ); rids << ImapParser::quote( item.remoteId().toUtf8() ); } rv += " " AKONADI_CMD_RID " "; rv += command; rv += " ("; rv += ImapParser::join( rids, " " ); rv += ')'; } return rv; } diff --git a/akonadi/session_p.h b/akonadi/session_p.h index 7565767e5..cfed0e56a 100644 --- a/akonadi/session_p.h +++ b/akonadi/session_p.h @@ -1,115 +1,115 @@ /* Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_SESSION_P_H #define AKONADI_SESSION_P_H #include "session.h" #include "imapparser_p.h" #include #include #include #include class QLocalSocket; namespace Akonadi { /** * @internal */ class SessionPrivate { public: SessionPrivate( Session *parent ) : mParent( parent ), mConnectionSettings( 0 ), protocolVersion( 0 ) { parser = new ImapParser(); } ~SessionPrivate() { delete parser; delete mConnectionSettings; } void startNext(); void reconnect(); void socketDisconnected(); void socketError( QLocalSocket::LocalSocketError error ); void dataReceived(); void doStartNext(); void startJob( Job* job ); void jobDone( KJob* job ); void jobWriteFinished( Akonadi::Job* job ); void jobDestroyed( QObject *job ); bool canPipelineNext(); /** * Creates a new default session for this thread with * the given @p sessionId. The session can be accessed * later by defaultSession(). * * You only need to call this method if you want that the * default session has a special custom id, otherwise a random unique * id is used automatically. */ static void createDefaultSession( const QByteArray &sessionId ); /** Associates the given Job object with this session. */ void addJob( Job* job ); /** Returns the next IMAP tag. */ int nextTag(); /** Sends the given raw data. */ void writeData( const QByteArray &data ); - static int minimumProtocolVersion() { return 16; } + static int minimumProtocolVersion() { return 17; } Session *mParent; QByteArray sessionId; QSettings *mConnectionSettings; QLocalSocket* socket; bool connected; int theNextTag; int protocolVersion; // job management QQueue queue; QQueue pipeline; Job* currentJob; bool jobRunning; // parser stuff ImapParser *parser; }; } #endif diff --git a/akonadi/tests/collectionsynctest.cpp b/akonadi/tests/collectionsynctest.cpp index 74851db97..9697f195e 100644 --- a/akonadi/tests/collectionsynctest.cpp +++ b/akonadi/tests/collectionsynctest.cpp @@ -1,260 +1,261 @@ /* Copyright (c) 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "test_utils.h" #include #include #include #include #include #include #include "../akonadi/collectionsync.cpp" #include #include #include #include using namespace Akonadi; Q_DECLARE_METATYPE( KJob* ) class CollectionSyncTest : public QObject { Q_OBJECT private: Collection::List fetchCollections( const QString &res ) { CollectionFetchJob *fetch = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); fetch->fetchScope().setResource( res ); + fetch->fetchScope().setAncestorRetrieval( CollectionFetchScope::All ); Q_ASSERT( fetch->exec() ); Q_ASSERT( !fetch->collections().isEmpty() ); return fetch->collections(); } void makeTestData() { QTest::addColumn( "hierarchicalRIDs" ); QTest::addColumn( "resource" ); QTest::newRow( "akonadi_knut_resource_0 global RID" ) << false << "akonadi_knut_resource_0"; QTest::newRow( "akonadi_knut_resource_1 global RID" ) << false << "akonadi_knut_resource_1"; QTest::newRow( "akonadi_knut_resource_2 global RID" ) << false << "akonadi_knut_resource_2"; -/* QTest::newRow( "akonadi_knut_resource_0 hierarchical RID" ) << true << "akonadi_knut_resource_0"; + QTest::newRow( "akonadi_knut_resource_0 hierarchical RID" ) << true << "akonadi_knut_resource_0"; QTest::newRow( "akonadi_knut_resource_1 hierarchical RID" ) << true << "akonadi_knut_resource_1"; - QTest::newRow( "akonadi_knut_resource_2 hierarchical RID" ) << true << "akonadi_knut_resource_2";*/ + QTest::newRow( "akonadi_knut_resource_2 hierarchical RID" ) << true << "akonadi_knut_resource_2"; } private slots: void initTestCase() { Control::start(); qRegisterMetaType(); // switch all resources offline to reduce interference from them foreach ( Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances() ) agent.setIsOnline( false ); } void testFullSync_data() { makeTestData(); } void testFullSync() { QFETCH( bool, hierarchicalRIDs ); QFETCH( QString, resource ); Collection::List origCols = fetchCollections( resource ); CollectionSync* syncer = new CollectionSync( resource, this ); syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setRemoteCollections( origCols ); AKVERIFYEXEC( syncer ); Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); } void testFullStreamingSync_data() { makeTestData(); } void testFullStreamingSync() { QFETCH( bool, hierarchicalRIDs ); QFETCH( QString, resource ); Collection::List origCols = fetchCollections( resource ); CollectionSync* syncer = new CollectionSync( resource, this ); syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setAutoDelete( false ); QSignalSpy spy( syncer, SIGNAL(result(KJob*)) ); QVERIFY( spy.isValid() ); syncer->setStreamingEnabled( true ); QTest::qWait( 10 ); QCOMPARE( spy.count(), 0 ); for ( int i = 0; i < origCols.count(); ++i ) { Collection::List l; l << origCols[i]; syncer->setRemoteCollections( l ); if ( i < origCols.count() - 1 ) QTest::qWait( 10 ); // enter the event loop so itemsync actually can do something QCOMPARE( spy.count(), 0 ); } syncer->retrievalDone(); QTest::qWait( 1000 ); // let it finish its job QCOMPARE( spy.count(), 1 ); KJob *job = spy.at( 0 ).at( 0 ).value(); QCOMPARE( job, syncer ); QCOMPARE( job->errorText(), QString() ); QCOMPARE( job->error(), 0 ); Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); delete syncer; } void testIncrementalSync_data() { makeTestData(); } void testIncrementalSync() { QFETCH( bool, hierarchicalRIDs ); QFETCH( QString, resource ); if ( resource == QLatin1String( "akonadi_knut_resource_2" ) ) QSKIP( "test requires more than one collection", SkipSingle ); Collection::List origCols = fetchCollections( resource ); CollectionSync* syncer = new CollectionSync( resource, this ); syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setRemoteCollections( origCols, Collection::List() ); AKVERIFYEXEC( syncer ); Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); Collection::List delCols; delCols << resultCols.front(); resultCols.pop_front(); // ### not implemented yet I guess #if 0 Collection colWithOnlyRemoteId; colWithOnlyRemoteId.setRemoteId( resultCols.front().remoteId() ); delCols << colWithOnlyRemoteId; resultCols.pop_front(); #endif #if 0 // ### should this work? Collection colWithRandomRemoteId; colWithRandomRemoteId.setRemoteId( KRandom::randomString( 100 ) ); delCols << colWithRandomRemoteId; #endif syncer = new CollectionSync( resource, this ); syncer->setRemoteCollections( resultCols, delCols ); AKVERIFYEXEC( syncer ); Collection::List resultCols2 = fetchCollections( resource ); QCOMPARE( resultCols2.count(), resultCols.count() ); } void testIncrementalStreamingSync_data() { makeTestData(); } void testIncrementalStreamingSync() { QFETCH( bool, hierarchicalRIDs ); QFETCH( QString, resource ); Collection::List origCols = fetchCollections( resource ); CollectionSync* syncer = new CollectionSync( resource, this ); syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setAutoDelete( false ); QSignalSpy spy( syncer, SIGNAL(result(KJob*)) ); QVERIFY( spy.isValid() ); syncer->setStreamingEnabled( true ); QTest::qWait( 10 ); QCOMPARE( spy.count(), 0 ); for ( int i = 0; i < origCols.count(); ++i ) { Collection::List l; l << origCols[i]; syncer->setRemoteCollections( l, Collection::List() ); if ( i < origCols.count() - 1 ) QTest::qWait( 10 ); // enter the event loop so itemsync actually can do something QCOMPARE( spy.count(), 0 ); } syncer->retrievalDone(); QTest::qWait( 1000 ); // let it finish its job QCOMPARE( spy.count(), 1 ); KJob *job = spy.at( 0 ).at( 0 ).value(); QCOMPARE( job, syncer ); QCOMPARE( job->errorText(), QString() ); QCOMPARE( job->error(), 0 ); Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); delete syncer; } void testEmptyIncrementalSync_data() { makeTestData(); } void testEmptyIncrementalSync() { QFETCH( bool, hierarchicalRIDs ); QFETCH( QString, resource ); Collection::List origCols = fetchCollections( resource ); CollectionSync* syncer = new CollectionSync( resource, this ); syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setRemoteCollections( Collection::List(), Collection::List() ); AKVERIFYEXEC( syncer ); Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); } }; QTEST_AKONADIMAIN( CollectionSyncTest, NoGUI ) #include "collectionsynctest.moc" diff --git a/akonadi/tests/protocolhelpertest.cpp b/akonadi/tests/protocolhelpertest.cpp index 951de3761..57a6ad4fe 100644 --- a/akonadi/tests/protocolhelpertest.cpp +++ b/akonadi/tests/protocolhelpertest.cpp @@ -1,71 +1,107 @@ /* Copyright (c) 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "test_utils.h" #include "protocolhelper.cpp" using namespace Akonadi; class ProtocolHelperTest : public QObject { Q_OBJECT private slots: void testItemSetToByteArray_data() { QTest::addColumn( "items" ); QTest::addColumn( "result" ); QTest::addColumn( "shouldThrow" ); Item u1; u1.setId( 1 ); Item u2; u2.setId( 2 ); Item u3; u3.setId( 3 ); Item r1; r1.setRemoteId( "A" ); Item r2; r2.setRemoteId( "B" ); QTest::newRow( "empty" ) << Item::List() << QByteArray() << true; QTest::newRow( "single uid" ) << (Item::List() << u1) << QByteArray( " UID CMD 1" ) << false; QTest::newRow( "multi uid" ) << (Item::List() << u1 << u3) << QByteArray( " UID CMD 1,3" ) << false; QTest::newRow( "block uid" ) << (Item::List() << u1 << u2 << u3) << QByteArray( " UID CMD 1:3" ) << false; QTest::newRow( "single rid" ) << (Item::List() << r1) << QByteArray( " RID CMD (\"A\")" ) << false; QTest::newRow( "multi rid" ) << (Item::List() << r1 << r2) << QByteArray( " RID CMD (\"A\" \"B\")" ) << false; QTest::newRow( "invalid" ) << (Item::List() << Item()) << QByteArray() << true; QTest::newRow( "mixed" ) << (Item::List() << u1 << r1) << QByteArray() << true; } void testItemSetToByteArray() { QFETCH( Item::List, items ); QFETCH( QByteArray, result ); QFETCH( bool, shouldThrow ); bool didThrow = false; try { const QByteArray r = ProtocolHelper::itemSetToByteArray( items, "CMD" ); QCOMPARE( r, result ); } catch ( const std::exception &e ) { qDebug() << e.what(); didThrow = true; } QCOMPARE( didThrow, shouldThrow ); } + + void testCollectionParsing_data() + { + QTest::addColumn( "input" ); + QTest::addColumn( "collection" ); + + QByteArray b = "3 2 (REMOTEID \"r3\" ANCESTORS ((2 \"r2\") (1 \"r1\") (0 \"\")))"; + Collection c; + c.setId( 3 ); + c.setRemoteId( "r3" ); + c.parentCollection().setId( 2 ); + c.parentCollection().setRemoteId( "r2" ); + c.parentCollection().parentCollection().setId( 1 ); + c.parentCollection().parentCollection().setRemoteId( "r1" ); + c.parentCollection().parentCollection().setParentCollection( Collection::root() ); + + QTest::newRow( "ancestors" ) << b << c; + } + + void testCollectionParsing() + { + QFETCH( QByteArray, input ); + QFETCH( Collection, collection ); + + Collection parsedCollection; + ProtocolHelper::parseCollection( input, parsedCollection ); + + while ( collection.isValid() || parsedCollection.isValid() ) { + QCOMPARE( parsedCollection.id(), collection.id() ); + QCOMPARE( parsedCollection.remoteId(), collection.remoteId() ); + const Collection p1( parsedCollection.parentCollection() ); + const Collection p2( collection.parentCollection() ); + parsedCollection = p1; + collection = p2; + } + } }; QTEST_KDEMAIN( ProtocolHelperTest, NoGUI ) #include "protocolhelpertest.moc"