diff --git a/akonadi/CMakeLists.txt b/akonadi/CMakeLists.txt index f907be9f4..55ad9e6a7 100644 --- a/akonadi/CMakeLists.txt +++ b/akonadi/CMakeLists.txt @@ -1,269 +1,271 @@ project(akonadi-kde) add_definitions( -DKDE_DEFAULT_DEBUG_AREA=5250 ) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}" ) if(CMAKE_COMPILE_GCOV) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") endif(CMAKE_COMPILE_GCOV) if (KDE4_BUILD_TESTS) # only with this macro the AKONADI_TESTS_EXPORT macro will do something add_definitions(-DCOMPILING_TESTS) add_subdirectory( tests ) endif (KDE4_BUILD_TESTS) add_definitions( -DQT_NO_CAST_FROM_ASCII ) add_definitions( -DQT_NO_CAST_TO_ASCII ) add_subdirectory( kabc ) add_subdirectory( kmime ) include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${QT_QTDBUS_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ${KDE4_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR}/akonadi/private ) # libakonadi-kde set( akonadikde_LIB_SRC entity.cpp # keep it at top to not break enable-final agentbase.cpp agentfilterproxymodel.cpp agentinstance.cpp agentinstancecreatejob.cpp agentinstancemodel.cpp agentinstancewidget.cpp agentmanager.cpp agenttype.cpp agenttypemodel.cpp agenttypewidget.cpp agenttypedialog.cpp attribute.cpp attributefactory.cpp cachepolicy.cpp cachepolicypage.cpp changerecorder.cpp collection.cpp collectioncopyjob.cpp collectioncreatejob.cpp collectiondeletejob.cpp collectiondialog.cpp collectionfilterproxymodel.cpp collectiongeneralpropertiespage.cpp collectionfetchjob.cpp collectionfetchscope.cpp collectionmodel.cpp collectionmodel_p.cpp collectionmodifyjob.cpp collectionmovejob.cpp collectionpathresolver.cpp collectionpropertiesdialog.cpp collectionpropertiespage.cpp collectionrequester.cpp collectionrightsattribute.cpp collectionselectjob.cpp collectionstatistics.cpp collectionstatisticsdelegate.cpp collectionstatisticsjob.cpp collectionstatisticsmodel.cpp collectionsync.cpp collectionview.cpp control.cpp descendantsproxymodel.cpp entitycache.cpp entitydisplayattribute.cpp entityhiddenattribute.cpp entitytreemodel.cpp entitytreemodel_p.cpp entityfilterproxymodel.cpp entitytreeviewstatesaver.cpp erroroverlay.cpp exception.cpp favoritecollectionsmodel.cpp firstrun.cpp flatcollectionproxymodel.cpp item.cpp itemcreatejob.cpp itemcopyjob.cpp itemdeletejob.cpp itemfetchjob.cpp itemfetchscope.cpp itemmodel.cpp itemmonitor.cpp itemmovejob.cpp itemsearchjob.cpp itemserializer.cpp itemserializerplugin.cpp itemmodifyjob.cpp itemsync.cpp itemview.cpp job.cpp linkjob.cpp filteractionjob.cpp mimetypechecker.cpp monitor.cpp monitor_p.cpp + partfetcher.cpp pastehelper.cpp preprocessorbase.cpp protocolhelper.cpp resourcebase.cpp resourcescheduler.cpp resourceselectjob.cpp resourcesynchronizationjob.cpp searchcreatejob.cpp selectionproxymodel.cpp selftestdialog.cpp session.cpp servermanager.cpp standardactionmanager.cpp statisticsproxymodel.cpp statisticstooltipproxymodel.cpp subscriptionjob.cpp subscriptionchangeproxymodel.cpp subscriptiondialog.cpp subscriptionmodel.cpp transactionjobs.cpp transactionsequence.cpp transportresourcebase.cpp unlinkjob.cpp # Temporary until ported to Qt-plugin framework pluginloader.cpp ) # DBus interfaces and adaptors set(akonadi_xml ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.NotificationManager.xml) set_source_files_properties(${akonadi_xml} PROPERTIES INCLUDE "notificationmessage_p.h") qt4_add_dbus_interface( akonadikde_LIB_SRC ${akonadi_xml} notificationmanagerinterface ) qt4_add_dbus_interfaces( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.AgentManager.xml ) qt4_add_dbus_interfaces( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Tracer.xml ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Resource.xml resourcebase.h Akonadi::ResourceBase ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Preprocessor.xml preprocessorbase.h Akonadi::PreprocessorBase ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Agent.Status.xml agentbase.h Akonadi::AgentBase ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Agent.Control.xml agentbase.h Akonadi::AgentBase ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC interfaces/org.freedesktop.Akonadi.Resource.Transport.xml transportresourcebase_p.h Akonadi::TransportResourceBasePrivate ) kde4_add_ui_files( akonadikde_LIB_SRC cachepolicypage.ui collectiongeneralpropertiespage.ui subscriptiondialog.ui controlprogressindicator.ui selftestdialog.ui ) kde4_add_library( akonadi-kde SHARED ${akonadikde_LIB_SRC} ) macro_ensure_version( "4.2.0" ${KDE_VERSION} KDE_IS_AT_LEAST_42 ) target_link_libraries( akonadi-kde ${KDE4_SOLID_LIBS} ${QT_QTNETWORK_LIBRARY} ${QT_QTDBUS_LIBRARY} ${QT_QTSQL_LIBRARY} ${KDE4_KDEUI_LIBS} ${KDE4_KIO_LIBS} ${AKONADI_COMMON_LIBRARIES} ) set( AKONADI_KDE_DEPS ${KDE4_KDEUI_LIBS} ${QT_QTDBUS_LIBRARY} ${QT_QTCORE_LIBRARY} ) if(${KDE_IS_AT_LEAST_42}) target_link_libraries( akonadi-kde LINK_INTERFACE_LIBRARIES ${AKONADI_KDE_DEPS}) else(${KDE_IS_AT_LEAST_42}) target_link_libraries( akonadi-kde ${AKONADI_KDE_DEPS}) endif(${KDE_IS_AT_LEAST_42}) set_target_properties( akonadi-kde PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) install( TARGETS akonadi-kde EXPORT kdepimlibsLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS} ) ########### install files ############### install( FILES akonadi_export.h agentbase.h agentfilterproxymodel.h agentinstance.h agentinstancecreatejob.h agentinstancemodel.h agentinstancewidget.h agentmanager.h agenttype.h agenttypemodel.h agenttypewidget.h agenttypedialog.h attribute.h attributefactory.h cachepolicy.h changerecorder.h collection.h collectioncopyjob.h collectioncreatejob.h collectiondeletejob.h collectiondialog.h collectionfilterproxymodel.h collectionfetchjob.h collectionfetchscope.h collectionmodel.h collectionmodifyjob.h collectionpropertiesdialog.h collectionpropertiespage.h collectionrequester.h collectionstatisticsdelegate.h collectionstatisticsmodel.h collectionstatistics.h collectionstatisticsjob.h collectionview.h control.h descendantsproxymodel.h entity.h entitydisplayattribute.h entityhiddenattribute.h entitytreemodel.h entityfilterproxymodel.h entitytreeviewstatesaver.h exception.h favoritecollectionsmodel.h item.h itemcreatejob.h itemcopyjob.h itemdeletejob.h itemfetchjob.h itemfetchscope.h itemmodel.h itemmodifyjob.h itemmonitor.h itemmovejob.h itempayloadinternals_p.h itemsearchjob.h itemserializerplugin.h itemsync.h itemview.h job.h linkjob.h filteractionjob.h mimetypechecker.h monitor.h - qtest_akonadi.h + partfetcher.h preprocessorbase.h + qtest_akonadi.h resourcebase.h resourcesynchronizationjob.h searchcreatejob.h selectionproxymodel.h session.h servermanager.h standardactionmanager.h statisticsproxymodel.h statisticstooltipproxymodel.h transactionjobs.h transactionsequence.h transportresourcebase.h unlinkjob.h DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi COMPONENT Devel ) install( FILES collectionpathresolver_p.h DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi/private COMPONENT Devel ) install( FILES kcfg2dbus.xsl DESTINATION ${DATA_INSTALL_DIR}/akonadi-kde ) diff --git a/akonadi/collectionfetchjob.cpp b/akonadi/collectionfetchjob.cpp index e1d290986..60e8102bd 100644 --- a/akonadi/collectionfetchjob.cpp +++ b/akonadi/collectionfetchjob.cpp @@ -1,263 +1,272 @@ /* 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 "collectionutils_p.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 ) { CollectionFetchJob *subJob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); subJob->setFetchScope( fetchScope() ); } 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->mBase.isValid() ) { + if ( CollectionUtils::hasValidHierarchicalRID( d->mBase ) ) + command += " HRID"; + else + command += " " AKONADI_CMD_RID; + } if ( d->mScope.includeUnubscribed() ) command += " LIST "; else command += " LSUB "; + if ( d->mBase.isValid() ) command += QByteArray::number( d->mBase.id() ); + else if ( CollectionUtils::hasValidHierarchicalRID( d->mBase ) ) + command += '(' + ProtocolHelper::hierarchicalRidToByteArray( d->mBase ) + ')'; 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() ) { filter.append( "RESOURCE" ); filter.append( d->mScope.resource().toUtf8() ); } if ( !d->mScope.contentMimeTypes().isEmpty() ) { filter.append( "MIMETYPE" ); QList mts; foreach ( const QString &mt, d->mScope.contentMimeTypes() ) mts.append( mt.toUtf8() ); filter.append( '(' + ImapParser::join( mts, " " ) + ')' ); } QList options; if ( d->mScope.includeStatistics() ) { 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 += 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/collectionutils_p.h b/akonadi/collectionutils_p.h index 19279de9c..5070e84d3 100644 --- a/akonadi/collectionutils_p.h +++ b/akonadi/collectionutils_p.h @@ -1,90 +1,99 @@ /* Copyright (c) 2008 Tobias Koenig 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_COLLECTIONUTILS_P_H #define AKONADI_COLLECTIONUTILS_P_H #include namespace Akonadi { /** * @internal */ namespace CollectionUtils { inline bool isVirtualParent( const Collection &collection ) { return (collection.parentCollection() == Collection::root() && collection.resource() == QLatin1String( "akonadi_search_resource" )); } inline bool isVirtual( const Collection &collection ) { return (collection.resource() == QLatin1String( "akonadi_search_resource" )); } inline bool isResource( const Collection &collection ) { return (collection.parentCollection() == Collection::root()); } inline bool isStructural( const Collection &collection ) { return collection.contentMimeTypes().isEmpty(); } inline bool isFolder( const Collection &collection ) { return (collection.parentCollection() != Collection::root() && collection.resource() != QLatin1String( "akonadi_search_resource" ) && !collection.contentMimeTypes().isEmpty()); } inline QString defaultIconName( const Collection &col ) { if ( CollectionUtils::isVirtualParent( col ) ) return QLatin1String( "edit-find" ); if ( CollectionUtils::isVirtual( col ) ) return QLatin1String( "document-preview" ); if ( CollectionUtils::isResource( col ) ) return QLatin1String( "network-server" ); if ( CollectionUtils::isStructural( col ) ) return QLatin1String( "folder-grey" ); const QStringList content = col.contentMimeTypes(); if ( content.size() == 1 || (content.size() == 2 && content.contains( Collection::mimeType() )) ) { if ( content.contains( QLatin1String( "text/x-vcard" ) ) || content.contains( QLatin1String( "text/directory" ) ) || content.contains( QLatin1String( "text/vcard" ) ) ) return QLatin1String( "x-office-address-book" ); // TODO: add all other content types and/or fix their mimetypes if ( content.contains( QLatin1String( "akonadi/event" ) ) || content.contains( QLatin1String( "text/ical" ) ) ) return QLatin1String( "view-pim-calendar" ); if ( content.contains( QLatin1String( "akonadi/task" ) ) ) return QLatin1String( "view-pim-tasks" ); } else if ( content.isEmpty() ) { return QLatin1String( "folder-grey" ); } return QLatin1String( "folder" ); } + + inline bool hasValidHierarchicalRID( const Collection &col ) + { + if ( col == Collection::root() ) + return true; + if ( col.remoteId().isEmpty() ) + return false; + return hasValidHierarchicalRID( col.parentCollection() ); + } } } #endif diff --git a/akonadi/entitytreemodel.cpp b/akonadi/entitytreemodel.cpp index 0c6d93e34..2267de542 100644 --- a/akonadi/entitytreemodel.cpp +++ b/akonadi/entitytreemodel.cpp @@ -1,948 +1,960 @@ /* Copyright (c) 2008 Stephen Kelly 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 "entitytreemodel.h" #include "entitytreemodel_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include "collectionfetchscope.h" #include "collectionutils_p.h" #include "kdebug.h" using namespace Akonadi; EntityTreeModel::EntityTreeModel( Session *session, Monitor *monitor, QObject *parent ) : QAbstractItemModel( parent ), d_ptr( new EntityTreeModelPrivate( this ) ) { Q_D( EntityTreeModel ); d->m_monitor = monitor; d->m_session = session; d->m_includeStatistics = true; d->m_monitor->fetchCollectionStatistics( true ); + d->m_monitor->collectionFetchScope().setAncestorRetrieval( Akonadi::CollectionFetchScope::All ); d->m_mimeChecker.setWantedMimeTypes( d->m_monitor->mimeTypesMonitored() ); connect( monitor, SIGNAL( mimeTypeMonitored( const QString&, bool ) ), SLOT( monitoredMimeTypeChanged( const QString&, bool ) ) ); // monitor collection changes connect( monitor, SIGNAL( collectionChanged( const Akonadi::Collection& ) ), SLOT( monitoredCollectionChanged( const Akonadi::Collection& ) ) ); connect( monitor, SIGNAL( collectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ) ), SLOT( monitoredCollectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ) ) ); connect( monitor, SIGNAL( collectionRemoved( const Akonadi::Collection& ) ), SLOT( monitoredCollectionRemoved( const Akonadi::Collection&) ) ); // connect( monitor, // SIGNAL( collectionMoved( const Akonadi::Collection &, const Akonadi::Collection &, const Akonadi::Collection & ) ), // SLOT( monitoredCollectionMoved( const Akonadi::Collection &, const Akonadi::Collection &, const Akonadi::Collection & ) ) ); //TODO: Figure out if the monitor emits these signals even without an item fetch scope. // Wrap them in an if() if so. // Don't want to be adding items to a model if NoItemPopulation is set. // If LazyPopulation is set, then we'll have to add items to collections which // have already been lazily populated. // Monitor item changes. connect( monitor, SIGNAL( itemAdded( const Akonadi::Item&, const Akonadi::Collection& ) ), SLOT( monitoredItemAdded( const Akonadi::Item&, const Akonadi::Collection& ) ) ); connect( monitor, SIGNAL( itemChanged( const Akonadi::Item&, const QSet& ) ), SLOT( monitoredItemChanged( const Akonadi::Item&, const QSet& ) ) ); connect( monitor, SIGNAL( itemRemoved( const Akonadi::Item& ) ), SLOT( monitoredItemRemoved( const Akonadi::Item& ) ) ); //connect( monitor, SIGNAL( itemMoved( const Akonadi::Item, const Akonadi::Collection, const Akonadi::Collection ) ), // SLOT( monitoredItemMoved( const Akonadi::Item, const Akonadi::Collection, const Akonadi::Collection ) ) ); connect( monitor, SIGNAL( collectionStatisticsChanged( Akonadi::Collection::Id, const Akonadi::CollectionStatistics& ) ), SLOT(monitoredCollectionStatisticsChanged( Akonadi::Collection::Id, const Akonadi::CollectionStatistics& ) ) ); connect( monitor, SIGNAL( itemLinked( const Akonadi::Item&, const Akonadi::Collection& )), SLOT( monitoredItemLinked( const Akonadi::Item&, const Akonadi::Collection& ))); connect( monitor, SIGNAL( itemUnlinked( const Akonadi::Item&, const Akonadi::Collection& )), SLOT( monitoredItemUnlinked( const Akonadi::Item&, const Akonadi::Collection& ))); // connect( q, SIGNAL( modelReset() ), q, SLOT( slotModelReset() ) ); d->m_rootCollection = Collection::root(); d->m_rootCollectionDisplayName = QLatin1String( "[*]" ); // Initializes the model cleanly. clearAndReset(); } EntityTreeModel::~EntityTreeModel() { Q_D( EntityTreeModel ); foreach( QList list, d->m_childEntities ) { qDeleteAll(list); list.clear(); } delete d_ptr; } void EntityTreeModel::clearAndReset() { Q_D( EntityTreeModel ); d->m_collections.clear(); d->m_items.clear(); d->m_childEntities.clear(); reset(); QTimer::singleShot( 0, this, SLOT( startFirstListJob() ) ); } Collection EntityTreeModel::collectionForId( Collection::Id id ) const { Q_D( const EntityTreeModel ); return d->m_collections.value( id ); } Item EntityTreeModel::itemForId( Item::Id id ) const { Q_D( const EntityTreeModel ); return d->m_items.value( id ); } int EntityTreeModel::columnCount( const QModelIndex & parent ) const { // TODO: Statistics? if ( parent.isValid() && parent.column() != 0 ) return 0; return qMax( getColumnCount( CollectionTreeHeaders ), getColumnCount( ItemListHeaders ) ); } QVariant EntityTreeModel::getData( const Item &item, int column, int role ) const { if ( column == 0 ) { switch ( role ) { case Qt::DisplayRole: case Qt::EditRole: if ( item.hasAttribute() && !item.attribute()->displayName().isEmpty() ) { return item.attribute()->displayName(); } else { return item.remoteId(); } break; case Qt::DecorationRole: if ( item.hasAttribute() && !item.attribute()->iconName().isEmpty() ) return item.attribute()->icon(); break; - case MimeTypeRole: - return item.mimeType(); - break; - case RemoteIdRole: - return item.remoteId(); - break; - case ItemRole: - return QVariant::fromValue( item ); - break; - case ItemIdRole: - return item.id(); - break; default: break; } } return QVariant(); } QVariant EntityTreeModel::getData( const Collection &collection, int column, int role ) const { Q_D(const EntityTreeModel); if ( column > 0 ) return QString(); if ( collection == Collection::root() ) { // Only display the root collection. It may not be edited. if ( role == Qt::DisplayRole ) return d->m_rootCollectionDisplayName; if ( role == Qt::EditRole ) return QVariant(); } if ( column == 0 && (role == Qt::DisplayRole || role == Qt::EditRole) ) { if ( collection.hasAttribute() && !collection.attribute()->displayName().isEmpty() ) return collection.attribute()->displayName(); return collection.name(); } switch ( role ) { case Qt::DisplayRole: case Qt::EditRole: if ( column == 0 ) { if ( collection.hasAttribute() && !collection.attribute()->displayName().isEmpty() ) { return collection.attribute()->displayName(); } return collection.name(); } break; case Qt::DecorationRole: if ( collection.hasAttribute() && !collection.attribute()->iconName().isEmpty() ) { return collection.attribute()->icon(); } return KIcon( CollectionUtils::defaultIconName( collection ) ); - break; - case MimeTypeRole: - return collection.mimeType(); - break; - case RemoteIdRole: - return collection.remoteId(); - break; - case CollectionIdRole: - return collection.id(); - break; - case CollectionRole: { - return QVariant::fromValue( collection ); - break; - } default: break; } return QVariant(); } QVariant EntityTreeModel::data( const QModelIndex & index, int role ) const { + Q_D( const EntityTreeModel ); + if ( role == SessionRole ) + return QVariant::fromValue( qobject_cast( d->m_session ) ); + const int headerSet = (role / TerminalUserRole); role %= TerminalUserRole; if ( !index.isValid() ) { if (ColumnCountRole != role) return QVariant(); return getColumnCount(headerSet); } if (ColumnCountRole == role) return getColumnCount(headerSet); - Q_D( const EntityTreeModel ); const Node *node = reinterpret_cast( index.internalPointer() ); if (ParentCollectionRole == role) { const Collection parentCollection = d->m_collections.value( node->parent ); Q_ASSERT(parentCollection.isValid()); return QVariant::fromValue(parentCollection); } if ( Node::Collection == node->type ) { const Collection collection = d->m_collections.value( node->id ); if ( !collection.isValid() ) return QVariant(); - return getData( collection, index.column(), role ); + switch ( role ) + { + case MimeTypeRole: + return collection.mimeType(); + case RemoteIdRole: + return collection.remoteId(); + case CollectionIdRole: + return collection.id(); + case CollectionRole: + return QVariant::fromValue( collection ); + default: + return getData( collection, index.column(), role ); + } + } else if ( Node::Item == node->type ) { const Item item = d->m_items.value( node->id ); if ( !item.isValid() ) return QVariant(); - return getData( item, index.column(), role ); + switch ( role ) + { + case MimeTypeRole: + return item.mimeType(); + break; + case RemoteIdRole: + return item.remoteId(); + break; + case ItemRole: + return QVariant::fromValue( item ); + break; + case ItemIdRole: + return item.id(); + break; + case LoadedPartsRole: + return QVariant::fromValue( item.loadedPayloadParts() ); + case AvailablePartsRole: + return QVariant::fromValue( item.availablePayloadParts() ); + default: + return getData( item, index.column(), role ); + } } return QVariant(); } Qt::ItemFlags EntityTreeModel::flags( const QModelIndex & index ) const { Q_D( const EntityTreeModel ); // Pass modeltest. // http://labs.trolltech.com/forums/topic/79 if ( !index.isValid() ) return 0; Qt::ItemFlags flags = QAbstractItemModel::flags( index ); // Only show and enable items in columns other than 0. if ( index.column() != 0 ) return flags; const Node *node = reinterpret_cast(index.internalPointer()); if ( Node::Collection == node->type ) { const Collection collection = d->m_collections.value( node->id ); if ( collection.isValid() ) { if ( collection == Collection::root() ) { // Selectable and displayable only. return flags; } const int rights = collection.rights(); if ( rights & Collection::CanChangeCollection ) { flags |= Qt::ItemIsEditable; // Changing the collection includes changing the metadata (child entityordering). // Need to allow this by drag and drop. flags |= Qt::ItemIsDropEnabled; } if ( rights & Collection::CanDeleteCollection ) { // If this collection is moved, it will need to be deleted flags |= Qt::ItemIsDragEnabled; } if ( rights & ( Collection::CanCreateCollection | Collection::CanCreateItem ) ) { // Can we drop new collections and items into this collection? flags |= Qt::ItemIsDropEnabled; } } } else if ( Node::Item == node->type ) { // Rights come from the parent collection. const Node *parentNode = reinterpret_cast( index.parent().internalPointer() ); // TODO: Is this right for the root collection? I think so, but only by chance. // But will it work if m_rootCollection is different from Collection::root? // Should probably rely on index.parent().isValid() for that. const Collection parentCollection = d->m_collections.value( parentNode->id ); if ( parentCollection.isValid() ) { const int rights = parentCollection.rights(); // Can't drop onto items. if ( rights & Collection::CanChangeItem ) { flags = flags | Qt::ItemIsEditable; } if ( rights & Collection::CanDeleteItem ) { // If this item is moved, it will need to be deleted from its parent. flags = flags | Qt::ItemIsDragEnabled; } } } return flags; } Qt::DropActions EntityTreeModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList EntityTreeModel::mimeTypes() const { // TODO: Should this return the mimetypes that the items provide? Allow dragging a contact from here for example. return QStringList() << QLatin1String( "text/uri-list" ); } bool EntityTreeModel::dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ) { Q_D( EntityTreeModel ); // TODO Use action and collection rights and return false if necessary // if row and column are -1, then the drop was on parent directly. // data should then be appended on the end of the items of the collections as appropriate. // That will mean begin insert rows etc. // Otherwise it was a sibling of the row^th item of parent. // That will need to be handled by a proxy model. This one can't handle ordering. // if parent is invalid the drop occurred somewhere on the view that is no model, and corresponds to the root. kDebug() << "ismove" << ( action == Qt::MoveAction ); if ( action == Qt::IgnoreAction ) return true; // Shouldn't do this. Need to be able to drop vcards for example. // if (!data->hasFormat("text/uri-list")) // return false; // TODO This is probably wrong and unnecessary. if ( column > 0 ) return false; const Node *node = reinterpret_cast( parent.internalId() ); if ( Node::Item == node->type ) { // Can't drop data onto an item, although we can drop data between items. return false; // TODO: Maybe if it's a drop on an item I should drop below the item instead? // Find out what others do. } if ( Node::Collection == node->type ) { const Collection destCollection = d->m_collections.value( node->id ); // Applications can't create new collections in root. Only resources can. if ( destCollection == Collection::root() ) return false; if ( data->hasFormat( QLatin1String( "text/uri-list" ) ) ) { MimeTypeChecker mimeChecker; mimeChecker.setWantedMimeTypes( destCollection.contentMimeTypes() ); TransactionSequence *transaction = new TransactionSequence( d->m_session ); const KUrl::List urls = KUrl::List::fromMimeData( data ); foreach ( const KUrl &url, urls ) { const Collection collection = d->m_collections.value( Collection::fromUrl( url ).id() ); if ( collection.isValid() ) { if ( !mimeChecker.isWantedCollection( collection ) ) return false; if ( Qt::MoveAction == action ) { // new CollectionMoveJob(col, destCol, transaction); } else if ( Qt::CopyAction == action ) { CollectionCopyJob *collectionCopyJob = new CollectionCopyJob( collection, destCollection, transaction ); connect( collectionCopyJob, SIGNAL( result( KJob* ) ), SLOT( copyJobDone( KJob* ) ) ); } } else { const Item item = d->m_items.value( Item::fromUrl( url ).id() ); if ( item.isValid() ) { if ( Qt::MoveAction == action ) { ItemMoveJob *itemMoveJob = new ItemMoveJob( item, destCollection, transaction ); connect( itemMoveJob, SIGNAL( result( KJob* ) ), SLOT( moveJobDone( KJob* ) ) ); } else if ( Qt::CopyAction == action ) { ItemCopyJob *itemCopyJob = new ItemCopyJob( item, destCollection, transaction); connect( itemCopyJob, SIGNAL( result( KJob* ) ), SLOT( copyJobDone( KJob* ) ) ); } } else { // A uri, but not an akonadi url. What to do? // Should handle known mimetypes like vcards first. // That should make any remaining uris meaningless at this point. } } } return false; // ### Return false so that the view does not update with the dropped // in place where they were dropped. That will be done when the monitor notifies the model // through collectionsReceived that the move was successful. } else { // not a set of uris. Maybe vcards etc. Check if the parent supports them, and maybe do // fromMimeData for them. Hmm, put it in the same transaction with the above? // TODO: This should be handled first, not last. } } return false; } QModelIndex EntityTreeModel::index( int row, int column, const QModelIndex & parent ) const { Q_D( const EntityTreeModel ); if ( parent.column() > 0 ) { return QModelIndex(); } //TODO: don't use column count here? Use some d-> func. if ( column >= columnCount() || column < 0 ) return QModelIndex(); QList childEntities; const Node *parentNode = reinterpret_cast( parent.internalPointer() ); if ( !parentNode || !parent.isValid() ) { if ( d->m_showRootCollection ) childEntities << d->m_childEntities.value( -1 ); else childEntities = d->m_childEntities.value( d->m_rootCollection.id() ); } else { if ( parentNode->id >= 0 ) childEntities = d->m_childEntities.value( parentNode->id ); } const int size = childEntities.size(); if ( row < 0 || row >= size ) return QModelIndex(); Node *node = childEntities.at( row ); return createIndex( row, column, reinterpret_cast( node ) ); } QModelIndex EntityTreeModel::parent( const QModelIndex & index ) const { Q_D( const EntityTreeModel ); if ( !index.isValid() ) return QModelIndex(); const Node *node = reinterpret_cast( index.internalPointer() ); if ( !node ) return QModelIndex(); const Collection collection = d->m_collections.value( node->parent ); if ( !collection.isValid() ) return QModelIndex(); if ( collection.id() == d->m_rootCollection.id() ) { if ( !d->m_showRootCollection ) return QModelIndex(); else return createIndex( 0, 0, reinterpret_cast( d->m_rootNode ) ); } const int row = d->indexOf( d->m_childEntities.value( collection.parentCollection().id()), collection.id() ); Node *parentNode = d->m_childEntities.value( collection.parentCollection().id() ).at( row ); return createIndex( row, 0, reinterpret_cast( parentNode ) ); } int EntityTreeModel::rowCount( const QModelIndex & parent ) const { Q_D( const EntityTreeModel ); const Node *node = reinterpret_cast( parent.internalPointer() ); qint64 id; if ( !parent.isValid() ) { // If we're showing the root collection then it will be the only child of the root. if ( d->m_showRootCollection ) return d->m_childEntities.value( -1 ).size(); id = d->m_rootCollection.id(); } else { if ( !node ) return 0; if ( Node::Item == node->type ) return 0; id = node->id; } if ( parent.column() <= 0 ) return d->m_childEntities.value( id ).size(); return 0; } int EntityTreeModel::getColumnCount(int headerSet) const { // Not needed in this model. Q_UNUSED(headerSet); return 1; } QVariant EntityTreeModel::getHeaderData( int section, Qt::Orientation orientation, int role, int headerSet) const { // Not needed in this model. Q_UNUSED(headerSet); if ( section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole ) return i18nc( "@title:column, name of a thing", "Name" ); return QAbstractItemModel::headerData( section, orientation, role ); } QVariant EntityTreeModel::headerData( int section, Qt::Orientation orientation, int role ) const { const int headerSet = (role / TerminalUserRole); role %= TerminalUserRole; return getHeaderData( section, orientation, role, headerSet ); } QMimeData *EntityTreeModel::mimeData( const QModelIndexList &indexes ) const { Q_D( const EntityTreeModel ); QMimeData *data = new QMimeData(); KUrl::List urls; foreach( const QModelIndex &index, indexes ) { if ( index.column() != 0 ) continue; if (!index.isValid()) continue; const Node *node = reinterpret_cast( index.internalPointer() ); if ( Node::Collection == node->type ) urls << d->m_collections.value(node->id).url(); else if ( Node::Item == node->type ) urls << d->m_items.value( node->id ).url( Item::UrlWithMimeType ); else // if that happens something went horrible wrong Q_ASSERT(false); } urls.populateMimeData( data ); return data; } // Always return false for actions which take place asyncronously, eg via a Job. bool EntityTreeModel::setData( const QModelIndex &index, const QVariant &value, int role ) { Q_D( EntityTreeModel ); const Node *node = reinterpret_cast( index.internalPointer() ); - if ( index.column() == 0 && (role & (Qt::EditRole | ItemRole | CollectionRole)) ) { + if ( index.column() == 0 && (role & ( Qt::EditRole | ItemRole | CollectionRole ) ) ) { if ( Node::Collection == node->type ) { Collection collection = d->m_collections.value( node->id ); if ( !collection.isValid() || !value.isValid() ) return false; if ( Qt::EditRole == role ) { collection.setName( value.toString() ); if ( collection.hasAttribute() ) { EntityDisplayAttribute *displayAttribute = collection.attribute(); displayAttribute->setDisplayName( value.toString() ); collection.addAttribute( displayAttribute ); } } if ( CollectionRole == role ) collection = value.value(); CollectionModifyJob *job = new CollectionModifyJob( collection, d->m_session ); connect( job, SIGNAL( result( KJob* ) ), SLOT( updateJobDone( KJob* ) ) ); return false; } else if (Node::Item == node->type) { Item item = d->m_items.value( node->id ); if ( !item.isValid() || !value.isValid() ) return false; if ( Qt::EditRole == role ) { if ( item.hasAttribute() ) { EntityDisplayAttribute *displayAttribute = item.attribute( Entity::AddIfMissing ); displayAttribute->setDisplayName( value.toString() ); item.addAttribute( displayAttribute ); } } if ( ItemRole == role ) item = value.value(); ItemModifyJob *itemModifyJob = new ItemModifyJob( item, d->m_session ); connect( itemModifyJob, SIGNAL( result( KJob* ) ), SLOT( updateJobDone( KJob* ) ) ); return false; } } return QAbstractItemModel::setData( index, value, role ); } bool EntityTreeModel::canFetchMore( const QModelIndex & parent ) const { Q_D(const EntityTreeModel); const Item item = parent.data( ItemRole ).value(); if ( item.isValid() ) { // items can't have more rows. // TODO: Should I use this for fetching more of an item, ie more payload parts? return false; } else { // but collections can... const Collection::Id colId = parent.data( CollectionIdRole ).toULongLong(); // But the root collection can't... if ( Collection::root().id() == colId ) { return false; } foreach (Node *node, d->m_childEntities.value( colId ) ) { if ( Node::Item == node->type ) { // Only try to fetch more from a collection if we don't already have items in it. // Otherwise we'd spend all the time listing items in collections. // This means that collections which don't contain items get a lot of item fetch jobs started on them. // Will fix that later. return false; } } return true; } // TODO: It might be possible to get akonadi to tell us if a collection is empty // or not and use that information instead of assuming all collections are not empty. // Using Collection statistics? } void EntityTreeModel::fetchMore( const QModelIndex & parent ) { Q_D( EntityTreeModel ); if (!canFetchMore(parent)) return; if ( d->m_itemPopulation == ImmediatePopulation ) // Nothing to do. The items are already in the model. return; else if ( d->m_itemPopulation == LazyPopulation ) { const Collection collection = parent.data( CollectionRole ).value(); if ( !collection.isValid() ) return; d->fetchItems( collection ); } } bool EntityTreeModel::hasChildren( const QModelIndex &parent ) const { Q_D( const EntityTreeModel ); // TODO: Empty collections right now will return true and get a little + to expand. // There is probably no way to tell if a collection // has child items in akonadi without first attempting an itemFetchJob... // Figure out a way to fix this. (Statistics) return ((rowCount(parent) > 0) || (canFetchMore( parent ) && d->m_itemPopulation == LazyPopulation)); } bool EntityTreeModel::match(const Item &item, const QVariant &value, Qt::MatchFlags flags) const { Q_UNUSED(item); Q_UNUSED(value); Q_UNUSED(flags); return false; } bool EntityTreeModel::match(const Collection &collection, const QVariant &value, Qt::MatchFlags flags) const { Q_UNUSED(collection); Q_UNUSED(value); Q_UNUSED(flags); return false; } QModelIndexList EntityTreeModel::match(const QModelIndex& start, int role, const QVariant& value, int hits, Qt::MatchFlags flags ) const { if (role != AmazingCompletionRole) return QAbstractItemModel::match(start, role, value, hits, flags); // Try to match names, and email addresses. QModelIndexList list; if (role < 0 || !start.isValid() || !value.isValid()) return list; const int column = 0; int row = start.row(); QModelIndex parentIdx = start.parent(); int parentRowCount = rowCount(parentIdx); while (row < parentRowCount && (hits == -1 || list.size() < hits)) { QModelIndex idx = index(row, column, parentIdx); Item item = idx.data(ItemRole).value(); if (!item.isValid()) { Collection col = idx.data(CollectionRole).value(); if (!col.isValid()) { continue; } if (match(col, value, flags)) list << idx; } else { if (match(item, value, flags)) { list << idx; } } ++row; } return list; } bool EntityTreeModel::insertRows( int, int, const QModelIndex& ) { return false; } bool EntityTreeModel::insertColumns( int, int, const QModelIndex& ) { return false; } bool EntityTreeModel::removeRows( int start, int end, const QModelIndex &parent ) { /* beginRemoveRows(start, end, parent); // TODO: Implement me. endRemoveRows(start, end, parent); */ return false; } bool EntityTreeModel::removeColumns( int, int, const QModelIndex& ) { return false; } void EntityTreeModel::setRootCollection( const Collection &collection ) { Q_D(EntityTreeModel); Q_ASSERT( collection.isValid() ); d->m_rootCollection = collection; clearAndReset(); } Collection EntityTreeModel::rootCollection() const { Q_D(const EntityTreeModel); return d->m_rootCollection; } QModelIndex EntityTreeModel::indexForCollection( const Collection &collection ) const { Q_D(const EntityTreeModel); // The id of the parent of Collection::root is not guaranteed to be -1 as assumed by startFirstListJob, // we ensure that we use -1 for the invalid Collection. const Collection::Id parentId = collection.parentCollection().isValid() ? collection.parentCollection().id() : -1; const int row = d->indexOf( d->m_childEntities.value( parentId ), collection.id() ); if ( row < 0 ) return QModelIndex(); Node *node = d->m_childEntities.value( parentId ).at( row ); return createIndex( row, 0, reinterpret_cast( node ) ); } QModelIndexList EntityTreeModel::indexesForItem( const Item &item ) const { Q_D(const EntityTreeModel); QModelIndexList indexes; const Collection::List collections = d->getParentCollections( item ); const qint64 id = item.id(); foreach ( const Collection &collection, collections ) { const int row = d->indexOf( d->m_childEntities.value( collection.id() ), id ); Node *node = d->m_childEntities.value( collection.id() ).at( row ); indexes << createIndex( row, 0, reinterpret_cast( node ) ); } return indexes; } void EntityTreeModel::setItemPopulationStrategy( ItemPopulationStrategy strategy ) { Q_D(EntityTreeModel); d->m_itemPopulation = strategy; clearAndReset(); } EntityTreeModel::ItemPopulationStrategy EntityTreeModel::itemPopulationStrategy() const { Q_D(const EntityTreeModel); return d->m_itemPopulation; } void EntityTreeModel::setIncludeRootCollection( bool include ) { Q_D(EntityTreeModel); d->m_showRootCollection = include; clearAndReset(); } bool EntityTreeModel::includeRootCollection() const { Q_D(const EntityTreeModel); return d->m_showRootCollection; } void EntityTreeModel::setRootCollectionDisplayName( const QString &displayName ) { Q_D(EntityTreeModel); d->m_rootCollectionDisplayName = displayName; // TODO: Emit datachanged if it is being shown. } QString EntityTreeModel::rootCollectionDisplayName() const { Q_D( const EntityTreeModel); return d->m_rootCollectionDisplayName; } void EntityTreeModel::setCollectionFetchStrategy( CollectionFetchStrategy strategy ) { Q_D( EntityTreeModel); d->m_collectionFetchStrategy = strategy; clearAndReset(); } EntityTreeModel::CollectionFetchStrategy EntityTreeModel::collectionFetchStrategy() const { Q_D( const EntityTreeModel); return d->m_collectionFetchStrategy; } #include "entitytreemodel.moc" diff --git a/akonadi/entitytreemodel.h b/akonadi/entitytreemodel.h index ff65616a8..deeb361a6 100644 --- a/akonadi/entitytreemodel.h +++ b/akonadi/entitytreemodel.h @@ -1,348 +1,353 @@ /* Copyright (c) 2008 Stephen Kelly 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_ENTITYTREEMODEL_H #define AKONADI_ENTITYTREEMODEL_H #include "akonadi_export.h" #include #include #include #include +Q_DECLARE_METATYPE( QSet ) + // TODO (Applies to all these 'new' models, not just EntityTreeModel): // * Figure out how LazyPopulation and signals from monitor containing items should // fit together. Possibly store a list of collections whose items have already // been lazily fetched. // * Fgure out whether DescendantEntitiesProxyModel needs to use fetchMore. // * Profile this and DescendantEntitiesProxyModel. Make sure it's faster than // FlatCollectionProxyModel. See if the cache in that class can be cleared less often. // * Unit tests. Much of the stuff here is not covered by modeltest, and some of // it is akonadi specific, such as setting root collection etc. // * Implement support for includeUnsubscribed. // * Use CollectionStatistics for item count stuff. Find out if I can get stats by mimetype. // * Make sure there are applications using it before committing to it until KDE5. // Some API/ virtual methods might need to be added when real applications are made. // * Implement ordering support. // * Implement some proxy models for time-table like uses, eg KOrganizer events. // * Apidox++ namespace Akonadi { class CollectionStatistics; class Item; class ItemFetchScope; class Monitor; class Session; class EntityTreeModelPrivate; /** * @short A model for collections and items together. * * This class is a wrapper around a Akonadi::Monitor object. The model represents a * part of the collection and item tree configured in the Monitor. * * @code * * Monitor *monitor = new Monitor(this); * monitor->setCollectionMonitored(Collection::root()); * monitor->setMimeTypeMonitored(KABC::addresseeMimeType()); * * EntityTreeModel *model = new EntityTreeModel( session, monitor, this ); * * EntityTreeView *view = new EntityTreeView( this ); * view->setModel( model ); * * @endcode * * @author Stephen Kelly * @since 4.4 */ class AKONADI_EXPORT EntityTreeModel : public QAbstractItemModel { Q_OBJECT public: /** * Describes the roles for items. Roles for collections are defined by the superclass. */ enum Roles { //sebsauer, 2009-05-07; to be able here to keep the akonadi_next EntityTreeModel compatible with //the akonadi_old ItemModel and CollectionModel, we need to use the same int-values for //ItemRole, ItemIdRole and MimeTypeRole like the Akonadi::ItemModel is using and the same //CollectionIdRole and CollectionRole like the Akonadi::CollectionModel is using. ItemIdRole = Qt::UserRole + 1, ///< The item id ItemRole = Qt::UserRole + 2, ///< The Item MimeTypeRole = Qt::UserRole + 3, ///< The mimetype of the entity CollectionIdRole = Qt::UserRole + 10, ///< The collection id. CollectionRole = Qt::UserRole + 11, ///< The collection. RemoteIdRole, ///< The remoteId of the entity CollectionChildOrderRole, ///< Ordered list of child items if available AmazingCompletionRole, ///< Role used to implement amazing completion ParentCollectionRole, ///< The parent collection of the entity ColumnCountRole, ///< @internal Used by proxies to determine the number of columns for a header group. + LoadedPartsRole, ///< Parts available in the model for the item + AvailablePartsRole, ///< Parts available in the Akonadi server for the item + SessionRole, ///< The Session used by this model. @internal. UserRole = Qt::UserRole + 1000, ///< Role for user extensions. TerminalUserRole = 10000 ///< Last role for user extensions. Don't use a role beyond this or headerData will break. }; /** * Describes what header information the model shall return. */ enum HeaderGroup { EntityTreeHeaders, ///< Header information for a tree with collections and items CollectionTreeHeaders, ///< Header information for a collection-only tree ItemListHeaders, ///< Header information for a list of items UserHeaders = 1000 ///< Last header information for submodel extensions }; /** * Creates a new entity tree model. * * @param session The Session to use to communicate with Akonadi. * @param monitor The Monitor whose entities should be represented in the model. * @param parent The parent object. */ EntityTreeModel( Session *session, Monitor *monitor, QObject *parent = 0 ); /** * Destroys the entity tree model. */ virtual ~EntityTreeModel(); /** * Describes how the model should populated its items. */ enum ItemPopulationStrategy { NoItemPopulation, ///< Do not include items in the model. ImmediatePopulation, ///< Retrieve items immediately when their parent is in the model. This is the default. LazyPopulation ///< Fetch items only when requested (using canFetchMore/fetchMore) }; /** * Sets the item population @p strategy of the model. */ void setItemPopulationStrategy( ItemPopulationStrategy strategy ); /** * Returns the item population strategy of the model. */ ItemPopulationStrategy itemPopulationStrategy() const; /** * Sets the root collection to create an entity tree for. * The @p collection must be a valid Collection object. * * By default the Collection::root() is used. */ void setRootCollection( const Collection &collection ); /** * Returns the root collection of the entity tree. */ Collection rootCollection() const; /** * Sets whether the root collection shall be provided by the model. * * @see setRootCollectionDisplayName() */ void setIncludeRootCollection( bool include ); /** * Returns whether the root collection is provided by the model. */ bool includeRootCollection() const; /** * Sets the display @p name of the root collection of the model. * The default display name is "[*]". * * @note The display name for the root collection is only used if * the root collection has been included with setIncludeRootCollection(). */ void setRootCollectionDisplayName( const QString &name ); /** * Returns the display name of the root collection. */ QString rootCollectionDisplayName() const; /** * Describes what collections shall be fetched by and represent in the model. */ enum CollectionFetchStrategy { FetchNoCollections, ///< Fetches nothing. This creates an empty model. FetchFirstLevelChildCollections, ///< Fetches first level collections in the root collection. FetchCollectionsRecursive ///< Fetches collections in the root collection recursively. This is the default. }; /** * Sets the collection fetch @p strategy of the model. */ void setCollectionFetchStrategy( CollectionFetchStrategy strategy ); /** * Returns the collection fetch strategy of the model. */ CollectionFetchStrategy collectionFetchStrategy() const; /** * Returns the model index for the given @p collection. */ QModelIndex indexForCollection( const Collection &collection ) const; /** * Returns the model indexes for the given @p item. */ QModelIndexList indexesForItem( const Item &item ) const; /** * Returns the collection for the given collection @p id. */ Collection collectionForId( Collection::Id id ) const; /** * Returns the item for the given item @p id. */ Item itemForId( Item::Id id ) const; // TODO: Remove these and use the Monitor instead. Need to add api to Monitor for this. void setIncludeUnsubscribed( bool include ); bool includeUnsubscribed() const; virtual int columnCount( const QModelIndex & parent = QModelIndex() ) const; virtual int rowCount( const QModelIndex & parent = QModelIndex() ) const; virtual QVariant data( const QModelIndex & index, int role = Qt::DisplayRole ) const; virtual QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const; virtual Qt::ItemFlags flags( const QModelIndex &index ) const; virtual QStringList mimeTypes() const; virtual Qt::DropActions supportedDropActions() const; virtual QMimeData *mimeData( const QModelIndexList &indexes ) const; virtual bool dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent ); virtual bool setData( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole ); virtual QModelIndex index( int row, int column, const QModelIndex & parent = QModelIndex() ) const; virtual QModelIndex parent( const QModelIndex & index ) const; // TODO: Review the implementations of these. I think they could be better. virtual bool canFetchMore( const QModelIndex & parent ) const; virtual void fetchMore( const QModelIndex & parent ); virtual bool hasChildren( const QModelIndex &parent = QModelIndex() ) const; /** * Reimplemented to handle the AmazingCompletionRole. */ virtual QModelIndexList match( const QModelIndex& start, int role, const QVariant& value, int hits = 1, Qt::MatchFlags flags = Qt::MatchFlags( Qt::MatchStartsWith | Qt::MatchWrap ) ) const; /** * Reimplement this in a subclass to return true if @p item matches @p value with @p flags in the AmazingCompletionRole. */ virtual bool match( const Item &item, const QVariant &value, Qt::MatchFlags flags ) const; /** * Reimplement this in a subclass to return true if @p collection matches @p value with @p flags in the AmazingCompletionRole. */ virtual bool match( const Collection &collection, const QVariant &value, Qt::MatchFlags flags ) const; protected: /** * Clears and resets the model. Always call this instead of the reset method in the superclass. * Using the reset method will not reliably clear or refill the model. */ void clearAndReset(); /** * Provided for convenience of subclasses. */ virtual QVariant getData( const Item &item, int column, int role = Qt::DisplayRole ) const; /** * Provided for convenience of subclasses. */ virtual QVariant getData( const Collection &collection, int column, int role = Qt::DisplayRole ) const; /** * Reimplement this to provide different header data. This is needed when using one model * with multiple proxies and views, and each should show different header data. */ virtual QVariant getHeaderData( int section, Qt::Orientation orientation, int role, int headerSet ) const; virtual int getColumnCount(int headerSet) const; /** * Removes the rows from @p start to @p end from @parent */ virtual bool removeRows( int start, int end, const QModelIndex &parent = QModelIndex() ); private: //@cond PRIVATE Q_DECLARE_PRIVATE( EntityTreeModel ) EntityTreeModelPrivate * const d_ptr; // Make these private, they shouldn't be called by applications virtual bool insertRows( int , int, const QModelIndex& = QModelIndex() ); virtual bool insertColumns( int, int, const QModelIndex& = QModelIndex() ); virtual bool removeColumns( int, int, const QModelIndex& = QModelIndex() ); Q_PRIVATE_SLOT( d_func(), void monitoredCollectionStatisticsChanged( Akonadi::Collection::Id, const Akonadi::CollectionStatistics& ) ) Q_PRIVATE_SLOT( d_func(), void startFirstListJob() ) // Q_PRIVATE_SLOT( d_func(), void slotModelReset() ) // TODO: Can I merge these into one jobResult slot? Q_PRIVATE_SLOT( d_func(), void fetchJobDone( KJob *job ) ) Q_PRIVATE_SLOT( d_func(), void copyJobDone( KJob *job ) ) Q_PRIVATE_SLOT( d_func(), void moveJobDone( KJob *job ) ) Q_PRIVATE_SLOT( d_func(), void updateJobDone( KJob *job ) ) Q_PRIVATE_SLOT( d_func(), void itemsFetched( Akonadi::Item::List ) ) Q_PRIVATE_SLOT( d_func(), void collectionsFetched( Akonadi::Collection::List ) ) Q_PRIVATE_SLOT( d_func(), void ancestorsFetched( Akonadi::Collection::List ) ) Q_PRIVATE_SLOT( d_func(), void monitoredMimeTypeChanged( const QString&, bool ) ) Q_PRIVATE_SLOT( d_func(), void monitoredCollectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ) ) Q_PRIVATE_SLOT( d_func(), void monitoredCollectionRemoved( const Akonadi::Collection& ) ) Q_PRIVATE_SLOT( d_func(), void monitoredCollectionChanged( const Akonadi::Collection& ) ) Q_PRIVATE_SLOT( d_func(), void monitoredCollectionMoved( const Akonadi::Collection&, const Akonadi::Collection&, const Akonadi::Collection&) ) Q_PRIVATE_SLOT( d_func(), void monitoredItemAdded( const Akonadi::Item&, const Akonadi::Collection& ) ) Q_PRIVATE_SLOT( d_func(), void monitoredItemRemoved( const Akonadi::Item& ) ) Q_PRIVATE_SLOT( d_func(), void monitoredItemChanged( const Akonadi::Item&, const QSet& ) ) Q_PRIVATE_SLOT( d_func(), void monitoredItemMoved( const Akonadi::Item&, const Akonadi::Collection&, const Akonadi::Collection& ) ) Q_PRIVATE_SLOT( d_func(), void monitoredItemLinked( const Akonadi::Item&, const Akonadi::Collection& ) ) Q_PRIVATE_SLOT( d_func(), void monitoredItemUnlinked( const Akonadi::Item&, const Akonadi::Collection& ) ) //@endcond }; } // namespace #endif diff --git a/akonadi/entitytreemodel_p.cpp b/akonadi/entitytreemodel_p.cpp index 42b1029b7..bd7022756 100644 --- a/akonadi/entitytreemodel_p.cpp +++ b/akonadi/entitytreemodel_p.cpp @@ -1,788 +1,842 @@ /* Copyright (c) 2008 Stephen Kelly 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 "entitytreemodel_p.h" #include "entitytreemodel.h" #include #include #include #include #include #include #include #include #include #include +#include #include #include #include using namespace Akonadi; EntityTreeModelPrivate::EntityTreeModelPrivate( EntityTreeModel *parent ) : q_ptr( parent ), m_collectionFetchStrategy( EntityTreeModel::FetchCollectionsRecursive ), m_itemPopulation( EntityTreeModel::ImmediatePopulation ), m_includeUnsubscribed( true ), m_includeStatistics( false ), m_showRootCollection( false ) { } int EntityTreeModelPrivate::indexOf( const QList &nodes, Entity::Id id ) const { int i = 0; foreach ( const Node *node, nodes ) { if ( node->id == id ) return i; i++; } return -1; } ItemFetchJob* EntityTreeModelPrivate::getItemFetchJob( const Collection &parent, ItemFetchScope scope ) const { ItemFetchJob *itemJob = new Akonadi::ItemFetchJob( parent, m_session ); itemJob->setFetchScope( scope ); return itemJob; } ItemFetchJob* EntityTreeModelPrivate::getItemFetchJob( const Item &item, ItemFetchScope scope ) const { ItemFetchJob *itemJob = new Akonadi::ItemFetchJob( item, m_session ); itemJob->setFetchScope( scope ); return itemJob; } void EntityTreeModelPrivate::runItemFetchJob(ItemFetchJob *itemFetchJob, const Collection &parent) const { Q_Q( const EntityTreeModel ); // TODO: This hack is probably not needed anymore. Remove it. // ### HACK: itemsReceivedFromJob needs to know which collection items were added to. // That is not provided by akonadi, so we attach it in a property. itemFetchJob->setProperty( ItemFetchCollectionId(), QVariant( parent.id() ) ); q->connect( itemFetchJob, SIGNAL( itemsReceived( const Akonadi::Item::List& ) ), q, SLOT( itemsFetched( const Akonadi::Item::List& ) ) ); q->connect( itemFetchJob, SIGNAL( result( KJob* ) ), q, SLOT( fetchJobDone( KJob* ) ) ); } void EntityTreeModelPrivate::fetchItems( const Collection &parent ) { Q_Q( EntityTreeModel ); // TODO: Use a more specific fetch scope to get only the envelope for mails etc. ItemFetchJob *itemJob = getItemFetchJob(parent, m_monitor->itemFetchScope() ); runItemFetchJob(itemJob, parent); } void EntityTreeModelPrivate::fetchCollections( const Collection &collection, CollectionFetchJob::Type type ) { Q_Q( EntityTreeModel ); CollectionFetchJob *job = new CollectionFetchJob( collection, type, m_session ); job->fetchScope().setIncludeUnsubscribed( m_includeUnsubscribed ); job->fetchScope().setIncludeStatistics( m_includeStatistics ); job->fetchScope().setContentMimeTypes( m_monitor->mimeTypesMonitored() ); q->connect( job, SIGNAL( collectionsReceived( const Akonadi::Collection::List& ) ), q, SLOT( collectionsFetched( const Akonadi::Collection::List& ) ) ); q->connect( job, SIGNAL( result( KJob* ) ), q, SLOT( fetchJobDone( KJob* ) ) ); } void EntityTreeModelPrivate::collectionsFetched( const Akonadi::Collection::List& collections ) { // TODO: refactor this stuff into separate methods for listing resources in Collection::root, and listing collections within resources. Q_Q( EntityTreeModel ); Akonadi::AgentManager *agentManager = Akonadi::AgentManager::self(); Collection::List _collections = collections; forever { int collectionsSize = _collections.size(); QMutableListIterator it(_collections); while (it.hasNext()) { const Collection col = it.next(); const Collection::Id parentId = col.parentCollection().id(); const Collection::Id colId = col.id(); if ( m_collections.contains( parentId ) ) { insertCollection( col, m_collections.value( parentId ) ); if ( m_itemPopulation == EntityTreeModel::ImmediatePopulation ) fetchItems( col ); if ( m_pendingChildCollections.contains( colId ) ) { QList pendingParentIds = m_pendingChildCollections.value( colId ); foreach(const Collection::Id &id, pendingParentIds) { Collection pendingCollection = m_pendingCollections.value(id); Q_ASSERT( pendingCollection.isValid() ); insertPendingCollection( pendingCollection, col, it ); m_pendingCollections.remove(id); } if ( !it.findNext(col) && !it.findPrevious(col) ) { Q_ASSERT("Something went very wrong" == "false"); } m_pendingChildCollections.remove( colId ); } it.remove(); } else { m_pendingCollections.insert( colId, col ); if ( !m_pendingChildCollections.value( parentId ).contains( colId ) ) m_pendingChildCollections[ parentId ].append( colId ); } } if ( _collections.isEmpty() ) break; // forever if( _collections.size() == collectionsSize ) { // Didn't process any collections this iteration. // Persist them until the next time collectionsFetched receives collections. kWarning() << "Some collections could not be inserted into the model yet."; break; // forever } } } void EntityTreeModelPrivate::itemsFetched( const Akonadi::Item::List& items ) { Q_Q( EntityTreeModel ); QObject *job = q->sender(); Q_ASSERT( job ); const Collection::Id collectionId = job->property( ItemFetchCollectionId() ).value(); Item::List itemsToInsert; Item::List itemsToUpdate; const Collection collection = m_collections.value( collectionId ); Q_ASSERT( collection.isValid() ); const QList collectionEntities = m_childEntities.value( collectionId ); foreach ( const Item &item, items ) { if ( indexOf( collectionEntities, item.id() ) != -1 ) { itemsToUpdate << item; } else { if ( m_mimeChecker.wantedMimeTypes().isEmpty() || m_mimeChecker.isWantedItem( item ) ) { itemsToInsert << item; } } } if ( itemsToInsert.size() > 0 ) { const int startRow = m_childEntities.value( collectionId ).size(); const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collectionId ) ); q->beginInsertRows( parentIndex, startRow, startRow + items.size() - 1 ); foreach ( const Item &item, items ) { Item::Id itemId = item.id(); m_items.insert( itemId, item ); Node *node = new Node; node->id = itemId; node->parent = collectionId; node->type = Node::Item; m_childEntities[ collectionId ].append( node ); } q->endInsertRows(); } + if ( itemsToUpdate.size() > 0 ) + { + foreach (const Item &item, itemsToUpdate) + { + m_items[ item.id() ].merge( item ); + foreach ( const QModelIndex &idx, q->indexesForItem( item ) ) + { + q->dataChanged( idx, idx ); + } + } + } + } void EntityTreeModelPrivate::monitoredMimeTypeChanged( const QString & mimeType, bool monitored ) { if ( monitored ) m_mimeChecker.addWantedMimeType( mimeType ); else m_mimeChecker.removeWantedMimeType( mimeType ); } -void EntityTreeModelPrivate::retrieveAncestors(const Akonadi::Collection& collection) +void EntityTreeModelPrivate::retrieveAncestors( const Akonadi::Collection& collection ) { Q_Q( EntityTreeModel ); - // Unlike fetchCollections, this method fetches collections by traversing up, not down. - CollectionFetchJob *job = new CollectionFetchJob( collection.parentCollection(), CollectionFetchJob::Base, m_session ); - job->fetchScope().setIncludeUnsubscribed( m_includeUnsubscribed ); - job->fetchScope().setIncludeStatistics( m_includeStatistics ); - q->connect( job, SIGNAL( collectionsReceived( const Akonadi::Collection::List& ) ), - q, SLOT( ancestorsFetched( const Akonadi::Collection::List& ) ) ); - q->connect( job, SIGNAL( result( KJob* ) ), - q, SLOT( fetchJobDone( KJob* ) ) ); -} -void EntityTreeModelPrivate::ancestorsFetched(const Akonadi::Collection::List& collectionList) -{ - // List is a size of one. - foreach(const Collection &collection, collectionList) - { - // We should find a collection already in the tree before we reach the collection root. - // We're looking to bridge a gap here. - Q_ASSERT(collection != Collection::root()); + Collection parentCollection = collection.parentCollection(); - // We already checked this either on the previous recursion or in monitoredCollectionAdded. - Q_ASSERT(!m_collections.contains(collection.id())); + Q_ASSERT( parentCollection != Collection::root() ); - m_ancestors.prepend(collection); - if (m_collections.contains(collection.parentCollection().id())) - { - m_ancestors.prepend( m_collections.value(collection.parentCollection().id()) ); - insertAncestors(m_ancestors); - } else { - retrieveAncestors(collection); - } + Collection temp; + + Collection::List ancestors; + + while ( !m_collections.contains( parentCollection.id() ) ) + { + // Put a temporary node in the tree later. + ancestors.prepend( parentCollection ); + + // Fetch the real ancestor + CollectionFetchJob *job = new CollectionFetchJob( parentCollection, CollectionFetchJob::Base, m_session ); + job->fetchScope().setIncludeUnsubscribed( m_includeUnsubscribed ); + job->fetchScope().setIncludeStatistics( m_includeStatistics ); + q->connect( job, SIGNAL( collectionsReceived( const Akonadi::Collection::List& ) ), + q, SLOT( ancestorsFetched( const Akonadi::Collection::List& ) ) ); + q->connect( job, SIGNAL( result( KJob* ) ), + q, SLOT( fetchJobDone( KJob* ) ) ); + + temp = parentCollection.parentCollection(); + parentCollection = temp; } -} -void EntityTreeModelPrivate::insertAncestors(const Akonadi::Collection::List& collectionList) -{ + QModelIndex parent = q->indexForCollection( parentCollection ); + + // Still prepending all collections for now. + int row = 0; + + // Although we insert several Collections here, we only need to notify though the model + // about the top-level one. The rest will be found auotmatically by the view. + q->beginInsertRows(parent, row, row); + Collection::List::const_iterator it; - const Collection::List::const_iterator begin = collectionList.constBegin() + 1; - const Collection::List::const_iterator end = collectionList.constEnd(); - for (it = begin; it != end; ++it) + const Collection::List::const_iterator begin = ancestors.constBegin(); + const Collection::List::const_iterator end = ancestors.constEnd(); + + for ( it = begin; it != end; ++it ) { - insertCollection(*it, *(it-1)); + Collection col = *it; + m_collections.insert( col.id(), col ); + + Node *node = new Node; + node->id = col.id(); + node->parent = col.parentCollection().id(); + node->type = Node::Collection; + m_childEntities[ node->parent ].prepend( node ); } - m_ancestors.clear(); + + q->endInsertRows(); +} + +void EntityTreeModelPrivate::ancestorsFetched( const Akonadi::Collection::List& collectionList ) +{ + Q_Q( EntityTreeModel ); + Q_ASSERT( collectionList.size() == 1 ); + + const Collection collection = collectionList.at( 0 ); + + m_collections[ collection.id() ] = collection; + + const QModelIndex index = q->indexForCollection( collection ); + Q_ASSERT( index.isValid() ); + q->dataChanged( index, index ); } void EntityTreeModelPrivate::insertCollection( const Akonadi::Collection& collection, const Akonadi::Collection& parent ) { - Q_ASSERT(collection.isValid()); - Q_ASSERT(parent.isValid()); + Q_ASSERT( collection.isValid() ); + Q_ASSERT( parent.isValid() ); Q_Q( EntityTreeModel ); // TODO: Use order attribute of parent if available // Otherwise prepend collections and append items. Currently this prepends all collections. // Or I can prepend and append for single signals, then 'change' the parent. // QList childCols = m_childEntities.value( parent.id() ); // int row = childCols.size(); // int numChildCols = childCollections.value(parent.id()).size(); const int row = 0; const QModelIndex parentIndex = q->indexForCollection( parent ); q->beginInsertRows( parentIndex, row, row ); m_collections.insert( collection.id(), collection ); Node *node = new Node; node->id = collection.id(); node->parent = parent.id(); node->type = Node::Collection; m_childEntities[ parent.id() ].prepend( node ); q->endInsertRows(); } void EntityTreeModelPrivate::insertPendingCollection( const Akonadi::Collection& collection, const Akonadi::Collection& parent, QMutableListIterator &colIt ) { insertCollection(collection, parent); m_pendingCollections.remove( collection.id() ); if ( m_itemPopulation == EntityTreeModel::ImmediatePopulation ) fetchItems( collection ); - if (colIt.findPrevious(collection) || colIt.findNext(collection)) + if ( colIt.findPrevious( collection ) || colIt.findNext( collection ) ) { colIt.remove(); } - Q_ASSERT(m_collections.contains(parent.id())); + Q_ASSERT( m_collections.contains( parent.id() ) ); - QList pendingChildCollectionsToInsert = m_pendingChildCollections.value(collection.id()); + QList pendingChildCollectionsToInsert = m_pendingChildCollections.value( collection.id() ); QList::const_iterator it; const QList::const_iterator begin = pendingChildCollectionsToInsert.constBegin(); const QList::const_iterator end = pendingChildCollectionsToInsert.constEnd(); for ( it = begin; it != end; ++it ) { - insertPendingCollection(m_pendingCollections.value( *it ), collection, colIt); + insertPendingCollection( m_pendingCollections.value( *it ), collection, colIt ); } m_pendingChildCollections.remove( parent.id() ); } void EntityTreeModelPrivate::monitoredCollectionAdded( const Akonadi::Collection& collection, const Akonadi::Collection& parent ) { // If the resource is removed while populating the model with it, we might still // get some monitor signals. These stale/out-of-order signals can't be completely eliminated // in the akonadi server due to implementation details, so we also handle such signals in the model silently // in all the monitored slots. // Stephen Kelly, 28, July 2009 // This is currently temporarily blocked by a uninitialized value bug in the server. // if ( !m_collections.contains( parent.id() ) ) // { // kWarning() << "Got a stale notification for a collection whose parent was already removed." << collection.id() << collection.remoteId(); // return; // } // Some collection trees contain multiple mimetypes. Even though server side filtering ensures we // only get the ones we're interested in from the job, we have to filter on collections received through signals too. if ( !m_mimeChecker.wantedMimeTypes().isEmpty() && !m_mimeChecker.isWantedCollection( collection ) ) return; - if (!m_collections.contains(parent.id())) + if ( !m_collections.contains(parent.id() ) ) { // The collection we're interested in is contained in a collection we're not interested in. // We download the ancestors of the collection we're interested in to complete the tree. - m_ancestors.prepend(collection); - retrieveAncestors(collection); + retrieveAncestors( collection ); return; } - insertCollection(collection, parent); + insertCollection( collection, parent ); } void EntityTreeModelPrivate::monitoredCollectionRemoved( const Akonadi::Collection& collection ) { if ( !m_collections.contains( collection.parent() ) ) return; Q_Q( EntityTreeModel ); // This may be a signal for a collection we've already removed by removing its ancestor. if ( !m_collections.contains( collection.id() ) ) { kWarning() << "Got a stale notification for a collection which was already removed." << collection.id() << collection.remoteId(); return; } const int row = indexOf( m_childEntities.value( collection.parentCollection().id() ), collection.id() ); const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.parentCollection().id() ) ); q->beginRemoveRows( parentIndex, row, row ); // Delete all descendant collections and items. removeChildEntities(collection.id()); // Remove deleted collection from its parent. m_childEntities[ collection.parentCollection().id() ].removeAt( row ); q->endRemoveRows(); } void EntityTreeModelPrivate::removeChildEntities(Collection::Id colId) { QList::const_iterator it; QList childList = m_childEntities.value(colId); const QList::const_iterator begin = childList.constBegin(); const QList::const_iterator end = childList.constEnd(); for (it = begin; it != end; ++it) { if (Node::Item == (*it)->type) { m_items.remove((*it)->id); } else { removeChildEntities((*it)->id); m_collections.remove((*it)->id); } } m_childEntities.remove(colId); } void EntityTreeModelPrivate::monitoredCollectionMoved( const Akonadi::Collection& collection, const Akonadi::Collection& sourceCollection, const Akonadi::Collection& destCollection ) { if ( !m_collections.contains( collection.id() ) ) { kWarning() << "Got a stale notification for a collection which was already removed." << collection.id() << collection.remoteId(); return; } Q_Q( EntityTreeModel ); const int srcRow = indexOf( m_childEntities.value( sourceCollection.id() ), collection.id() ); const QModelIndex srcParentIndex = q->indexForCollection( sourceCollection ); const QModelIndex destParentIndex = q->indexForCollection( destCollection ); const int destRow = 0; // Prepend collections // TODO: Uncomment for Qt4.6 // q->beginMoveRows( srcParentIndex, srcRow, srcRow, destParentIndex, destRow ); // Node *node = m_childEntities[ sourceCollection.id() ].takeAt( srcRow ); // m_childEntities[ destCollection.id() ].prepend( node ); // q->endMoveRows(); } void EntityTreeModelPrivate::monitoredCollectionChanged( const Akonadi::Collection &collection ) { Q_Q( EntityTreeModel ); if ( !m_collections.contains( collection.id() ) ) { kWarning() << "Got a stale notification for a collection which was already removed." << collection.id() << collection.remoteId(); return; } m_collections[ collection.id() ] = collection; const QModelIndex index = q->indexForCollection( collection ); Q_ASSERT( index.isValid() ); q->dataChanged( index, index ); } void EntityTreeModelPrivate::monitoredCollectionStatisticsChanged( Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics ) { Q_Q( EntityTreeModel ); if ( !m_collections.contains( id ) ) { kWarning() << "Got statistics response for non-existing collection:" << id; } else { m_collections[ id ].setStatistics( statistics ); const QModelIndex index = q->indexForCollection( m_collections[ id ] ); q->dataChanged( index, index ); } } void EntityTreeModelPrivate::monitoredItemAdded( const Akonadi::Item& item, const Akonadi::Collection& collection ) { Q_Q( EntityTreeModel ); if ( !m_collections.contains( collection.id() ) ) { kWarning() << "Got a stale notification for an item whose collection was already removed." << item.id() << item.remoteId(); return; } Q_ASSERT( m_collections.contains( collection.id() ) ); if ( !m_mimeChecker.wantedMimeTypes().isEmpty() && !m_mimeChecker.isWantedItem( item ) ) return; const int row = m_childEntities.value( collection.id() ).size(); const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.id() ) ); q->beginInsertRows( parentIndex, row, row ); m_items.insert( item.id(), item ); Node *node = new Node; node->id = item.id(); node->parent = collection.id(); node->type = Node::Item; m_childEntities[ collection.id() ].append( node ); q->endInsertRows(); } void EntityTreeModelPrivate::monitoredItemRemoved( const Akonadi::Item &item ) { Q_Q( EntityTreeModel ); const Collection::List parents = getParentCollections( item ); if ( parents.isEmpty() ) return; if ( !m_items.contains( item.id() ) ) { kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); return; } // TODO: Iterate over all (virtual) collections. const Collection collection = parents.first(); Q_ASSERT( m_collections.contains( collection.id() ) ); const int row = indexOf( m_childEntities.value( collection.id() ), item.id() ); const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.id() ) ); q->beginRemoveRows( parentIndex, row, row ); m_items.remove( item.id() ); m_childEntities[ collection.id() ].removeAt( row ); q->endRemoveRows(); } void EntityTreeModelPrivate::monitoredItemChanged( const Akonadi::Item &item, const QSet& ) { Q_Q( EntityTreeModel ); if ( !m_items.contains( item.id() ) ) { kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); return; } - - m_items[ item.id() ] = item; + m_items[ item.id() ].merge(item); const QModelIndexList indexes = q->indexesForItem( item ); foreach ( const QModelIndex &index, indexes ) { if( !index.isValid() ) { kWarning() << "item has invalid index:" << item.id() << item.remoteId(); } else { q->dataChanged( index, index ); } } } void EntityTreeModelPrivate::monitoredItemMoved( const Akonadi::Item& item, const Akonadi::Collection& sourceCollection, const Akonadi::Collection& destCollection ) { Q_Q( EntityTreeModel ); if ( !m_items.contains( item.id() ) ) { kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); return; } Q_ASSERT( m_collections.contains( sourceCollection.id() ) ); Q_ASSERT( m_collections.contains( destCollection.id() ) ); const Item::Id itemId = item.id(); const int srcRow = indexOf( m_childEntities.value( sourceCollection.id() ), itemId ); const QModelIndex srcIndex = q->indexForCollection( sourceCollection ); const QModelIndex destIndex = q->indexForCollection( destCollection ); // Where should it go? Always append items and prepend collections and reorganize them with separate reactions to Attributes? const int destRow = q->rowCount( destIndex ); // TODO: Uncomment for Qt4.6 // q->beginMoveRows( srcIndex, srcRow, srcRow, destIndex, destRow ); // Node *node = m_childEntities[ sourceItem.id() ].takeAt( srcRow ); // m_childEntities[ destItem.id() ].append( node ); // q->endMoveRows(); } void EntityTreeModelPrivate::monitoredItemLinked( const Akonadi::Item& item, const Akonadi::Collection& collection ) { Q_Q( EntityTreeModel ); if ( !m_items.contains( item.id() ) ) { kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); return; } Q_ASSERT( m_collections.contains( collection.id() ) ); if ( !m_mimeChecker.wantedMimeTypes().isEmpty() && !m_mimeChecker.isWantedItem( item ) ) return; const int row = m_childEntities.value( collection.id() ).size(); const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.id() ) ); q->beginInsertRows( parentIndex, row, row ); Node *node = new Node; node->id = item.id(); node->parent = collection.id(); node->type = Node::Item; m_childEntities[ collection.id()].append( node ); q->endInsertRows(); } void EntityTreeModelPrivate::monitoredItemUnlinked( const Akonadi::Item& item, const Akonadi::Collection& collection ) { Q_Q( EntityTreeModel ); if ( !m_items.contains( item.id() ) ) { kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); return; } Q_ASSERT( m_collections.contains( collection.id() ) ); const int row = indexOf( m_childEntities.value( collection.id() ), item.id() ); const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.id() ) ); q->beginInsertRows( parentIndex, row, row ); m_childEntities[ collection.id() ].removeAt( row ); q->endInsertRows(); } void EntityTreeModelPrivate::fetchJobDone( KJob *job ) { Q_ASSERT(m_pendingCollections.isEmpty()); Q_ASSERT(m_pendingChildCollections.isEmpty()); if ( job->error() ) { kWarning() << "Job error: " << job->errorString() << endl; } } void EntityTreeModelPrivate::copyJobDone( KJob *job ) { if ( job->error() ) { kWarning() << "Job error: " << job->errorString() << endl; } } void EntityTreeModelPrivate::moveJobDone( KJob *job ) { if ( job->error() ) { kWarning() << "Job error: " << job->errorString() << endl; } } void EntityTreeModelPrivate::updateJobDone( KJob *job ) { + Q_Q(EntityTreeModel); + if ( job->error() ) { // TODO: handle job errors kWarning() << "Job error:" << job->errorString(); } else { + + ItemModifyJob *modifyJob = qobject_cast(job); + if (!modifyJob) + return; + + Item item = modifyJob->item(); + + Q_ASSERT( item.isValid() ); + + m_items[ item.id() ].merge( item ); + QModelIndexList list = q->indexesForItem( item ); + + foreach (const QModelIndex &idx, list) + { + q->dataChanged( idx, idx ); + } + // TODO: Is this trying to do the job of collectionstatisticschanged? // CollectionStatisticsJob *csjob = static_cast( job ); // Collection result = csjob->collection(); // collectionStatisticsChanged( result.id(), csjob->statistics() ); } } void EntityTreeModelPrivate::startFirstListJob() { Q_Q(EntityTreeModel); if (m_collections.size() > 0) return; Collection rootCollection; // Even if the root collection is the invalid collection, we still need to start // the first list job with Collection::root. if ( m_showRootCollection ) { rootCollection = Collection::root(); // Notify the outside that we're putting collection::root into the model. q->beginInsertRows( QModelIndex(), 0, 0 ); m_collections.insert( rootCollection.id(), rootCollection ); m_rootNode = new Node; m_rootNode->id = rootCollection.id(); m_rootNode->parent = -1; m_rootNode->type = Node::Collection; m_childEntities[ -1 ].append( m_rootNode ); q->endInsertRows(); } else { // Otherwise store it silently because it's not part of the usable model. rootCollection = m_rootCollection; m_rootNode = new Node; m_rootNode->id = rootCollection.id(); m_rootNode->parent = -1; m_rootNode->type = Node::Collection; m_collections.insert( rootCollection.id(), rootCollection ); } // Includes recursive trees. Lower levels are fetched in the onRowsInserted slot if // necessary. // HACK: fix this for recursive listing if we filter on mimetypes that only exit deeper // in the hierarchy if ( ( m_collectionFetchStrategy == EntityTreeModel::FetchFirstLevelChildCollections) /*|| ( m_collectionFetchStrategy == EntityTreeModel::FetchCollectionsRecursive )*/ ) { fetchCollections( rootCollection, CollectionFetchJob::FirstLevel ); } if ( m_collectionFetchStrategy == EntityTreeModel::FetchCollectionsRecursive ) fetchCollections( rootCollection, CollectionFetchJob::Recursive ); // If the root collection is not collection::root, then it could have items, and they will need to be // retrieved now. if ( m_itemPopulation != EntityTreeModel::NoItemPopulation ) { if (rootCollection != Collection::root()) fetchItems( rootCollection ); } } Collection EntityTreeModelPrivate::getParentCollection( Entity::Id id ) const { QHashIterator > iter( m_childEntities ); while ( iter.hasNext() ) { iter.next(); if ( indexOf( iter.value(), id ) != -1 ) { return m_collections.value( iter.key() ); } } return Collection(); } Collection::List EntityTreeModelPrivate::getParentCollections( const Item &item ) const { Collection::List list; QHashIterator > iter( m_childEntities ); while ( iter.hasNext() ) { iter.next(); if ( indexOf( iter.value(), item.id() ) != -1 ) { list << m_collections.value( iter.key() ); } } return list; } Collection EntityTreeModelPrivate::getParentCollection( const Collection &collection ) const { return m_collections.value( collection.parentCollection().id() ); } Entity::Id EntityTreeModelPrivate::childAt( Collection::Id id, int position, bool *ok ) const { const QList list = m_childEntities.value( id ); if ( list.size() <= position ) { *ok = false; return 0; } *ok = true; return list.at( position )->id; } int EntityTreeModelPrivate::indexOf( Collection::Id parent, Collection::Id collectionId ) const { return indexOf( m_childEntities.value( parent ), collectionId ); } Item EntityTreeModelPrivate::getItem( Item::Id id) const { if ( id > 0 ) id *= -1; return m_items.value( id ); } diff --git a/akonadi/entitytreemodel_p.h b/akonadi/entitytreemodel_p.h index fa5b6718d..838ac6585 100644 --- a/akonadi/entitytreemodel_p.h +++ b/akonadi/entitytreemodel_p.h @@ -1,157 +1,155 @@ /* Copyright (c) 2008 Stephen Kelly 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 ENTITYTREEMODELPRIVATE_H #define ENTITYTREEMODELPRIVATE_H #include #include #include #include #include #include "entitytreemodel.h" namespace Akonadi { class ItemFetchJob; } struct Node { Akonadi::Entity::Id id; Akonadi::Entity::Id parent; enum Type { Item, Collection }; int type; }; namespace Akonadi { /** * @internal */ class EntityTreeModelPrivate { public: EntityTreeModelPrivate( EntityTreeModel *parent ); EntityTreeModel *q_ptr; // void collectionStatisticsChanged( Collection::Id, const Akonadi::CollectionStatistics& ); enum RetrieveDepth { Base, Recursive }; void fetchCollections( const Collection &collection, CollectionFetchJob::Type = CollectionFetchJob::FirstLevel ); void fetchItems( const Collection &collection ); void collectionsFetched( const Akonadi::Collection::List& ); // void resourceTopCollectionsFetched( const Akonadi::Collection::List& ); void itemsFetched( const Akonadi::Item::List& ); void monitoredCollectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ); void monitoredCollectionRemoved( const Akonadi::Collection& ); void monitoredCollectionChanged( const Akonadi::Collection& ); void monitoredCollectionStatisticsChanged( Akonadi::Collection::Id, const Akonadi::CollectionStatistics& ); void monitoredCollectionMoved( const Akonadi::Collection&, const Akonadi::Collection&, const Akonadi::Collection& ); void monitoredItemAdded( const Akonadi::Item&, const Akonadi::Collection& ); void monitoredItemRemoved( const Akonadi::Item& ); void monitoredItemChanged( const Akonadi::Item&, const QSet& ); void monitoredItemMoved( const Akonadi::Item&, const Akonadi::Collection&, const Akonadi::Collection& ); void monitoredItemLinked( const Akonadi::Item&, const Akonadi::Collection& ); void monitoredItemUnlinked( const Akonadi::Item&, const Akonadi::Collection& ); void monitoredMimeTypeChanged( const QString &mimeType, bool monitored ); Collection getParentCollection( Entity::Id id ) const; Collection::List getParentCollections( const Item &item ) const; Collection getParentCollection( const Collection &collection ) const; Entity::Id childAt( Collection::Id, int position, bool *ok ) const; int indexOf( Collection::Id parent, Collection::Id id ) const; Item getItem( Item::Id id ) const; void removeChildEntities(Collection::Id colId); void retrieveAncestors(const Akonadi::Collection& collection); void ancestorsFetched(const Akonadi::Collection::List& collectionList); void insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection& parent ); void insertPendingCollection(const Akonadi::Collection &collection, const Akonadi::Collection& parent, QMutableListIterator &it ); - void insertAncestors(const Akonadi::Collection::List &collectionList ); ItemFetchJob* getItemFetchJob(const Collection &parent, ItemFetchScope scope) const; ItemFetchJob* getItemFetchJob(const Item &item, ItemFetchScope scope) const; void runItemFetchJob(ItemFetchJob* itemFetchJob, const Collection &parent) const; QHash m_collections; QHash m_items; QHash > m_childEntities; QSet m_populatedCols; - Collection::List m_ancestors; QHash m_pendingCollections; QHash > m_pendingChildCollections; Monitor *m_monitor; Collection m_rootCollection; Node *m_rootNode; QString m_rootCollectionDisplayName; QStringList m_mimeTypeFilter; MimeTypeChecker m_mimeChecker; EntityTreeModel::CollectionFetchStrategy m_collectionFetchStrategy; EntityTreeModel::ItemPopulationStrategy m_itemPopulation; bool m_includeUnsubscribed; bool m_includeStatistics; bool m_showRootCollection; void startFirstListJob(); void fetchJobDone( KJob *job ); void copyJobDone( KJob *job ); void moveJobDone( KJob *job ); void updateJobDone( KJob *job ); /** * Returns the index of the node in @p list with the id @p id. Returns -1 if not found. */ int indexOf( const QList &list, Entity::Id id ) const; /** * The id of the collection which starts an item fetch job. This is part of a hack with QObject::sender * in itemsReceivedFromJob to correctly insert items into the model. */ static QByteArray ItemFetchCollectionId() { return "ItemFetchCollectionId"; } Session *m_session; Q_DECLARE_PUBLIC( EntityTreeModel ) }; } #endif diff --git a/akonadi/item.cpp b/akonadi/item.cpp index 64e593a53..507f0308c 100644 --- a/akonadi/item.cpp +++ b/akonadi/item.cpp @@ -1,212 +1,225 @@ /* Copyright (c) 2006 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 "item.h" #include "item_p.h" #include "itemserializer_p.h" #include "protocol_p.h" #include #include using namespace Akonadi; // Change to something != RFC822 as soon as the server supports it const char* Item::FullPayload = "RFC822"; Item::Item() : Entity( new ItemPrivate ) { } Item::Item( Id id ) : Entity( new ItemPrivate( id ) ) { } Item::Item( const QString & mimeType ) : Entity( new ItemPrivate ) { d_func()->mMimeType = mimeType; } Item::Item( const Item &other ) : Entity( other ) { } Item::~Item() { } Item::Flags Item::flags() const { return d_func()->mFlags; } void Item::setFlag( const QByteArray & name ) { Q_D( Item ); d->mFlags.insert( name ); if ( !d->mFlagsOverwritten ) d->mAddedFlags.insert( name ); } void Item::clearFlag( const QByteArray & name ) { Q_D( Item ); d->mFlags.remove( name ); if ( !d->mFlagsOverwritten ) d->mDeletedFlags.insert( name ); } void Item::setFlags( const Flags &flags ) { Q_D( Item ); d->mFlags = flags; d->mFlagsOverwritten = true; } void Item::clearFlags() { Q_D( Item ); d->mFlags.clear(); d->mFlagsOverwritten = true; } QDateTime Item::modificationTime() const { return d_func()->mModificationTime; } void Item::setModificationTime( const QDateTime &datetime ) { d_func()->mModificationTime = datetime; } bool Item::hasFlag( const QByteArray & name ) const { return d_func()->mFlags.contains( name ); } QSet Item::loadedPayloadParts() const { return ItemSerializer::parts( *this ); } QByteArray Item::payloadData() const { int version = 0; QByteArray data; ItemSerializer::serialize( *this, FullPayload, data, version ); return data; } void Item::setPayloadFromData( const QByteArray &data ) { ItemSerializer::deserialize( *this, FullPayload, data, 0, false ); } int Item::revision() const { return d_func()->mRevision; } void Item::setRevision( int rev ) { d_func()->mRevision = rev; } Entity::Id Item::storageCollectionId() const { return d_func()->mCollectionId; } void Item::setStorageCollectionId( Entity::Id collectionId ) { d_func()->mCollectionId = collectionId; } QString Item::mimeType() const { return d_func()->mMimeType; } void Item::setSize( qint64 size ) { Q_D( Item ); d->mSize = size; d->mSizeChanged = true; } qint64 Item::size() const { return d_func()->mSize; } void Item::setMimeType( const QString & mimeType ) { d_func()->mMimeType = mimeType; } bool Item::hasPayload() const { return d_func()->mPayload != 0; } KUrl Item::url( UrlType type ) const { KUrl url; url.setProtocol( QString::fromLatin1("akonadi") ); url.addQueryItem( QLatin1String( "item" ), QString::number( id() ) ); if ( type == UrlWithMimeType ) url.addQueryItem( QLatin1String( "type" ), mimeType() ); return url; } Item Item::fromUrl( const KUrl &url ) { if ( url.protocol() != QLatin1String( "akonadi" ) ) return Item(); const QString itemStr = url.queryItem( QLatin1String( "item" ) ); bool ok = false; Item::Id itemId = itemStr.toLongLong( &ok ); if ( !ok ) return Item(); return Item( itemId ); } PayloadBase* Item::payloadBase() const { return d_func()->mPayload; } void Item::setPayloadBase( PayloadBase* p ) { Q_D( Item ); delete d->mPayload; d->mPayload = p; } +QSet Item::availablePayloadParts() const +{ + return ItemSerializer::availableParts( *this ); +} + +void Item::merge( const Item &other ) +{ + foreach( Attribute *attribute, other.attributes() ) + addAttribute( attribute->clone() ); + + ItemSerializer::merge( *this, other ); +} + AKONADI_DEFINE_PRIVATE( Item ) diff --git a/akonadi/item.h b/akonadi/item.h index 0f4b50cfb..f2fa81fc4 100644 --- a/akonadi/item.h +++ b/akonadi/item.h @@ -1,417 +1,439 @@ /* Copyright (c) 2006 Volker Krause 2007 Till Adam 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_ITEM_H #define AKONADI_ITEM_H #include "akonadi_export.h" #include #include #include "itempayloadinternals_p.h" #include #include #include #include #include #include class KUrl; namespace std { template class auto_ptr; } namespace Akonadi { class ItemPrivate; /** * @short Represents a PIM item stored in Akonadi storage. * * A PIM item consists of one or more parts, allowing a fine-grained access on its * content where needed (eg. mail envelope, mail body and attachments). * * There is also a namespace (prefix) for special parts which are local to Akonadi. * These parts, prefixed by "akonadi-" will never be fetched in the resource. * They are useful for local extensions like agents which might want to add meta data * to items in order to handle them but the meta data should not be stored back to the * resource. * * This class contains beside some type-agnostic information (flags, revision) * a single payload object representing its actual data. Which objects these actually * are depends on the mimetype of the item and the corresponding serializer plugin. * * This class is implicitly shared. * *

Payload

* * Technically the only restriction on payload objects is that they have to be copyable. * For safety reasons, pointer payloads are forbidden as well though, as the * ownership would not be clear. In this case, usage of a shared pointer is * recommended (such as boost::shared_ptr or QSharedPointer). * * Using a shared pointer is also recommended in case the payload uses polymorphic * types. For supported shared pointer types implicit casting is provided when possible. * * When using a value-based class as payload, it is recommended to use one that does * support implicit sharing as setting and retrieving a payload as well as copying * an Akonadi::Item object imply copying of the payload object. * * The availability of a payload of a specific type can be checked using hasPayload(), * payloads can be retrieved by using payload() and set by using setPayload(). Refer * to the documentation of those methods for more details. * * @author Volker Krause , Till Adam */ class AKONADI_EXPORT Item : public Entity { public: /** * Describes a list of items. */ typedef QList List; /** * Describes a flag name. */ typedef QByteArray Flag; /** * Describes a set of flag names. */ typedef QSet Flags; /** * Describes the part name that is used to fetch the * full payload of an item. */ static const char* FullPayload; /** * Creates a new item. */ Item(); /** * Creates a new item with the given unique @p id. */ explicit Item( Id id ); /** * Creates a new item with the given mime type. * * @param mimeType The mime type of the item. */ explicit Item( const QString &mimeType ); /** * Creates a new item from an @p other item. */ Item( const Item &other ); /** * Destroys the item. */ ~Item(); /** * Creates an item from the given @p url. */ static Item fromUrl( const KUrl &url ); /** * Returns all flags of this item. */ Flags flags() const; /** * Returns the timestamp of the last modification of this item. * @since 4.2 */ QDateTime modificationTime() const; /** * Sets the timestamp of the last modification of this item. * * @note Do not modify this value from within an application, * it is updated automatically by the revision checking functions. * @since 4.2 */ void setModificationTime( const QDateTime &datetime ); /** * Returns whether the flag with the given @p name is * set in the item. */ bool hasFlag( const QByteArray &name ) const; /** * Sets the flag with the given @p name in the item. */ void setFlag( const QByteArray &name ); /** * Removes the flag with the given @p name from the item. */ void clearFlag( const QByteArray &name ); /** * Overwrites all flags of the item by the given @p flags. */ void setFlags( const Flags &flags ); /** * Removes all flags from the item. */ void clearFlags(); /** * Sets the payload based on the canonical representation normally * used for data of this mime type. * * @param data The encoded data. * @see fullPayloadData */ void setPayloadFromData( const QByteArray &data ); /** * Returns the full payload in its canonical representation, e.g. the * binary or textual format usually used for data with this mime type. * This is useful when communicating with non-Akonadi application by * e.g. drag&drop, copy&paste or stored files. */ QByteArray payloadData() const; /** * Returns the list of loaded payload parts. This is not necessarily * identical to all parts in the cache or to all available parts on the backend. */ QSet loadedPayloadParts() const; /** * Sets the @p revision number of the item. * * @note Do not modify this value from within an application, * it is updated automatically by the revision checking functions. */ void setRevision( int revision ); /** * Returns the revision number of the item. */ int revision() const; /** * Returns the unique identifier of the collection this item is stored in. There is only * a single such collection, although the item can be linked into arbitrary many * virtual collections. * Calling this method makes sense only after running an ItemFetchJob on the item. * @returns the collection ID if it is know, -1 otherwise. * @since 4.3 */ Entity::Id storageCollectionId() const; /** * Set the size of the item in bytes. * * @since 4.2 */ void setSize( qint64 size ); /** * Returns the size of the items in bytes. * * @since 4.2 */ qint64 size() const; /** * Sets the mime type of the item to @p mimeType. */ void setMimeType( const QString &mimeType ); /** * Returns the mime type of the item. */ QString mimeType() const; /** * Sets the payload object of this PIM item. * * @param p The payload object. Must be copyable and must not be a pointer, * will cause a compilation failure otherwise. Using a type that can be copied * fast (such as implicitly shared classes) is recommended. * If the payload type is polymorphic and you intend to set and retrieve payload * objects with mismatching but castable types, make sure to use a supported * shared pointer implementation (currently boost::shared_ptr and QSharedPointer) * and make sure there is a specialization of Akonadi::super_trait for your class. */ template void setPayload( const T &p ); //@cond PRIVATE template void setPayload( T* p ); template void setPayload( std::auto_ptr p ); //@endcond /** * Returns the payload object of this PIM item. This method will only succeed if either * you requested the exact same payload type that was put in or the payload uses a * supported shared pointer type (currently boost::shared_ptr and QSharedPointer), and * is castable to the requested type. For this to work there needs to be a specialization * of Akonadi::super_trait of the used classes. * * If a mismatching or non-castable payload type is requested, an Akonadi::PayloadException * is thrown. Therefore it is generally recommended to guard calls to payload() with a * corresponding hasPayload() call. * * Trying to retrieve a pointer type will fail to compile. */ template T payload() const; /** * Returns whether the item has a payload object. */ bool hasPayload() const; /** * Returns whether the item has a payload of type @c T. * This method will only return @c true if either you requested the exact same payload type * that was put in or the payload uses a supported shared pointer type (currently boost::shared_ptr * and QSharedPointer), and is castable to the requested type. For this to work there needs * to be a specialization of Akonadi::super_trait of the used classes. * * Trying to retrieve a pointer type will fail to compile. */ template bool hasPayload() const; /** * Describes the type of url which is returned in url(). */ enum UrlType { UrlShort = 0, ///< A short url which contains the identifier only (default) UrlWithMimeType = 1 ///< A url with identifier and mimetype }; /** * Returns the url of the item. */ KUrl url( UrlType type = UrlShort ) const; + /** + * Returns the parts available for this item. + * + * @since 4.4 + */ + QSet availablePayloadParts() const; + + /** + * Merges the Item @p other into this item. + * Any parts or attributes available in other, will be merged into this item, + * and the payload parts of other will be inserted into this item, overwriting + * any existing parts with the same part name. + * + * If there is an ItemSerialzerPluginV2 for the type, the merge method in that plugin is + * used to perform the merge. If only an ItemSerialzerPlugin class is found, or the merge + * method of the -V2 plugin is not implemented, the merge is performed with multiple deserializations + * of the payload. + * + * @since 4.4 + */ + void merge( const Item &other ); + private: //@cond PRIVATE friend class ItemModifyJob; friend class ItemFetchJob; PayloadBase* payloadBase() const; void setPayloadBase( PayloadBase* ); /** * Set the collection ID to where the item is stored in. Should be set only by the ItemFetchJob. * @param collectionId the unique identifier of the the collection where this item is stored in. * @since 4.3 */ void setStorageCollectionId( Entity::Id collectionId); //@endcond AKONADI_DECLARE_PRIVATE( Item ) }; template T Item::payload() const { BOOST_STATIC_ASSERT( !boost::is_pointer::value ); if ( !payloadBase() ) throw PayloadException( "No payload set" ); typedef Internal::PayloadTrait PayloadType; if ( PayloadType::isPolymorphic ) { try { const typename PayloadType::SuperType sp = payload(); return PayloadType::castFrom( sp ); } catch ( const Akonadi::PayloadException& ) {} } Payload *p = Internal::payload_cast( payloadBase() ); if ( !p ) { throw PayloadException( QString::fromLatin1( "Wrong payload type (is '%1', requested '%2')" ) .arg( QLatin1String( payloadBase()->typeName() ) ) .arg( QLatin1String( typeid(p).name() ) ) ); } return p->payload; } template bool Item::hasPayload() const { BOOST_STATIC_ASSERT( !boost::is_pointer::value ); if ( !hasPayload() ) return false; typedef Internal::PayloadTrait PayloadType; if ( PayloadType::isPolymorphic ) { try { const typename PayloadType::SuperType sp = payload(); return PayloadType::canCastFrom( sp ); } catch ( const Akonadi::PayloadException& ) {} } return Internal::payload_cast( payloadBase() ); } template void Item::setPayload( const T &p ) { typedef Internal::PayloadTrait PayloadType; if ( PayloadType::isPolymorphic ) { const typename PayloadType::SuperType sp = PayloadType::template castTo( p ); if ( !Internal::PayloadTrait::isNull( sp ) || PayloadType::isNull( p ) ) { setPayload( sp ); return; } } setPayloadBase( new Payload( p ) ); } template void Item::setPayload( T* p ) { p.You_MUST_NOT_use_a_pointer_as_payload; } template void Item::setPayload( std::auto_ptr p ) { p.Nice_try_but_a_std_auto_ptr_is_not_allowed_as_payload_either; } } Q_DECLARE_METATYPE(Akonadi::Item) Q_DECLARE_METATYPE(Akonadi::Item::List) #endif diff --git a/akonadi/itemserializer.cpp b/akonadi/itemserializer.cpp index 024cc85a6..79293091b 100644 --- a/akonadi/itemserializer.cpp +++ b/akonadi/itemserializer.cpp @@ -1,294 +1,336 @@ /* Copyright (c) 2007 Till Adam 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. */ #include "itemserializer_p.h" #include "item.h" #include "itemserializerplugin.h" #include "attributefactory.h" // KDE core #include #include #include // Qt #include #include #include #include #include #include #include #include // temporary #include "pluginloader_p.h" namespace Akonadi { class DefaultItemSerializerPlugin; class DefaultItemSerializerPlugin : public ItemSerializerPlugin { public: DefaultItemSerializerPlugin() { } bool deserialize( Item& item, const QByteArray& label, QIODevice& data, int ) { if ( label != Item::FullPayload ) return false; item.setPayload( data.readAll() ); return true; } void serialize( const Item& item, const QByteArray& label, QIODevice& data, int& ) { Q_ASSERT( label == Item::FullPayload ); if ( item.hasPayload() ) data.write( item.payload() ); } }; K_GLOBAL_STATIC( DefaultItemSerializerPlugin, s_defaultItemSerializerPlugin ) class PluginEntry { public: PluginEntry() : mPlugin( 0 ) { } explicit PluginEntry( const QString &identifier, ItemSerializerPlugin *plugin = 0 ) : mIdentifier( identifier ), mPlugin( plugin ) { } inline ItemSerializerPlugin* plugin() const { if ( mPlugin ) return mPlugin; QObject *object = PluginLoader::self()->createForName( mIdentifier ); if ( !object ) { kWarning() << "ItemSerializerPluginLoader: " << "plugin" << mIdentifier << "is not valid!" << endl; // we try to use the default in that case mPlugin = s_defaultItemSerializerPlugin; } mPlugin = qobject_cast( object ); if ( !mPlugin ) { kWarning() << "ItemSerializerPluginLoader: " << "plugin" << mIdentifier << "doesn't provide interface ItemSerializerPlugin!" << endl; // we try to use the default in that case mPlugin = s_defaultItemSerializerPlugin; } Q_ASSERT( mPlugin ); return mPlugin; } QString type() const { return mIdentifier; } bool operator<( const PluginEntry &other ) const { return mIdentifier < other.mIdentifier; } bool operator<( const QString &type ) const { return mIdentifier < type; } private: QString mIdentifier; mutable ItemSerializerPlugin *mPlugin; }; static bool operator<( const QString &type, const PluginEntry &entry ) { return type < entry.type(); } class PluginRegistry { public: PluginRegistry() : mDefaultPlugin( PluginEntry( QLatin1String("application/octet-stream"), s_defaultItemSerializerPlugin ) ) { const PluginLoader* pl = PluginLoader::self(); if ( !pl ) { kWarning() << "Cannot instantiate plugin loader!" << endl; return; } const QStringList types = pl->types(); kDebug() << "ItemSerializerPluginLoader: " << "found" << types.size() << "plugins." << endl; allPlugins.reserve( types.size() + 1 ); foreach ( const QString &type, types ) allPlugins.append( PluginEntry( type ) ); allPlugins.append( mDefaultPlugin ); std::sort( allPlugins.begin(), allPlugins.end() ); } const PluginEntry& findBestMatch( const QString &type ) { KMimeType::Ptr mimeType = KMimeType::mimeType( type, KMimeType::ResolveAliases ); if ( mimeType.isNull() ) return mDefaultPlugin; // step 1: find all plugins that match at all QVector matchingIndexes; for ( int i = 0, end = allPlugins.size(); i < end; ++i ) { if ( mimeType->is( allPlugins[i].type() ) ) matchingIndexes.append( i ); } // 0 matches: no luck (shouldn't happend though, as application/octet-stream matches everything) if ( matchingIndexes.isEmpty() ) return mDefaultPlugin; // 1 match: we are done if ( matchingIndexes.size() == 1 ) return allPlugins[matchingIndexes.first()]; // step 2: if we have more than one match, find the most specific one using topological sort boost::adjacency_list<> graph( matchingIndexes.size() ); for ( int i = 0, end = matchingIndexes.size() ; i != end ; ++i ) { KMimeType::Ptr mimeType = KMimeType::mimeType( allPlugins[matchingIndexes[i]].type(), KMimeType::ResolveAliases ); if ( mimeType.isNull() ) continue; for ( int j = 0; j != end; ++j ) { if ( i != j && mimeType->is( allPlugins[matchingIndexes[j]].type() ) ) boost::add_edge( j, i, graph ); } } QVector order; order.reserve( allPlugins.size() ); try { boost::topological_sort( graph, std::back_inserter( order ) ); } catch ( boost::not_a_dag &e ) { kWarning() << "Mimetype tree is not a DAG!"; return mDefaultPlugin; } return allPlugins[matchingIndexes[order.first()]]; } QVector allPlugins; QHash cachedPlugins; private: PluginEntry mDefaultPlugin; }; K_GLOBAL_STATIC( PluginRegistry, s_pluginRegistry ) /*static*/ void ItemSerializer::deserialize( Item& item, const QByteArray& label, const QByteArray& data, int version, bool external ) { if ( external ) { QFile file( QString::fromUtf8(data) ); if ( file.open( QIODevice:: ReadOnly ) ) { deserialize( item, label, file, version ); file.close(); } } else { QBuffer buffer; buffer.setData( data ); buffer.open( QIODevice::ReadOnly ); buffer.seek( 0 ); deserialize( item, label, buffer, version ); buffer.close(); } } /*static*/ void ItemSerializer::deserialize( Item& item, const QByteArray& label, QIODevice& data, int version ) { - if ( !ItemSerializer::pluginForMimeType( item.mimeType() ).deserialize( item, label, data, version ) ) + if ( !ItemSerializer::pluginForMimeType( item.mimeType() )->deserialize( item, label, data, version ) ) kWarning() << "Unable to deserialize payload part:" << label; } /*static*/ void ItemSerializer::serialize( const Item& item, const QByteArray& label, QByteArray& data, int &version ) { QBuffer buffer; buffer.setBuffer( &data ); buffer.open( QIODevice::WriteOnly ); buffer.seek( 0 ); serialize( item, label, buffer, version ); buffer.close(); } /*static*/ void ItemSerializer::serialize( const Item& item, const QByteArray& label, QIODevice& data, int &version ) { if ( !item.hasPayload() ) return; - ItemSerializerPlugin& plugin = pluginForMimeType( item.mimeType() ); - plugin.serialize( item, label, data, version ); + ItemSerializerPlugin *plugin = pluginForMimeType( item.mimeType() ); + plugin->serialize( item, label, data, version ); } -QSet ItemSerializer::parts(const Item & item) +void ItemSerializer::merge( Item &item, const Item &other ) +{ + if ( !other.hasPayload() ) + return; + + ItemSerializerPlugin *plugin = pluginForMimeType( item.mimeType() ); + + ItemSerializerPluginV2 *pluginV2 = dynamic_cast( plugin ); + if ( pluginV2 ) { + pluginV2->merge( item, other ); + return; + } + + // Old-school merge: + QBuffer buffer; + buffer.setBuffer( &other.payloadData() ); + buffer.open( QIODevice::ReadOnly ); + + foreach ( const QByteArray &part, other.loadedPayloadParts() ) { + buffer.seek( 0 ); + deserialize( item, part, buffer, 0 ); + } + + buffer.close(); +} + +QSet ItemSerializer::parts( const Item & item ) { if ( !item.hasPayload() ) return QSet(); - return pluginForMimeType( item.mimeType() ).parts( item ); + return pluginForMimeType( item.mimeType() )->parts( item ); +} + +QSet ItemSerializer::availableParts( const Item & item ) +{ + if ( !item.hasPayload() ) + return QSet(); + ItemSerializerPlugin *plugin = pluginForMimeType( item.mimeType() ); + ItemSerializerPluginV2 *pluginV2 = dynamic_cast( plugin ); + + if ( pluginV2 ) + return pluginV2->availableParts( item ); + + if (item.hasPayload()) + return QSet(); + + return QSet() << Item::FullPayload; } /*static*/ -ItemSerializerPlugin& ItemSerializer::pluginForMimeType( const QString & mimetype ) +ItemSerializerPlugin* ItemSerializer::pluginForMimeType( const QString & mimetype ) { // plugin cached, so let's take that one if ( s_pluginRegistry->cachedPlugins.contains( mimetype ) ) - return *(s_pluginRegistry->cachedPlugins.value( mimetype )); + return s_pluginRegistry->cachedPlugins.value( mimetype ); ItemSerializerPlugin *plugin = 0; // check if we have one that matches exactly const QVector::const_iterator it = qBinaryFind( s_pluginRegistry->allPlugins.constBegin(), s_pluginRegistry->allPlugins.constEnd(), mimetype ); if ( it != s_pluginRegistry->allPlugins.constEnd() ) { - plugin = (*it).plugin(); + plugin = ( *it ).plugin(); } else { // check if we have a more generic plugin const PluginEntry &entry = s_pluginRegistry->findBestMatch( mimetype ); kDebug() << "Did not find exactly matching serializer plugin for type" << mimetype << ", taking" << entry.type() << "as the closest match"; plugin = entry.plugin(); } Q_ASSERT(plugin); s_pluginRegistry->cachedPlugins.insert( mimetype, plugin ); - return *plugin; + return plugin; } } diff --git a/akonadi/itemserializer_p.h b/akonadi/itemserializer_p.h index fa996e672..7d4b42402 100644 --- a/akonadi/itemserializer_p.h +++ b/akonadi/itemserializer_p.h @@ -1,61 +1,77 @@ /* Copyright (c) 2007 Till Adam 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_ITEM_SERIALIZER_P_H #define AKONADI_ITEM_SERIALIZER_P_H #include #include #include "akonadiprivate_export.h" class QIODevice; namespace Akonadi { class Item; class ItemSerializerPlugin; /** @internal Serialization/Deserialization of item parts, serializer plugin management. */ class AKONADI_TESTS_EXPORT ItemSerializer { public: /** throws ItemSerializerException on failure */ static void deserialize( Item& item, const QByteArray& label, const QByteArray& data, int version, bool external ); /** throws ItemSerializerException on failure */ static void deserialize( Item& item, const QByteArray& label, QIODevice& data, int version ); /** throws ItemSerializerException on failure */ static void serialize( const Item& item, const QByteArray& label, QByteArray& data, int &version ); /** throws ItemSerializerException on failure */ static void serialize( const Item& item, const QByteArray& label, QIODevice& data, int &version ); - /** Returns a list of parts available in the item payload. */ + /** + * Throws ItemSerializerException on failure. + * + * @since 4.4 + */ + static void merge( Item& item, const Item &other ); + + /** + * Returns a list of parts available in the item payload. + */ static QSet parts( const Item &item ); + /** + * Returns a list of parts available remotely in the item payload. + * + * @since 4.4 + */ + static QSet availableParts( const Item &item ); + private: - static ItemSerializerPlugin& pluginForMimeType( const QString& mimetype ); + static ItemSerializerPlugin* pluginForMimeType( const QString& mimetype ); }; } #endif diff --git a/akonadi/itemserializerplugin.cpp b/akonadi/itemserializerplugin.cpp index 0631d7c10..4f84e937c 100644 --- a/akonadi/itemserializerplugin.cpp +++ b/akonadi/itemserializerplugin.cpp @@ -1,36 +1,65 @@ /* Copyright (c) 2007 Till Adam 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. */ #include "itemserializerplugin.h" #include "item.h" +#include + using namespace Akonadi; ItemSerializerPlugin::~ItemSerializerPlugin() { } -QSet ItemSerializerPlugin::parts(const Item & item) const +QSet ItemSerializerPlugin::parts( const Item & item ) const { QSet set; if ( item.hasPayload() ) set.insert( Item::FullPayload ); + return set; } + +ItemSerializerPluginV2::~ItemSerializerPluginV2() +{ +} + +QSet ItemSerializerPluginV2::availableParts( const Item & item ) const +{ + if ( item.hasPayload() ) + return QSet(); + + return QSet() << Item::FullPayload; +} + +void ItemSerializerPluginV2::merge( Item &item, const Item &other ) +{ + QBuffer buffer; + buffer.setBuffer( &other.payloadData() ); + buffer.open( QIODevice::ReadOnly ); + + foreach ( const QByteArray part, other.loadedPayloadParts() ) { + buffer.seek( 0 ); + deserialize( item, part, buffer, 0 ); + } + + buffer.close(); +} diff --git a/akonadi/itemserializerplugin.h b/akonadi/itemserializerplugin.h index aae3801b0..8a84389af 100644 --- a/akonadi/itemserializerplugin.h +++ b/akonadi/itemserializerplugin.h @@ -1,181 +1,214 @@ /* Copyright (c) 2007 Till Adam 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_ITEMSERIALIZERPLUGIN_H #define AKONADI_ITEMSERIALIZERPLUGIN_H #include #include +#include "item.h" #include "akonadi_export.h" class QIODevice; namespace Akonadi { -class Item; /** * @short The base class for item type serializer plugins. * * Serializer plugins convert between the payload of Akonadi::Item objects and * a textual or binary representation of the actual content data. * This allows to easily add support for new types to Akonadi. * * The following example shows how to implement a serializer plugin for * a new data type PimNote. * * The PimNote data structure: * @code * typedef struct { * QString author; * QDateTime dateTime; * QString text; * } PimNote; * @endcode * * The serializer plugin code: * @code * #include * * class SerializerPluginPimNote : public QObject, public Akonadi::ItemSerializerPlugin * { * Q_OBJECT * Q_INTERFACES( Akonadi::ItemSerializerPlugin ) * * public: * bool deserialize( Akonadi::Item& item, const QByteArray& label, QIODevice& data, int version ) * { * // we don't handle versions in this example * Q_UNUSED( version ); * * // we work only on full payload * if ( label != Akonadi::Item::FullPayload ) * return false; * * QDataStream stream( &data ); * * PimNote note; * stream >> note.author; * stream >> note.dateTime; * stream >> note.text; * * item.setPayload( note ); * * return true; * } * * void serialize( const Akonadi::Item& item, const QByteArray& label, QIODevice& data, int &version ) * { * // we don't handle versions in this example * Q_UNUSED( version ); * * if ( label != Akonadi::Item::FullPayload || !item.hasPayload() ) * return; * * QDataStream stream( &data ); * * PimNote note = item.payload(); * * stream << note.author; * stream << note.dateTime; * stream << note.text; * } * }; * * Q_EXPORT_PLUGIN2( akonadi_serializer_pimnote, SerializerPluginPimNote ) * * @endcode * * The desktop file: * @code * [Misc] * Name=Pim Note Serializer * Comment=An Akonadi serializer plugin for note objects * * [Plugin] * Type=application/x-pimnote * X-KDE-Library=akonadi_serializer_pimnote * @endcode * * @author Till Adam , Volker Krause */ class AKONADI_EXPORT ItemSerializerPlugin { -public: + public: /** * Destroys the item serializer plugin. */ virtual ~ItemSerializerPlugin(); /** * Converts serialized item data provided in @p data into payload for @p item. * * @param item The item to which the payload should be added. * It is guaranteed to have a mime type matching one of the supported * mime types of this plugin. * However it might contain a unsuited payload added manually * by the application developer. * Verifying the payload type in case a payload is already available * is recommended therefore. * @param label The part identifier of the part to deserialize. * @p label might be an unsupported item part, return @c false if this is the case. * @param data A QIODevice providing access to the serialized data. * The QIODevice is opened in read-only mode and positioned at the beginning. * The QIODevice is guaranteed to be valid. * @param version The version of the data format as set by the user in serialize() or @c 0 (default). * @return @c false if the specified part is not supported by this plugin, @c true if the part * could be de-serialized successfully. */ virtual bool deserialize( Item& item, const QByteArray& label, QIODevice& data, int version ) = 0; /** * Convert the payload object provided in @p item into its serialzed form into @p data. * * @param item The item which contains the payload. * It is guaranteed to have a mimetype matching one of the supported * mimetypes of this plugin as well as the existence of a payload object. * However it might contain an unsupported payload added manually by * the application developer. * Verifying the payload type is recommended therefore. * @param label The part identifier of the part to serialize. * @p label will be one of the item parts returned by parts(). * @param data The QIODevice where the serialized data should be written to. * The QIODevice is opened in write-only mode and positioned at the beginning. * The QIODevice is guaranteed to be valid. * @param version The version of the data format. Can be set by the user to handle different * versions. */ virtual void serialize( const Item& item, const QByteArray& label, QIODevice& data, int &version ) = 0; /** * Returns a list of available parts for the given item payload. * The default implementation returns Item::FullPayload if a payload is set. * * @param item The item. */ virtual QSet parts( const Item &item ) const; + +}; + +class AKONADI_EXPORT ItemSerializerPluginV2 : public ItemSerializerPlugin +{ + public: + /** + * Destroys the item serializer plugin. + */ + virtual ~ItemSerializerPluginV2(); + + /** + * Merges the payload parts in @p other into @p item. + * + * The default implementation is slow as it requires serializing @p other, and deserializing @p item multiple times. + * Reimplementing this is recommended if your type uses payload parts. + * + * @since 4.4 + */ + virtual void merge( Item &item, const Item &other ); + + /** + * Returns the parts available in the item @p item. + * + * This should be reimplemented to return available parts. + * + * The default implementation returns an empty set if the item has a payload, + * and a set containing Item::FullPayload if the item has no payload. + * + * @since 4.4 + */ + virtual QSet availableParts( const Item &item ) const; }; } Q_DECLARE_INTERFACE( Akonadi::ItemSerializerPlugin, "org.freedesktop.Akonadi.ItemSerializerPlugin/1.0" ) +Q_DECLARE_INTERFACE( Akonadi::ItemSerializerPluginV2, "org.freedesktop.Akonadi.ItemSerializerPlugin/1.1" ) #endif diff --git a/akonadi/partfetcher.cpp b/akonadi/partfetcher.cpp new file mode 100755 index 000000000..62f9af93b --- /dev/null +++ b/akonadi/partfetcher.cpp @@ -0,0 +1,174 @@ +/* + Copyright (c) 2009 Stephen Kelly + + 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 "partfetcher.h" + +#include "entitytreemodel.h" +#include "session.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" + +using namespace Akonadi; + +namespace Akonadi +{ + +class PartFetcherPrivate +{ + PartFetcherPrivate( PartFetcher *partFetcher, const QModelIndex &index, const QByteArray &partName ) + : m_persistentIndex( index ), m_partName( partName ), q_ptr( partFetcher ) + { + } + + void fetchJobDone( KJob* ); + + void modelDataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight ); + + QPersistentModelIndex m_persistentIndex; + QByteArray m_partName; + Item m_item; + + Q_DECLARE_PUBLIC( PartFetcher ) + PartFetcher *q_ptr; +}; + +} + +void PartFetcherPrivate::fetchJobDone( KJob *job ) +{ + Q_Q( PartFetcher ); + if ( job->error() ) { + q->setError( KJob::UserDefinedError ); + q->setErrorText( QLatin1String( "Unable to fetch item for index" ) ); + q->emitResult(); + return; + } + + ItemFetchJob *fetchJob = qobject_cast( job ); + + const Item::List list = fetchJob->items(); + + Q_ASSERT( list.size() == 1 ); + + // If m_persistentIndex comes from a selection proxy model, it could become + // invalid if the user clicks around a lot. + if ( !m_persistentIndex.isValid() ) { + q->setError( KJob::UserDefinedError ); + q->setErrorText( QLatin1String( "Index is no longer available" ) ); + q->emitResult(); + return; + } + + const QSet loadedParts = m_persistentIndex.data( EntityTreeModel::LoadedPartsRole ).value >(); + + Q_ASSERT( !loadedParts.contains( m_partName ) ); + + Item item = m_persistentIndex.data( EntityTreeModel::ItemRole ).value(); + + item.merge( list.at( 0 ) ); + + QAbstractItemModel *model = const_cast( m_persistentIndex.model() ); + + Q_ASSERT( model ); + + QVariant itemVariant = QVariant::fromValue( item ); + model->setData( m_persistentIndex, itemVariant, EntityTreeModel::ItemRole ); + + m_item = item; + + emit q->emitResult(); +} + +PartFetcher::PartFetcher( const QModelIndex &index, const QByteArray &partName, QObject *parent ) + : KJob( parent ), d_ptr( new PartFetcherPrivate( this, index, partName ) ) +{ +} + +void PartFetcher::start() +{ + Q_D( PartFetcher ); + + const QModelIndex index = d->m_persistentIndex; + + const QSet loadedParts = index.data( EntityTreeModel::LoadedPartsRole ).value >(); + + if ( loadedParts.contains( d->m_partName ) ) { + d->m_item = d->m_persistentIndex.data( EntityTreeModel::ItemRole ).value(); + emitResult(); + return; + } + + const QSet availableParts = index.data( EntityTreeModel::AvailablePartsRole ).value >(); + if ( !availableParts.contains( d->m_partName ) ) { + setError( UserDefinedError ); + setErrorText( QString::fromLatin1( "Payload part '%1' is not available for this index" ) + .arg( QString::fromLatin1( d->m_partName ) ) ); + emitResult(); + return; + } + + Akonadi::Session *session = qobject_cast( qvariant_cast( index.data( EntityTreeModel::SessionRole ) ) ); + + if ( !session ) { + setError( UserDefinedError ); + setErrorText( QLatin1String( "No session available for this index" ) ); + emitResult(); + return; + } + + const Akonadi::Item item = index.data( EntityTreeModel::ItemRole ).value(); + + if ( !item.isValid() ) { + setError( UserDefinedError ); + setErrorText( QLatin1String( "No item available for this index" ) ); + emitResult(); + return; + } + + ItemFetchScope scope; + scope.fetchPayloadPart( d->m_partName ); + ItemFetchJob *itemFetchJob = new Akonadi::ItemFetchJob( item, session ); + itemFetchJob->setFetchScope( scope ); + + connect( itemFetchJob, SIGNAL( result( KJob* ) ), + this, SLOT( fetchJobDone( KJob* ) ) ); +} + +QModelIndex PartFetcher::index() const +{ + Q_D( const PartFetcher ); + + return d->m_persistentIndex; +} + +QByteArray PartFetcher::partName() const +{ + Q_D( const PartFetcher ); + + return d->m_partName; +} + +Item PartFetcher::item() const +{ + Q_D( const PartFetcher ); + + return d->m_item; +} + +#include "partfetcher.moc" diff --git a/akonadi/partfetcher.h b/akonadi/partfetcher.h new file mode 100755 index 000000000..ca896ae81 --- /dev/null +++ b/akonadi/partfetcher.h @@ -0,0 +1,118 @@ +/* + Copyright (c) 2009 Stephen Kelly + + 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_PART_FETCHER_H +#define AKONADI_PART_FETCHER_H + +#include + +#include "akonadi_export.h" + +class QModelIndex; + +namespace Akonadi +{ + +class Item; +class PartFetcherPrivate; + +/** + * @short Convenience class for getting payload parts from an Akonadi Model. + * + * This class can be used to retrieve individual payload parts from an EntityTreeModel, + * and fetch them asynchronously from the Akonadi storage if necessary. + * + * The requested part is emitted though the partFetched signal. + * + * Example: + * + * @code + * + * const QModelIndex index = view->selectionModel()->currentIndex(); + * + * PartFetcher *fetcher = new PartFetcher( index, Akonadi::MessagePart::Envelope ); + * connect( fetcher, SIGNAL( result( KJob* ) ), SLOT( fetchResult( KJob* ) ) ); + * fetcher->start(); + * + * ... + * + * MyClass::fetchResult( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << job->errorText(); + * return; + * } + * + * PartFetcher *fetcher = qobject_cast( job ); + * + * const Item item = fetcher->item(); + * // do something with the item + * } + * + * @endcode + * + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADI_EXPORT PartFetcher : public KJob +{ + Q_OBJECT + + public: + /** + * Creates a new part fetcher. + * + * @param index The index of the item to fetch the part from. + * @param partName The name of the payload part to fetch. + * @param parent The parent object. + */ + explicit PartFetcher( const QModelIndex &index, const QByteArray &partName, QObject *parent = 0 ); + + /** + * Starts the fetch operation. + */ + virtual void start(); + + /** + * Returns the index of the item the part was fetched from. + */ + QModelIndex index() const; + + /** + * Returns the name of the part that has been fetched. + */ + QByteArray partName() const; + + /** + * Returns the item that contains the fetched payload part. + */ + Item item() const; + + private: + //@cond PRIVATE + Q_DECLARE_PRIVATE( Akonadi::PartFetcher ) + PartFetcherPrivate *d_ptr; + + Q_PRIVATE_SLOT( d_func(), void fetchJobDone( KJob *job ) ) + //@endcond +}; + +} + +#endif diff --git a/akonadi/protocolhelper.cpp b/akonadi/protocolhelper.cpp index 6a40163f3..a51686ada 100644 --- a/akonadi/protocolhelper.cpp +++ b/akonadi/protocolhelper.cpp @@ -1,253 +1,263 @@ /* 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 ancestors; ImapParser::parseParenthesizedList( data, ancestors ); Entity* current = entity; 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() ) { current->setParentCollection( Collection::root() ); break; } current->parentCollection().setId( uid ); current->parentCollection().setRemoteId( rid ); current = ¤t->parentCollection(); } } int ProtocolHelper::parseCollection(const QByteArray & data, Collection & collection, int start) { int pos = start; // collection and parent id Collection::Id colId = -1; bool ok = false; pos = ImapParser::parseNumber( data, colId, &ok, pos ); if ( !ok || colId <= 0 ) { kDebug() << "Could not parse collection id from response:" << data; return start; } Collection::Id parentId = -1; pos = ImapParser::parseNumber( data, parentId, &ok, pos ); if ( !ok || parentId < 0 ) { kDebug() << "Could not parse parent id from response:" << data; return start; } collection = Collection( colId ); collection.setParentCollection( Collection( parentId ) ); // attributes QVarLengthArray 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" ) { parseAncestors( value, &collection ); } 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; } + +QByteArray ProtocolHelper::hierarchicalRidToByteArray( const Collection &col ) +{ + if ( col == Collection::root() ) + return QByteArray("(0 \"\")"); + if ( col.remoteId().isEmpty() ) + return QByteArray(); + const QByteArray parentHrid = hierarchicalRidToByteArray( col.parentCollection() ); + return '(' + QByteArray::number( col.id() ) + ' ' + ImapParser::quote( col.remoteId().toUtf8() ) + ") " + parentHrid; +} diff --git a/akonadi/protocolhelper_p.h b/akonadi/protocolhelper_p.h index c64767234..b87819774 100644 --- a/akonadi/protocolhelper_p.h +++ b/akonadi/protocolhelper_p.h @@ -1,99 +1,105 @@ /* 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. */ #ifndef AKONADI_PROTOCOLHELPER_P_H #define AKONADI_PROTOCOLHELPER_P_H #include #include #include namespace Akonadi { /** @internal Helper methods for converting between libakonadi objects and their protocol representation. @todo Add unit tests for this. @todo Use exceptions for a useful error handling */ class ProtocolHelper { public: /** Part namespaces. */ enum PartNamespace { PartGlobal, PartPayload, PartAttribute }; /** Parse a cache policy definition. @param data The input data. @param policy The parsed cache policy. @param start Start of the data, ie. postion after the label. @returns Position in data after the cache policy description. */ static int parseCachePolicy( const QByteArray &data, CachePolicy &policy, int start = 0 ); /** Convert a cache policy object into its protocol representation. */ static QByteArray cachePolicyToByteArray( const CachePolicy &policy ); /** Convert a ancestor chain from its protocol representation into an Entity object. */ static void parseAncestors( const QByteArray &data, Entity *entity, int start = 0 ); /** Parse a collection description. @param data The input data. @param collection The parsed collection. @param start Start of the data. @returns Position in data after the collection description. */ static int parseCollection( const QByteArray &data, Collection &collection, int start = 0 ); /** Convert attributes to their protocol representation. */ static QByteArray attributesToByteArray( const Entity &entity, bool ns = false ); /** Encodes part label and namespace. */ static QByteArray encodePartIdentifier( PartNamespace ns, const QByteArray &label, int version = 0 ); /** Decode part label and namespace. */ static QByteArray decodePartIdentifier( const QByteArray &data, PartNamespace &ns ); /** Converts the given set of items into a protocol representation. @throws A Akonadi::Exception if the item set contains items with missing/invalid identifiers. */ static QByteArray itemSetToByteArray( const Item::List &items, const QByteArray &command ); + + /** + Converts the given collection's hierarchical RID into a protocol representation. + Assumes @p col has a valid hierarchical RID, so check that before! + */ + static QByteArray hierarchicalRidToByteArray( const Collection &col ); }; } #endif diff --git a/akonadi/session_p.h b/akonadi/session_p.h index 7c889327d..80624b2f3 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 19; } + static int minimumProtocolVersion() { return 20; } 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/CMakeLists.txt b/akonadi/tests/CMakeLists.txt index 2d7576566..a7d3538db 100644 --- a/akonadi/tests/CMakeLists.txt +++ b/akonadi/tests/CMakeLists.txt @@ -1,129 +1,130 @@ if(${EXECUTABLE_OUTPUT_PATH}) set( PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH} ) else(${EXECUTABLE_OUTPUT_PATH}) set( PREVIOUS_EXEC_OUTPUT_PATH . ) endif(${EXECUTABLE_OUTPUT_PATH}) set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}" ) include_directories( ${CMAKE_SOURCE_DIR}/akonadi ${CMAKE_CURRENT_SOURCE_DIR}/../ ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}/../ ${Boost_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR}/akonadi/private ) # add testrunner (application for managing a self-contained test # environment) add_subdirectory(testrunner) # add benchmarker add_subdirectory(benchmarker) # convenience macro to add akonadi demo application macro(add_akonadi_demo _source) set(_test ${_source}) get_filename_component(_name ${_source} NAME_WE) kde4_add_executable(${_name} TEST ${_test}) target_link_libraries(${_name} akonadi-kde akonadi-kmime ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${KDE4_KDEUI_LIBS}) endmacro(add_akonadi_demo) # convenience macro to add akonadi qtestlib unit-tests macro(add_akonadi_test _source) set(_test ${_source}) get_filename_component(_name ${_source} NAME_WE) kde4_add_unit_test(${_name} TESTNAME akonadi-${_name} ${_test} fakesession.cpp fakemonitor.cpp fakeserver.cpp) target_link_libraries(${_name} akonadi-kde akonadi-kmime ${QT_QTTEST_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTNETWORK_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${AKONADI_COMMON_LIBRARIES}) endmacro(add_akonadi_test) # convenience macro to add akonadi testrunner unit-tests macro(add_akonadi_isolated_test _source) set(_test ${_source}) get_filename_component(_name ${_source} NAME_WE) kde4_add_executable(${_name} TEST ${_test}) target_link_libraries(${_name} akonadi-kde akonadi-kmime ${QT_QTTEST_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${AKONADI_COMMON_LIBRARIES}) # based on kde4_add_unit_test if (WIN32) get_target_property( _loc ${_name} LOCATION ) set(_executable ${_loc}.bat) set(_testrunner ${PREVIOUS_EXEC_OUTPUT_PATH}/akonaditest.bat) else (WIN32) set(_executable ${EXECUTABLE_OUTPUT_PATH}/${_name}) set(_testrunner ${PREVIOUS_EXEC_OUTPUT_PATH}/akonaditest) endif (WIN32) if (UNIX) set(_executable ${_executable}.shell) set(_testrunner ${_testrunner}.shell) endif (UNIX) add_test( akonadi-mysql-db-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-mysql-db.xml ${_executable} ) add_test( akonadi-mysql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-mysql-fs.xml ${_executable} ) #add_test( akonadi-postgresql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-postgresql-fs.xml ${_executable} ) #add_test( akonadi-postgresql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-postgresql-fs.xml ${_executable} ) #add_test( akonadi-sqlite-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-sqlite.xml ${_executable} ) endmacro(add_akonadi_isolated_test) # demo applications add_akonadi_demo(itemdumper.cpp) add_akonadi_demo(subscriber.cpp) add_akonadi_demo(headfetcher.cpp) add_akonadi_demo(agentinstancewidgettest.cpp) add_akonadi_demo(agenttypewidgettest.cpp) add_akonadi_demo(pluginloadertest.cpp) add_akonadi_demo(selftester.cpp) kde4_add_executable( akonadi-firstrun TEST ../firstrun.cpp firstrunner.cpp ) target_link_libraries( akonadi-firstrun akonadi-kde ${KDE4_KDEUI_LIBS} ) # qtestlib unit tests add_akonadi_test(imapparsertest.cpp) add_akonadi_test(imapsettest.cpp) add_akonadi_test(itemhydratest.cpp) add_akonadi_test(itemtest.cpp) add_akonadi_test(itemserializertest.cpp) add_akonadi_test(mimetypecheckertest.cpp) add_akonadi_test(protocolhelpertest.cpp) add_akonadi_test(entitytreemodeltest.cpp) +add_akonadi_test(collectionutilstest.cpp) # qtestlib tests that need non-exported stuff from akonadi-kde kde4_add_unit_test(resourceschedulertest TESTNAME akonadi-resourceschedulertest resourceschedulertest.cpp ../resourcescheduler.cpp) target_link_libraries(resourceschedulertest akonadi-kde ${QT_QTTEST_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${AKONADI_COMMON_LIBRARIES}) # testrunner tests add_akonadi_isolated_test(testenvironmenttest.cpp) add_akonadi_isolated_test(autoincrementtest.cpp) add_akonadi_isolated_test(attributefactorytest.cpp) add_akonadi_isolated_test(collectionjobtest.cpp) add_akonadi_isolated_test(collectionpathresolvertest.cpp) add_akonadi_isolated_test(collectionattributetest.cpp) add_akonadi_isolated_test(itemfetchtest.cpp) add_akonadi_isolated_test(itemappendtest.cpp) add_akonadi_isolated_test(itemstoretest.cpp) add_akonadi_isolated_test(itemdeletetest.cpp) add_akonadi_isolated_test(entitycachetest.cpp) add_akonadi_isolated_test(monitortest.cpp) add_akonadi_isolated_test(searchjobtest.cpp) add_akonadi_isolated_test(changerecordertest.cpp) add_akonadi_isolated_test(resourcetest.cpp) add_akonadi_isolated_test(subscriptiontest.cpp) add_akonadi_isolated_test(transactiontest.cpp) add_akonadi_isolated_test(filteractiontest.cpp) add_akonadi_isolated_test(itemcopytest.cpp) add_akonadi_isolated_test(itemmovetest.cpp) add_akonadi_isolated_test(collectioncopytest.cpp) add_akonadi_isolated_test(collectionmovetest.cpp) add_akonadi_isolated_test(collectionsynctest.cpp) add_akonadi_isolated_test(itemsynctest.cpp) add_akonadi_isolated_test(linktest.cpp) add_akonadi_isolated_test(cachetest.cpp) add_akonadi_isolated_test(servermanagertest.cpp) add_akonadi_isolated_test(collectioncreator.cpp) add_akonadi_isolated_test(itembenchmark.cpp) diff --git a/akonadi/tests/collectionjobtest.cpp b/akonadi/tests/collectionjobtest.cpp index 4ec0d05d9..acf0bfdb6 100644 --- a/akonadi/tests/collectionjobtest.cpp +++ b/akonadi/tests/collectionjobtest.cpp @@ -1,643 +1,654 @@ /* Copyright (c) 2006 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 #include "collectionjobtest.h" #include #include "test_utils.h" #include "testattribute.h" #include "agentmanager.h" #include "agentinstance.h" #include "attributefactory.h" #include "cachepolicy.h" #include "collection.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" #include "collectionselectjob_p.h" #include "collectionstatistics.h" #include "collectionstatisticsjob.h" #include "collectionpathresolver_p.h" #include "collectionutils_p.h" #include "control.h" #include "item.h" #include "kmime/messageparts.h" #include "resourceselectjob_p.h" #include "collectionfetchscope.h" #include #include using namespace Akonadi; QTEST_AKONADIMAIN( CollectionJobTest, NoGUI ) void CollectionJobTest::initTestCase() { qRegisterMetaType(); AttributeFactory::registerAttribute(); Control::start(); // switch all resources offline to reduce interference from them foreach ( Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances() ) agent.setIsOnline( false ); } static Collection findCol( const Collection::List &list, const QString &name ) { foreach ( const Collection &col, list ) if ( col.name() == name ) return col; return Collection(); } // list compare which ignores the order template static void compareLists( const QList &l1, const QList &l2 ) { QCOMPARE( l1.count(), l2.count() ); foreach ( const T entry, l1 ) { QVERIFY( l2.contains( entry ) ); } } template static T* extractAttribute( QList attrs ) { T dummy; foreach ( Attribute* attr, attrs ) { if ( attr->type() == dummy.type() ) return dynamic_cast( attr ); } return 0; } static Collection::Id res1ColId = 6; // -1; static Collection::Id res2ColId = 7; //-1; static Collection::Id res3ColId = -1; static Collection::Id searchColId = -1; void CollectionJobTest::testTopLevelList( ) { // non-recursive top-level list CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel ); QVERIFY( job->exec() ); Collection::List list = job->collections(); // check if everything is there and has the correct types and attributes QCOMPARE( list.count(), 4 ); Collection col; col = findCol( list, "res1" ); QVERIFY( col.isValid() ); res1ColId = col.id(); // for the next test QVERIFY( res1ColId > 0 ); QVERIFY( CollectionUtils::isResource( col ) ); QCOMPARE( col.parentCollection(), Collection::root() ); QCOMPARE( col.resource(), QLatin1String("akonadi_knut_resource_0") ); QVERIFY( findCol( list, "res2" ).isValid() ); res2ColId = findCol( list, "res2" ).id(); QVERIFY( res2ColId > 0 ); QVERIFY( findCol( list, "res3" ).isValid() ); res3ColId = findCol( list, "res3" ).id(); QVERIFY( res3ColId > 0 ); col = findCol( list, "Search" ); searchColId = col.id(); QVERIFY( col.isValid() ); QVERIFY( CollectionUtils::isVirtualParent( col ) ); QCOMPARE( col.resource(), QLatin1String("akonadi_search_resource") ); } void CollectionJobTest::testFolderList( ) { // recursive list of physical folders CollectionFetchJob *job = new CollectionFetchJob( Collection( res1ColId ), CollectionFetchJob::Recursive ); QSignalSpy spy( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)) ); QVERIFY( spy.isValid() ); QVERIFY( job->exec() ); Collection::List list = job->collections(); int count = 0; for ( int i = 0; i < spy.count(); ++i ) { Collection::List l = spy[i][0].value(); for ( int j = 0; j < l.count(); ++j ) { QVERIFY( list.count() > count + j ); QCOMPARE( list[count + j].id(), l[j].id() ); } count += l.count(); } QCOMPARE( count, list.count() ); // check if everything is there QCOMPARE( list.count(), 4 ); Collection col; QStringList contentTypes; col = findCol( list, "foo" ); QVERIFY( col.isValid() ); QCOMPARE( col.parentCollection().id(), res1ColId ); QVERIFY( CollectionUtils::isFolder( col ) ); contentTypes << "message/rfc822" << "text/calendar" << "text/directory" << "application/octet-stream" << "inode/directory"; compareLists( col.contentMimeTypes(), contentTypes ); QVERIFY( findCol( list, "bar" ).isValid() ); QCOMPARE( findCol( list, "bar" ).parentCollection(), col ); QVERIFY( findCol( list, "bla" ).isValid() ); } void CollectionJobTest::testNonRecursiveFolderList( ) { CollectionFetchJob *job = new CollectionFetchJob( Collection( res1ColId ), CollectionFetchJob::Base ); QVERIFY( job->exec() ); Collection::List list = job->collections(); QCOMPARE( list.count(), 1 ); QVERIFY( findCol( list, "res1" ).isValid() ); } void CollectionJobTest::testEmptyFolderList( ) { CollectionFetchJob *job = new CollectionFetchJob( Collection( res3ColId ), CollectionFetchJob::FirstLevel ); QVERIFY( job->exec() ); Collection::List list = job->collections(); QCOMPARE( list.count(), 0 ); } void CollectionJobTest::testSearchFolderList( ) { CollectionFetchJob *job = new CollectionFetchJob( Collection( searchColId ), CollectionFetchJob::FirstLevel ); QVERIFY( job->exec() ); Collection::List list = job->collections(); QCOMPARE( list.count(), 0 ); } void CollectionJobTest::testResourceFolderList() { // non-existing resource CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel ); job->fetchScope().setResource( "i_dont_exist" ); QVERIFY( !job->exec() ); // recursive listing of all collections of an existing resource job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); job->fetchScope().setResource( "akonadi_knut_resource_0" ); QVERIFY( job->exec() ); Collection::List list = job->collections(); QCOMPARE( list.count(), 5 ); QVERIFY( findCol( list, "res1" ).isValid() ); QVERIFY( findCol( list, "foo" ).isValid() ); QVERIFY( findCol( list, "bar" ).isValid() ); QVERIFY( findCol( list, "bla" ).isValid() ); int fooId = findCol( list, "foo" ).id(); // limited listing of a resource job = new CollectionFetchJob( Collection( fooId ), CollectionFetchJob::Recursive ); job->fetchScope().setResource( "akonadi_knut_resource_0" ); QVERIFY( job->exec() ); list = job->collections(); QCOMPARE( list.count(), 3 ); QVERIFY( findCol( list, "bar" ).isValid() ); QVERIFY( findCol( list, "bla" ).isValid() ); } void CollectionJobTest::testMimeTypeFilter() { CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); job->fetchScope().setContentMimeTypes( QStringList() << "message/rfc822" ); AKVERIFYEXEC( job ); Collection::List list = job->collections(); QCOMPARE( list.count(), 2 ); QVERIFY( findCol( list, "res1" ).isValid() ); QVERIFY( findCol( list, "foo" ).isValid() ); int fooId = findCol( list, "foo" ).id(); // limited listing of a resource job = new CollectionFetchJob( Collection( fooId ), CollectionFetchJob::Recursive ); job->fetchScope().setContentMimeTypes( QStringList() << "message/rfc822" ); AKVERIFYEXEC( job ); list = job->collections(); QCOMPARE( list.count(), 0 ); } void CollectionJobTest::testCreateDeleteFolder_data() { QTest::addColumn("collection"); QTest::addColumn("creatable"); Collection col; QTest::newRow("empty") << col << false; col.setName( "new folder" ); col.parentCollection().setId( res3ColId ); QTest::newRow("simple") << col << true; col.parentCollection().setId( res3ColId ); col.setName( "foo" ); QTest::newRow( "existing in different resource" ) << col << true; col.setName( "mail folder" ); QStringList mimeTypes; mimeTypes << "inode/directory" << "message/rfc822"; col.setContentMimeTypes( mimeTypes ); col.setRemoteId( "remote id" ); CachePolicy policy; policy.setInheritFromParent( false ); policy.setIntervalCheckTime( 60 ); policy.setLocalParts( QStringList( MessagePart::Envelope ) ); policy.setSyncOnDemand( true ); policy.setCacheTimeout( 120 ); col.setCachePolicy( policy ); QTest::newRow( "complex" ) << col << true; col = Collection(); col.setName( "New Folder" ); col.parentCollection().setId( searchColId ); QTest::newRow( "search folder" ) << col << false; col.parentCollection().setId( res2ColId ); col.setName( "foo2" ); QTest::newRow( "already existing" ) << col << false; col.setName( "Bla" ); col.parentCollection().setId( 2 ); QTest::newRow( "already existing with different case" ) << col << true; CollectionPathResolver *resolver = new CollectionPathResolver( "res2/foo2", this ); QVERIFY( resolver->exec() ); col.parentCollection().setId( resolver->collection() ); col.setName( "new folder" ); QTest::newRow( "parent noinferior" ) << col << false; col.parentCollection().setId( INT_MAX ); QTest::newRow( "missing parent" ) << col << false; col = Collection(); col.setName( "rid parent" ); col.parentCollection().setRemoteId( "8" ); QTest::newRow( "rid parent" ) << col << false; // missing resource context } void CollectionJobTest::testCreateDeleteFolder() { QFETCH( Collection, collection ); QFETCH( bool, creatable ); CollectionCreateJob *createJob = new CollectionCreateJob( collection, this ); QCOMPARE( createJob->exec(), creatable ); if ( !creatable ) return; Collection createdCol = createJob->collection(); QVERIFY( createdCol.isValid() ); QCOMPARE( createdCol.name(), collection.name() ); QCOMPARE( createdCol.parentCollection(), collection.parentCollection() ); QCOMPARE( createdCol.remoteId(), collection.remoteId() ); QCOMPARE( createdCol.cachePolicy(), collection.cachePolicy() ); CollectionFetchJob *listJob = new CollectionFetchJob( collection.parentCollection(), CollectionFetchJob::FirstLevel, this ); AKVERIFYEXEC( listJob ); Collection listedCol = findCol( listJob->collections(), collection.name() ); QCOMPARE( listedCol, createdCol ); QCOMPARE( listedCol.remoteId(), collection.remoteId() ); QCOMPARE( listedCol.cachePolicy(), collection.cachePolicy() ); // fetch parent to compare inherited collection properties Collection parentCol = Collection::root(); if ( collection.parentCollection().isValid() ) { CollectionFetchJob *listJob = new CollectionFetchJob( collection.parentCollection(), CollectionFetchJob::Base, this ); AKVERIFYEXEC( listJob ); QCOMPARE( listJob->collections().count(), 1 ); parentCol = listJob->collections().first(); } if ( collection.contentMimeTypes().isEmpty() ) compareLists( listedCol.contentMimeTypes(), parentCol.contentMimeTypes() ); else compareLists( listedCol.contentMimeTypes(), collection.contentMimeTypes() ); if ( collection.resource().isEmpty() ) QCOMPARE( listedCol.resource(), parentCol.resource() ); else QCOMPARE( listedCol.resource(), collection.resource() ); CollectionDeleteJob *delJob = new CollectionDeleteJob( createdCol, this ); AKVERIFYEXEC( delJob ); listJob = new CollectionFetchJob( collection.parentCollection(), CollectionFetchJob::FirstLevel, this ); AKVERIFYEXEC( listJob ); QVERIFY( !findCol( listJob->collections(), collection.name() ).isValid() ); } void CollectionJobTest::testIllegalDeleteFolder() { // non-existing folder CollectionDeleteJob *del = new CollectionDeleteJob( Collection( INT_MAX ), this ); QVERIFY( !del->exec() ); // root del = new CollectionDeleteJob( Collection::root(), this ); QVERIFY( !del->exec() ); } void CollectionJobTest::testStatistics() { // empty folder CollectionStatisticsJob *statistics = new CollectionStatisticsJob( Collection( res1ColId ), this ); QVERIFY( statistics->exec() ); CollectionStatistics s = statistics->statistics(); QCOMPARE( s.count(), 0ll ); QCOMPARE( s.unreadCount(), 0ll ); // folder with attributes and content CollectionPathResolver *resolver = new CollectionPathResolver( "res1/foo", this );; QVERIFY( resolver->exec() ); statistics = new CollectionStatisticsJob( Collection( resolver->collection() ), this ); QVERIFY( statistics->exec() ); s = statistics->statistics(); QCOMPARE( s.count(), 15ll ); QCOMPARE( s.unreadCount(), 14ll ); } void CollectionJobTest::testModify_data() { QTest::addColumn( "uid" ); QTest::addColumn( "rid" ); QTest::newRow( "uid" ) << collectionIdFromPath( "res1/foo" ) << QString(); QTest::newRow( "rid" ) << -1ll << QString( "10" ); } #define RESET_COLLECTION_ID \ col.setId( uid ); \ if ( !rid.isEmpty() ) col.setRemoteId( rid ) void CollectionJobTest::testModify() { QFETCH( qint64, uid ); QFETCH( QString, rid ); if ( !rid.isEmpty() ) { ResourceSelectJob *rjob = new ResourceSelectJob( "akonadi_knut_resource_0" ); AKVERIFYEXEC( rjob ); } QStringList reference; reference << "text/calendar" << "text/directory" << "message/rfc822" << "application/octet-stream" << "inode/directory"; Collection col; RESET_COLLECTION_ID; // test noop modify CollectionModifyJob *mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); CollectionFetchJob* ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QCOMPARE( ljob->collections().count(), 1 ); col = ljob->collections().first(); compareLists( col.contentMimeTypes(), reference ); // test clearing content types RESET_COLLECTION_ID; col.setContentMimeTypes( QStringList() ); mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QCOMPARE( ljob->collections().count(), 1 ); col = ljob->collections().first(); QVERIFY( col.contentMimeTypes().isEmpty() ); // test setting contnet types RESET_COLLECTION_ID; col.setContentMimeTypes( reference ); mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QCOMPARE( ljob->collections().count(), 1 ); col = ljob->collections().first(); compareLists( col.contentMimeTypes(), reference ); // add attribute RESET_COLLECTION_ID; col.attribute( Collection::AddIfMissing )->data = "new"; mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QVERIFY( ljob->collections().first().hasAttribute() ); QCOMPARE( ljob->collections().first().attribute()->data, QByteArray( "new" ) ); // modify existing attribute RESET_COLLECTION_ID; col.attribute()->data = "modified"; mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QVERIFY( ljob->collections().first().hasAttribute() ); QCOMPARE( ljob->collections().first().attribute()->data, QByteArray( "modified" ) ); // renaming RESET_COLLECTION_ID; col.setName( "foo (renamed)" ); mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QCOMPARE( ljob->collections().count(), 1 ); col = ljob->collections().first(); QCOMPARE( col.name(), QString( "foo (renamed)" ) ); RESET_COLLECTION_ID; col.setName( "foo" ); mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); } #undef RESET_COLLECTION_ID void CollectionJobTest::testIllegalModify() { // non-existing collection Collection col( INT_MAX ); col.parentCollection().setId( res1ColId ); CollectionModifyJob *mod = new CollectionModifyJob( col, this ); QVERIFY( !mod->exec() ); // rename to already existing name col = Collection( res1ColId ); col.setName( "res2" ); mod = new CollectionModifyJob( col, this ); QVERIFY( !mod->exec() ); } void CollectionJobTest::testUtf8CollectionName() { QString folderName = QString::fromUtf8( "รค" ); // create collection Collection col; col.parentCollection().setId( res3ColId ); col.setName( folderName ); CollectionCreateJob *create = new CollectionCreateJob( col, this ); QVERIFY( create->exec() ); col = create->collection(); QVERIFY( col.isValid() ); // list parent CollectionFetchJob *list = new CollectionFetchJob( Collection( res3ColId ), CollectionFetchJob::Recursive, this ); QVERIFY( list->exec() ); QCOMPARE( list->collections().count(), 1 ); QCOMPARE( col, list->collections().first() ); QCOMPARE( col.name(), folderName ); // modify collection col.setContentMimeTypes( QStringList( "message/rfc822'" ) ); CollectionModifyJob *modify = new CollectionModifyJob( col, this ); QVERIFY( modify->exec() ); // collection statistics CollectionStatisticsJob *statistics = new CollectionStatisticsJob( col, this ); QVERIFY( statistics->exec() ); CollectionStatistics s = statistics->statistics(); QCOMPARE( s.count(), 0ll ); QCOMPARE( s.unreadCount(), 0ll ); // delete collection CollectionDeleteJob *del = new CollectionDeleteJob( col, this ); QVERIFY( del->exec() ); } void CollectionJobTest::testMultiList() { Collection::List req; req << Collection( res1ColId ) << Collection( res2ColId ); CollectionFetchJob* job = new CollectionFetchJob( req, this ); QVERIFY( job->exec() ); Collection::List res; res = job->collections(); compareLists( res, req ); } void CollectionJobTest::testSelect() { CollectionPathResolver *resolver = new CollectionPathResolver( "res1/foo", this );; QVERIFY( resolver->exec() ); Collection col( resolver->collection() ); CollectionSelectJob *job = new CollectionSelectJob( col, this ); QVERIFY( job->exec() ); QCOMPARE( job->unseen(), -1 ); job = new CollectionSelectJob( col, this ); job->setRetrieveStatus( true ); QVERIFY( job->exec() ); QVERIFY( job->unseen() > -1 ); job = new CollectionSelectJob( Collection::root(), this ); QVERIFY( job->exec() ); job = new CollectionSelectJob( Collection( INT_MAX ), this ); QVERIFY( !job->exec() ); } void CollectionJobTest::testRidFetch() { Collection col; col.setRemoteId( "10" ); CollectionFetchJob *job = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); job->fetchScope().setResource( "akonadi_knut_resource_0" ); QVERIFY( job->exec() ); QCOMPARE( job->collections().count(), 1 ); col = job->collections().first(); QVERIFY( col.isValid() ); QCOMPARE( col.remoteId(), QString::fromLatin1( "10" ) ); } void CollectionJobTest::testRidCreateDelete() { Collection collection; collection.setName( "rid create" ); collection.parentCollection().setRemoteId( "8" ); collection.setRemoteId( "MY REMOTE ID" ); ResourceSelectJob *resSel = new ResourceSelectJob( "akonadi_knut_resource_2" ); AKVERIFYEXEC( resSel ); CollectionCreateJob *createJob = new CollectionCreateJob( collection, this ); AKVERIFYEXEC( createJob ); Collection createdCol = createJob->collection(); QVERIFY( createdCol.isValid() ); QCOMPARE( createdCol.name(), collection.name() ); CollectionFetchJob *listJob = new CollectionFetchJob( Collection( res3ColId ), CollectionFetchJob::FirstLevel, this ); AKVERIFYEXEC( listJob ); Collection listedCol = findCol( listJob->collections(), collection.name() ); QCOMPARE( listedCol, createdCol ); QCOMPARE( listedCol.name(), collection.name() ); QVERIFY( !collection.isValid() ); CollectionDeleteJob *delJob = new CollectionDeleteJob( collection, this ); AKVERIFYEXEC( delJob ); listJob = new CollectionFetchJob( Collection( res3ColId ), CollectionFetchJob::FirstLevel, this ); AKVERIFYEXEC( listJob ); QVERIFY( !findCol( listJob->collections(), collection.name() ).isValid() ); } void CollectionJobTest::testAncestorRetrieval() { Collection col; col.setRemoteId( "10" ); CollectionFetchJob *job = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); job->fetchScope().setResource( "akonadi_knut_resource_0" ); job->fetchScope().setAncestorRetrieval( CollectionFetchScope::All ); AKVERIFYEXEC( job ); QCOMPARE( job->collections().count(), 1 ); col = job->collections().first(); QVERIFY( col.isValid() ); QVERIFY( col.parentCollection().isValid() ); QCOMPARE( col.parentCollection().remoteId(), QString( "6" ) ); QCOMPARE( col.parentCollection().parentCollection(), Collection::root() ); + + ResourceSelectJob* select = new ResourceSelectJob( "akonadi_knut_resource_0", this ); + AKVERIFYEXEC( select ); + Collection col2( col ); + col2.setId( -1 ); // make it invalid but keep the ancestor chain + job = new CollectionFetchJob( col2, CollectionFetchJob::Base, this ); + AKVERIFYEXEC( job ); + QCOMPARE( job->collections().count(), 1 ); + col2 = job->collections().first(); + QVERIFY( col2.isValid() ); + QCOMPARE( col, col2 ); } #include "collectionjobtest.moc" diff --git a/akonadi/tests/collectionutilstest.cpp b/akonadi/tests/collectionutilstest.cpp new file mode 100644 index 000000000..75ce75c7f --- /dev/null +++ b/akonadi/tests/collectionutilstest.cpp @@ -0,0 +1,63 @@ +/* + 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 "entitycache.cpp" + +#include +#include +#include "../collectionutils_p.h" + +using namespace Akonadi; + +class CollectionUtilsTest : public QObject +{ + Q_OBJECT + private slots: + void testHasValidHierarchicalRID_data() + { + QTest::addColumn( "collection" ); + QTest::addColumn( "isHRID" ); + + QTest::newRow( "empty" ) << Collection() << false; + QTest::newRow( "root" ) << Collection::root() << true; + Collection c; + c.setParentCollection( Collection::root() ); + QTest::newRow( "one level not ok" ) << c << false; + c.setRemoteId( "r1" ); + QTest::newRow( "one level ok" ) << c << true; + Collection c2; + c2.setParentCollection( c ); + QTest::newRow( "two level not ok" ) << c2 << false; + c2.setRemoteId( "r2" ); + QTest::newRow( "two level ok" ) << c2 << true; + c2.parentCollection().setRemoteId( QString() ); + QTest::newRow( "mid RID missing" ) << c2 << false; + } + + void testHasValidHierarchicalRID() + { + QFETCH( Collection, collection ); + QFETCH( bool, isHRID ); + QCOMPARE( CollectionUtils::hasValidHierarchicalRID( collection ), isHRID ); + } +}; + +QTEST_AKONADIMAIN( CollectionUtilsTest, NoGUI ) + +#include "collectionutilstest.moc" diff --git a/akonadi/tests/protocolhelpertest.cpp b/akonadi/tests/protocolhelpertest.cpp index b595b0e2c..85488f444 100644 --- a/akonadi/tests/protocolhelpertest.cpp +++ b/akonadi/tests/protocolhelpertest.cpp @@ -1,116 +1,141 @@ /* 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" ); const QByteArray b1 = "2 1 (REMOTEID \"r2\" NAME \"n2\")"; Collection c1; c1.setId( 2 ); c1.setRemoteId( "r2" ); c1.parentCollection().setId( 1 ); c1.setName( "n2" ); QTest::newRow( "no ancestors" ) << b1 << c1; const QByteArray b2 = "3 2 (REMOTEID \"r3\" ANCESTORS ((2 \"r2\") (1 \"r1\") (0 \"\")))"; Collection c2; c2.setId( 3 ); c2.setRemoteId( "r3" ); c2.parentCollection().setId( 2 ); c2.parentCollection().setRemoteId( "r2" ); c2.parentCollection().parentCollection().setId( 1 ); c2.parentCollection().parentCollection().setRemoteId( "r1" ); c2.parentCollection().parentCollection().setParentCollection( Collection::root() ); QTest::newRow( "ancestors" ) << b2 << c2; } void testCollectionParsing() { QFETCH( QByteArray, input ); QFETCH( Collection, collection ); Collection parsedCollection; ProtocolHelper::parseCollection( input, parsedCollection ); QCOMPARE( parsedCollection.name(), collection.name() ); 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; } } + + void testHRidToByteArray_data() + { + QTest::addColumn( "collection" ); + QTest::addColumn( "result" ); + + QTest::newRow( "empty" ) << Collection() << QByteArray(); + QTest::newRow( "root" ) << Collection::root() << QByteArray( "(0 \"\")" ); + Collection c; + c.setParentCollection( Collection::root() ); + c.setRemoteId( "r1" ); + QTest::newRow( "one level" ) << c << QByteArray( "(-14 \"r1\") (0 \"\")" ); + Collection c2; + c2.setParentCollection( c ); + c2.setRemoteId( "r2" ); + QTest::newRow( "two level ok" ) << c2 << QByteArray( "(-15 \"r2\") (-14 \"r1\") (0 \"\")" ); + } + + void testHRidToByteArray() + { + QFETCH( Collection, collection ); + QFETCH( QByteArray, result ); + qDebug() << ProtocolHelper::hierarchicalRidToByteArray( collection ) << result; + QCOMPARE( ProtocolHelper::hierarchicalRidToByteArray( collection ), result ); + } }; QTEST_KDEMAIN( ProtocolHelperTest, NoGUI ) #include "protocolhelpertest.moc" diff --git a/kpimutils/linklocator.cpp b/kpimutils/linklocator.cpp index a98057597..18c60653f 100644 --- a/kpimutils/linklocator.cpp +++ b/kpimutils/linklocator.cpp @@ -1,452 +1,477 @@ /* Copyright (c) 2002 Dave Corrie 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. */ /** @file This file is part of the KDEPIM Utilities library and provides the LinkLocator class. @brief Identifies URLs and email addresses embedded in plaintext. @author Dave Corrie \ */ #include "linklocator.h" #include #include #include #include #include #if KDE_IS_VERSION( 4, 0, 95 ) #include #endif #include #include #include #include #include using namespace KPIMUtils; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KPIMUtils::LinkLocator::Private { public: int mMaxUrlLen; int mMaxAddressLen; }; //@endcond #if KDE_IS_VERSION( 4, 0, 95 ) // Use a static for this as calls to the KEmoticons constructor are expensive. K_GLOBAL_STATIC( KEmoticons, sEmoticons ) #endif LinkLocator::LinkLocator( const QString &text, int pos ) : mText( text ), mPos( pos ), d( new KPIMUtils::LinkLocator::Private ) { d->mMaxUrlLen = 4096; d->mMaxAddressLen = 255; // If you change either of the above values for maxUrlLen or // maxAddressLen, then please also update the documentation for // setMaxUrlLen()/setMaxAddressLen() in the header file AND the // default values used for the maxUrlLen/maxAddressLen parameters // of convertToHtml(). } LinkLocator::~LinkLocator() { delete d; } void LinkLocator::setMaxUrlLen( int length ) { d->mMaxUrlLen = length; } int LinkLocator::maxUrlLen() const { return d->mMaxUrlLen; } void LinkLocator::setMaxAddressLen( int length ) { d->mMaxAddressLen = length; } int LinkLocator::maxAddressLen() const { return d->mMaxAddressLen; } QString LinkLocator::getUrl() { QString url; if ( atUrl() ) { - // for reference: rfc1738: - // Thus, only alphanumerics, the special characters "$-_.+!*'(),", and - // reserved characters used for their reserved purposes may be used - // unencoded within a URL. - // NOTE: this implementation is not RFC conforming - int start = mPos; - while ( mPos < (int)mText.length() && - mText[mPos] > ' ' && mText[mPos] != '"' && - QString( "<>[]" ).indexOf( mText[mPos] ) == -1 ) { - ++mPos; + // NOTE: see http://tools.ietf.org/html/rfc3986#appendix-A and especially appendix-C + // Appendix-C mainly says, that when extracting URLs from plain text, line breaks shall + // be allowed and should be ignored when the URI is extracted. + + // This implementation follows this recommendation and + // allows the URL to be enclosed within different kind of brackets/quotes + // If an URL is enclosed, whitespace characters are allowed and removed, otherwise + // the URL ends with the first whitespace + // Also, if the URL is enclosed in brackets, the URL itself is not allowed + // to contain the closing bracket, as this would be detected as the end of the URL + + QChar beforeUrl, afterUrl; + + // detect if the url has been surrounded by brackets or quotes + if ( mPos > 0 ) { + beforeUrl = mText[mPos - 1]; + + if ( beforeUrl == '(' ) + afterUrl = ')'; + else if ( beforeUrl == '[' ) + afterUrl = ']'; + else if ( beforeUrl == '<' ) + afterUrl = '>'; + else if ( beforeUrl == '>' ) // for e.g. http://..... + afterUrl = '<'; + else if ( beforeUrl == '"' ) + afterUrl = '"'; } - // some URLs really end with: # / & - _ - const QString allowedSpecialChars = QString( "#/&-_" ); - while ( mPos > start && mText[mPos-1].isPunct() && - allowedSpecialChars.indexOf( mText[mPos-1] ) == -1 ) { - --mPos; + url.reserve( maxUrlLen() ); // avoid allocs + int start = mPos; + while ( ( mPos < (int)mText.length() ) && + ( mText[mPos].isPrint() || mText[mPos].isSpace() ) && + ( ( afterUrl.isNull() && !mText[mPos].isSpace() ) || + ( !afterUrl.isNull() && mText[mPos] != afterUrl ) ) + ) { + if ( !mText[mPos].isSpace() ) { // skip whitespace + url.append( mText[mPos] ); + if ( url.length() > maxUrlLen() ) + break; + } + + mPos++; } - url = mText.mid( start, mPos - start ); - if ( isEmptyUrl(url) || mPos - start > maxUrlLen() ) { + if ( isEmptyUrl(url) || ( url.length() > maxUrlLen() ) ) { mPos = start; url = ""; } else { --mPos; } } return url; } // keep this in sync with KMMainWin::slotUrlClicked() bool LinkLocator::atUrl() const { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ const QString allowedSpecialChars = QString( ".!#$%&'*+-/=?^_`{|}~" ); // the character directly before the URL must not be a letter, a number or // any other character allowed in a dot-atom (RFC 2822). if ( ( mPos > 0 ) && ( mText[mPos-1].isLetterOrNumber() || ( allowedSpecialChars.indexOf( mText[mPos-1] ) != -1 ) ) ) { return false; } QChar ch = mText[mPos]; return ( ch == 'h' && ( mText.mid( mPos, 7 ) == "http://" || mText.mid( mPos, 8 ) == "https://" ) ) || ( ch == 'v' && mText.mid( mPos, 6 ) == "vnc://" ) || ( ch == 'f' && ( mText.mid( mPos, 7 ) == "fish://" || mText.mid( mPos, 6 ) == "ftp://" || mText.mid( mPos, 7 ) == "ftps://" ) ) || ( ch == 's' && ( mText.mid( mPos, 7 ) == "sftp://" || mText.mid( mPos, 6 ) == "smb://" ) ) || ( ch == 'm' && mText.mid( mPos, 7 ) == "mailto:" ) || ( ch == 'w' && mText.mid( mPos, 4 ) == "www." ) || ( ch == 'f' && ( mText.mid( mPos, 4 ) == "ftp." || mText.mid( mPos, 7 ) == "file://" ) ) || ( ch == 'n' && mText.mid( mPos, 5 ) == "news:" ); } bool LinkLocator::isEmptyUrl( const QString &url ) const { return url.isEmpty() || url == "http://" || url == "https://" || url == "fish://" || url == "ftp://" || url == "ftps://" || url == "sftp://" || url == "smb://" || url == "vnc://" || url == "mailto" || url == "www" || url == "ftp" || url == "news" || url == "news://"; } QString LinkLocator::getEmailAddress() { QString address; if ( mText[mPos] == '@' ) { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ const QString allowedSpecialChars = QString( ".!#$%&'*+-/=?^_`{|}~" ); // determine the local part of the email address int start = mPos - 1; while ( start >= 0 && mText[start].unicode() < 128 && ( mText[start].isLetterOrNumber() || mText[start] == '@' || // allow @ to find invalid email addresses allowedSpecialChars.indexOf( mText[start] ) != -1 ) ) { if ( mText[start] == '@' ) { return QString(); // local part contains '@' -> no email address } --start; } ++start; // we assume that an email address starts with a letter or a digit while ( ( start < mPos ) && !mText[start].isLetterOrNumber() ) { ++start; } if ( start == mPos ) { return QString(); // local part is empty -> no email address } // determine the domain part of the email address int dotPos = INT_MAX; int end = mPos + 1; while ( end < (int)mText.length() && ( mText[end].isLetterOrNumber() || mText[end] == '@' || // allow @ to find invalid email addresses mText[end] == '.' || mText[end] == '-' ) ) { if ( mText[end] == '@' ) { return QString(); // domain part contains '@' -> no email address } if ( mText[end] == '.' ) { dotPos = qMin( dotPos, end ); // remember index of first dot in domain } ++end; } // we assume that an email address ends with a letter or a digit while ( ( end > mPos ) && !mText[end - 1].isLetterOrNumber() ) { --end; } if ( end == mPos ) { return QString(); // domain part is empty -> no email address } if ( dotPos >= end ) { return QString(); // domain part doesn't contain a dot } if ( end - start > maxAddressLen() ) { return QString(); // too long -> most likely no email address } address = mText.mid( start, end - start ); mPos = end - 1; } return address; } QString LinkLocator::convertToHtml( const QString &plainText, int flags, int maxUrlLen, int maxAddressLen ) { LinkLocator locator( plainText ); locator.setMaxUrlLen( maxUrlLen ); locator.setMaxAddressLen( maxAddressLen ); QString str; QString result( (QChar*)0, (int)locator.mText.length() * 2 ); QChar ch; int x; bool startOfLine = true; QString emoticon; for ( locator.mPos = 0, x = 0; locator.mPos < (int)locator.mText.length(); locator.mPos++, x++ ) { ch = locator.mText[locator.mPos]; if ( flags & PreserveSpaces ) { if ( ch == ' ' ) { if ( locator.mPos + 1 < locator.mText.length() ) { if ( locator.mText[locator.mPos + 1] != ' ' ) { // A single space, make it breaking if not at the start or end of the line const bool endOfLine = locator.mText[locator.mPos + 1] == '\n'; if ( !startOfLine && !endOfLine ) result += ' '; else result += " "; } else { // Whitespace of more than one space, make it all non-breaking while( locator.mPos < locator.mText.length() && locator.mText[locator.mPos] == ' ' ) { result += " "; locator.mPos++; x++; } // We incremented once to often, undo that locator.mPos--; x--; } } else { // Last space in the text, it is non-breaking result += " "; } if ( startOfLine ) { startOfLine = false; } continue; } else if ( ch == '\t' ) { do { result += " "; x++; } while ( ( x & 7 ) != 0 ); x--; startOfLine = false; continue; } } if ( ch == '\n' ) { result += "
\n"; // Keep the \n, so apps can figure out the quoting levels correctly. startOfLine = true; x = -1; continue; } startOfLine = false; if ( ch == '&' ) { result += "&"; } else if ( ch == '"' ) { result += """; } else if ( ch == '<' ) { result += "<"; } else if ( ch == '>' ) { result += ">"; } else { const int start = locator.mPos; if ( !( flags & IgnoreUrls ) ) { str = locator.getUrl(); if ( !str.isEmpty() ) { QString hyperlink; if ( str.left( 4 ) == "www." ) { hyperlink = "http://" + str; } else if ( str.left( 4 ) == "ftp." ) { hyperlink = "ftp://" + str; } else { hyperlink = str; } str = str.replace( '&', "&" ); result += "" + str + ""; x += locator.mPos - start; continue; } str = locator.getEmailAddress(); if ( !str.isEmpty() ) { // len is the length of the local part int len = str.indexOf( '@' ); QString localPart = str.left( len ); // remove the local part from the result (as '&'s have been expanded to // & we have to take care of the 4 additional characters per '&') result.truncate( result.length() - len - ( localPart.count( '&' ) * 4 ) ); x -= len; result += "" + str + ""; x += str.length() - 1; continue; } } if ( flags & HighlightText ) { str = locator.highlightedText(); if ( !str.isEmpty() ) { result += str; x += locator.mPos - start; continue; } } result += ch; } } #if KDE_IS_VERSION( 4, 0, 95 ) if ( flags & ReplaceSmileys ) { QStringList exclude; exclude << "(c)" << "(C)" << ">:-(" << ">:(" << "(B)" << "(b)" << "(P)" << "(p)"; exclude << "(O)" << "(o)" << "(D)" << "(d)" << "(E)" << "(e)" << "(K)" << "(k)"; exclude << "(I)" << "(i)" << "(L)" << "(l)" << "(8)" << "(T)" << "(t)" << "(G)"; exclude << "(g)" << "(F)" << "(f)" << "(H)"; exclude << "8)" << "(N)" << "(n)" << "(Y)" << "(y)" << "(U)" << "(u)" << "(W)" << "(w)"; static QString cachedEmoticonsThemeName; if ( cachedEmoticonsThemeName.isEmpty() ) { cachedEmoticonsThemeName = KEmoticons::currentThemeName(); } result = sEmoticons->theme( cachedEmoticonsThemeName ).parseEmoticons( result, KEmoticonsTheme::StrictParse | KEmoticonsTheme::SkipHTML, exclude ); } #endif return result; } QString LinkLocator::pngToDataUrl( const QString &iconPath ) { if ( iconPath.isEmpty() ) { return QString(); } QFile pngFile( iconPath ); if ( !pngFile.open( QIODevice::ReadOnly | QIODevice::Unbuffered ) ) { return QString(); } QByteArray ba = pngFile.readAll(); pngFile.close(); return QString::fromLatin1( "data:image/png;base64,%1" ).arg( ba.toBase64().constData() ); } QString LinkLocator::highlightedText() { // formating symbols must be prepended with a whitespace if ( ( mPos > 0 ) && !mText[mPos-1].isSpace() ) { return QString(); } const QChar ch = mText[mPos]; if ( ch != '/' && ch != '*' && ch != '_' ) { return QString(); } QRegExp re = QRegExp( QString( "\\%1([0-9A-Za-z]+)\\%2" ).arg( ch ).arg( ch ) ); if ( re.indexIn( mText, mPos ) == mPos ) { int length = re.matchedLength(); // there must be a whitespace after the closing formating symbol if ( mPos + length < mText.length() && !mText[mPos + length].isSpace() ) { return QString(); } mPos += length - 1; switch ( ch.toLatin1() ) { case '*': return "" + re.cap( 1 ) + ""; case '_': return "" + re.cap( 1 ) + ""; case '/': return "" + re.cap( 1 ) + ""; } } return QString(); } diff --git a/kpimutils/tests/testlinklocator.cpp b/kpimutils/tests/testlinklocator.cpp index da913a583..cbaec0c33 100644 --- a/kpimutils/tests/testlinklocator.cpp +++ b/kpimutils/tests/testlinklocator.cpp @@ -1,230 +1,296 @@ /* This file is part of the kpimutils library. Copyright (C) 2005 Ingo Kloecker Copyright (C) 2007 Allen Winter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 #include #include "testlinklocator.h" #include "testlinklocator.moc" QTEST_KDEMAIN( LinkLocatorTest, NoGUI ) #include "kpimutils/linklocator.h" using namespace KPIMUtils; void LinkLocatorTest::testGetEmailAddress() { // empty input const QString emptyQString; LinkLocator ll1( emptyQString, 0 ); QVERIFY( ll1.getEmailAddress().isEmpty() ); // no '@' at scan position LinkLocator ll2( "foo@bar.baz", 0 ); QVERIFY( ll2.getEmailAddress().isEmpty() ); // '@' in local part LinkLocator ll3( "foo@bar@bar.baz", 7 ); QVERIFY( ll3.getEmailAddress().isEmpty() ); // empty local part LinkLocator ll4( "@bar.baz", 0 ); QVERIFY( ll4.getEmailAddress().isEmpty() ); LinkLocator ll5( ".@bar.baz", 1 ); QVERIFY( ll5.getEmailAddress().isEmpty() ); LinkLocator ll6( " @bar.baz", 1 ); QVERIFY( ll6.getEmailAddress().isEmpty() ); LinkLocator ll7( ".!#$%&'*+-/=?^_`{|}~@bar.baz", strlen( ".!#$%&'*+-/=?^_`{|}~" ) ); QVERIFY( ll7.getEmailAddress().isEmpty() ); // allowed special chars in local part of address LinkLocator ll8( "a.!#$%&'*+-/=?^_`{|}~@bar.baz", strlen( "a.!#$%&'*+-/=?^_`{|}~" ) ); QVERIFY( ll8.getEmailAddress() == "a.!#$%&'*+-/=?^_`{|}~@bar.baz" ); // '@' in domain part LinkLocator ll9 ( "foo@bar@bar.baz", 3 ); QVERIFY( ll9.getEmailAddress().isEmpty() ); // domain part without dot LinkLocator lla( "foo@bar", 3 ); QVERIFY( lla.getEmailAddress().isEmpty() ); LinkLocator llb( "foo@bar.", 3 ); QVERIFY( llb.getEmailAddress().isEmpty() ); LinkLocator llc( ".foo@bar", 4 ); QVERIFY( llc.getEmailAddress().isEmpty() ); LinkLocator lld( "foo@bar ", 3 ); QVERIFY( lld.getEmailAddress().isEmpty() ); LinkLocator lle( " foo@bar", 4 ); QVERIFY( lle.getEmailAddress().isEmpty() ); LinkLocator llf( "foo@bar-bar", 3 ); QVERIFY( llf.getEmailAddress().isEmpty() ); // empty domain part LinkLocator llg( "foo@", 3 ); QVERIFY( llg.getEmailAddress().isEmpty() ); LinkLocator llh( "foo@.", 3 ); QVERIFY( llh.getEmailAddress().isEmpty() ); LinkLocator lli( "foo@-", 3 ); QVERIFY( lli.getEmailAddress().isEmpty() ); // simple address LinkLocator llj( "foo@bar.baz", 3 ); QVERIFY( llj.getEmailAddress() == "foo@bar.baz" ); LinkLocator llk( "foo@bar.baz.", 3 ); QVERIFY( llk.getEmailAddress() == "foo@bar.baz" ); LinkLocator lll( ".foo@bar.baz", 4 ); QVERIFY( lll.getEmailAddress() == "foo@bar.baz" ); LinkLocator llm( "foo@bar.baz-", 3 ); QVERIFY( llm.getEmailAddress() == "foo@bar.baz" ); LinkLocator lln( "-foo@bar.baz", 4 ); QVERIFY( lln.getEmailAddress() == "foo@bar.baz" ); LinkLocator llo( "foo@bar.baz ", 3 ); QVERIFY( llo.getEmailAddress() == "foo@bar.baz" ); LinkLocator llp( " foo@bar.baz", 4 ); QVERIFY( llp.getEmailAddress() == "foo@bar.baz" ); LinkLocator llq( "foo@bar-bar.baz", 3 ); QVERIFY( llq.getEmailAddress() == "foo@bar-bar.baz" ); } void LinkLocatorTest::testGetUrl() { QStringList brackets; brackets << "" << ""; // no brackets brackets << "(" << ")"; brackets << "<" << ">"; brackets << "[" << "]"; + brackets << "\"" << "\""; brackets << "" << ""; for (int i = 0; i < brackets.count(); i += 2) testGetUrl2(brackets[i], brackets[i+1]); } void LinkLocatorTest::testGetUrl2(const QString &left, const QString &right) { QStringList schemas; schemas << "http://"; schemas << "https://"; schemas << "vnc://"; schemas << "fish://"; schemas << "ftp://"; schemas << "ftps://"; schemas << "sftp://"; schemas << "smb://"; schemas << "file://"; QStringList urls; urls << "www.kde.org"; urls << "user@www.kde.org"; urls << "user:pass@www.kde.org"; urls << "user:pass@www.kde.org:1234"; urls << "user:pass@www.kde.org:1234/sub/path"; urls << "user:pass@www.kde.org:1234/sub/path?a=1"; urls << "user:pass@www.kde.org:1234/sub/path?a=1#anchor"; + urls << "user:pass@www.kde.org:1234/sub/\npath \n /long/ path \t ?a=1#anchor"; urls << "user:pass@www.kde.org:1234/sub/path/special(123)?a=1#anchor"; urls << "user:pass@www.kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor"; + urls << "user:pass@www.kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor[bla"; + urls << "user:pass@www.kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor[bla]"; + urls << "user:pass@www.kde.org:1234/\nsub/path:with:colon/\nspecial(123)?\na=1#anchor[bla]"; + urls << "user:pass@www.kde.org:1234/ \n sub/path:with:colon/ \n\t \t special(123)?\n\t \n\t a=1#anchor[bla]"; foreach (QString schema, schemas) { foreach (QString url, urls) { + // by defintion: if the URL is enclosed in brackets, the URL itself is not allowed + // to contain the closing bracket, as this would be detected as the end of the URL + if ( ( left.length() == 1 ) && ( url.contains( right[0] ) ) ) + continue; + + // if the url contains a whitespace, it must be enclosed with brackets + if ( (url.contains('\n') || url.contains('\t') || url.contains(' ')) && + left.isEmpty() ) + continue; + QString test(left + schema + url + right); LinkLocator ll(test, left.length()); QString gotUrl = ll.getUrl(); + // we want to have the url without whitespace + url.remove(' '); + url.remove('\n'); + url.remove('\t'); + bool ok = ( gotUrl == (schema + url) ); //qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << (schema + url); + if ( !ok ) qDebug() << "got:" << gotUrl; QVERIFY2( ok, qPrintable(test) ); } } QStringList urlsWithoutSchema; urlsWithoutSchema << ".kde.org"; urlsWithoutSchema << ".kde.org:1234/sub/path"; urlsWithoutSchema << ".kde.org:1234/sub/path?a=1"; urlsWithoutSchema << ".kde.org:1234/sub/path?a=1#anchor"; urlsWithoutSchema << ".kde.org:1234/sub/path/special(123)?a=1#anchor"; urlsWithoutSchema << ".kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor"; + urlsWithoutSchema << ".kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor[bla"; + urlsWithoutSchema << ".kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor[bla]"; + urlsWithoutSchema << ".kde.org:1234/\nsub/path:with:colon/\nspecial(123)?\na=1#anchor[bla]"; + urlsWithoutSchema << ".kde.org:1234/ \n sub/path:with:colon/ \n\t \t special(123)?\n\t \n\t a=1#anchor[bla]"; QStringList starts; starts << "www" << "ftp" << "news:www"; foreach (QString start, starts) { foreach (QString url, urlsWithoutSchema) { + // by defintion: if the URL is enclosed in brackets, the URL itself is not allowed + // to contain the closing bracket, as this would be detected as the end of the URL + if ( ( left.length() == 1 ) && ( url.contains( right[0] ) ) ) + continue; + + // if the url contains a whitespace, it must be enclosed with brackets + if ( (url.contains('\n') || url.contains('\t') || url.contains(' ')) && + left.isEmpty() ) + continue; + QString test(left + start + url + right); LinkLocator ll(test, left.length()); QString gotUrl = ll.getUrl(); + // we want to have the url without whitespace + url.remove(' '); + url.remove('\n'); + url.remove('\t'); + bool ok = ( gotUrl == (start + url) ); //qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << (start + url); - QVERIFY2( ok, qPrintable(test) ); + if ( !ok ) qDebug() << "got:" << gotUrl; + QVERIFY2( ok, qPrintable(gotUrl) ); } } + // test max url length + QString url = "http://www.kde.org/this/is/a_very_loooooong_url/test/test/test"; + { + LinkLocator ll(url); + ll.setMaxUrlLen(10); + QVERIFY( ll.getUrl().isEmpty() ); // url too long + } + { + LinkLocator ll(url); + ll.setMaxUrlLen(url.length() - 1); + QVERIFY( ll.getUrl().isEmpty() ); // url too long + } + { + LinkLocator ll(url); + ll.setMaxUrlLen(url.length()); + QVERIFY( ll.getUrl() == url ); + } + { + LinkLocator ll(url); + ll.setMaxUrlLen(url.length() + 1); + QVERIFY( ll.getUrl() == url ); + } + // mailto { QString addr = "mailto:test@kde.org"; QString test(left + addr + right); LinkLocator ll(test, left.length()); QString gotUrl = ll.getUrl(); bool ok = ( gotUrl == addr ); //qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << addr; - QVERIFY2( ok, qPrintable(test) ); + if ( !ok ) qDebug() << "got:" << gotUrl; + QVERIFY2( ok, qPrintable(gotUrl) ); } } void LinkLocatorTest::testHtmlConvert_data() { QTest::addColumn("plainText"); QTest::addColumn("flags"); QTest::addColumn("htmlText"); //QTest::newRow( "" ) << "foo" << 0 << "foo"; //QTest::newRow( "" ) << " foo " << 0 << " foo "; // Linker error when using PreserveSpaces, therefore the hardcoded 0x01 QTest::newRow( "" ) << " foo" << 0x01 << " foo"; QTest::newRow( "" ) << " foo" << 0x01 << "  foo"; QTest::newRow( "" ) << " foo " << 0x01 << "  foo  "; QTest::newRow( "" ) << " foo " << 0x01 << "  foo "; QTest::newRow( "" ) << "bla bla bla bla bla" << 0x01 << "bla bla bla bla bla"; QTest::newRow( "" ) << "bla bla bla \n bla bla bla " << 0x01 << "bla bla bla 
\n  bla bla bla "; QTest::newRow( "" ) << "bla bla bla" << 0x01 << "bla bla  bla"; QTest::newRow( "" ) << " bla bla \n bla bla a\n bla bla " << 0x01 << " bla bla 
\n bla bla a
\n  bla bla "; } void LinkLocatorTest::testHtmlConvert() { QFETCH(QString, plainText); QFETCH(int, flags); QFETCH(QString, htmlText); QString actualHtml = LinkLocator::convertToHtml( plainText, flags ); QCOMPARE( actualHtml, htmlText ); }