From: Maximiliano Curia Date: Tue, 19 Jul 2016 14:24:06 +0000 (+0100) Subject: Import akonadi_16.04.3.orig.tar.xz X-Git-Tag: archive/raspbian/4%20.08.2-2+rpi1~1^2^2~5 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=3253bdf143ed5ece248fc3ea3dba3b46ed950630;p=akonadi.git Import akonadi_16.04.3.orig.tar.xz [dgit import orig akonadi_16.04.3.orig.tar.xz] --- 3253bdf143ed5ece248fc3ea3dba3b46ed950630 diff --git a/.arcconfig b/.arcconfig new file mode 100644 index 0000000..20d53ee --- /dev/null +++ b/.arcconfig @@ -0,0 +1,4 @@ +{ + "phabricator.uri" : "https://phabricator.kde.org/project/profile/34/", + "history.immutable" : true +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5a13b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Ignore the following files +*~ +*.[oa] +*.kdev4 +.swp.* +.*.swp diff --git a/.kateconfig b/.kateconfig new file mode 100644 index 0000000..0d4badb --- /dev/null +++ b/.kateconfig @@ -0,0 +1 @@ +// kate: space-indent on; indent-width 4; remove-trailing-space on; remove-trailing-space-save on; diff --git a/.krazy b/.krazy new file mode 100644 index 0000000..614c639 --- /dev/null +++ b/.krazy @@ -0,0 +1,3 @@ +EXCLUDE i18ncheckarg,syscalls,qclasses,qmethods,crashy,strings,cpp +SKIP /tests/ + diff --git a/.reviewboardrc b/.reviewboardrc new file mode 100644 index 0000000..13c7e7e --- /dev/null +++ b/.reviewboardrc @@ -0,0 +1,7 @@ +REVIEWBOARD_URL = "https://git.reviewboard.kde.org" +TARGET_GROUPS = "akonadi" +REPOSITORY = "akonadi" +TARGET_PEOPLE = "dvratil" +#REPOSITORY = "git://anongit.kde.org/akonadi" +BRANCH = "master" + diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..fbe1c07 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,88 @@ +Maintainer: +- Dan Vrátil + +Main Authors: +- Volker Krause +- Till Adam +- Tobias Koenig +- Kevin Krammer + +Contributors: +- Albert Astals Cid +- Àlex Fiestas +- Alex Merry +- Alexander Neundorf +- Allen Winter +- Andras Mantia +- Andreas Cord-Landwehr +- Andreas Gungl +- Andreas Hartmetz +- Andreas Holzammer +- Andreas Pakulat +- Andre Heinecke +- Aurélien Gâteau +- Bertjan Broeksema +- Bjoern Ricks +- Carlo Segato +- Christian Ehrlicher +- Christian Mollekopf +- Cédric Villemain +- Christian Schaarschmidt +- Christophe Giboudeaux +- Constantin Berzan +- Dario Freddi +- David Faure +- David Jarvie +- Dirk Mueller +- Frank Osterfeld +- Grégory Oestreicher +- Gregory Schlomoff +- Guy Maurel +- Harald Fernengel +- Helio Chissini de Castro +- Igor Trindade Oliveira +- Ingo Kloecker +- Jaime Torres +- Jakub Stachowski +- Jesper Thomschütz +- Jesse Lee Zamora +- Kevin Ottens +- Kitware, Inc., Insight Consortium. +- Laurent Montel +- Leo Franchi +- Loic Marteau +- Manolo Valdes +- Marc Mutz +- Marco Martin +- Matthias Kretz +- Michael Drueing +- Michael Jansen +- Mike Arthur +- Milian Wolff +- Mirko Boehm +- Nicolás Alvarez +- Nicolas Lécureuil +- Olivier Trichet +- Patrick Spendrin +- Pavel Heimlich +- Pino Toscano +- Raphael Kubo da Costa +- Rex Dieter +- Robert Zwerus +- Rolf Eike Beer +- Romain Pokrzywka +- Sebastian Sauer +- Sebastian Trueg +- Sergio Martins +- Shaheed Haque +- Stephen Kelly +- Szymon Stefanek +- Thomas Friedrichsmeier +- Thomas McGuire +- Timo Hummel +- Tom Albers +- Torgny Nyblom +- Vadim Zhukov +- Will Stephenson +- Wolfgang Rohdewald +- Yury G. Kudryashov diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..936bdc3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,357 @@ +project(Akonadi) + +cmake_minimum_required(VERSION 2.8.12) + +# ECM setup +find_package(ECM 5.16.0 CONFIG REQUIRED) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) + +include(GenerateExportHeader) +include(ECMGenerateHeaders) +include(ECMGeneratePriFile) +include(ECMPackageConfigHelpers) +include(ECMSetupVersion) +include(FeatureSummary) +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) +include(CheckIncludeFiles) +include(ECMQtDeclareLoggingCategory) + +include(AkonadiMacros) + +set(QT_REQUIRED_VERSION "5.2.0") +set(AKONADI_VERSION "5.2.2") +set(KF5_VERSION "5.16.0") + +ecm_setup_version(${AKONADI_VERSION} + VARIABLE_PREFIX AKONADI + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/akonadi_version.h" + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiConfigVersion.cmake" + SOVERSION 5) + +# Find packages +find_package(Qt5Core ${QT_REQUIRED_VERSION} CONFIG REQUIRED) +find_package(Qt5Gui ${QT_REQUIRED_VERSION} CONFIG REQUIRED) +find_package(Qt5Widgets ${QT_REQUIRED_VERSION} CONFIG REQUIRED) +find_package(Qt5Sql ${QT_REQUIRED_VERSION} CONFIG REQUIRED) +find_package(Qt5Network ${QT_REQUIRED_VERSION} CONFIG REQUIRED) +find_package(Qt5Xml ${QT_REQUIRED_VERSION} CONFIG REQUIRED) +find_package(Qt5DBus ${QT_REQUIRED_VERSION} CONFIG REQUIRED) +find_package(Qt5Test ${QT_REQUIRED_VERSION} CONFIG REQUIRED) + +find_package(KF5ItemViews ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5KIO ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5Config ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5I18n ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5DesignerPlugin ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5DBusAddons ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5ItemModels ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5GuiAddons ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5IconThemes ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5WindowSystem ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5Completion ${KF5_VERSION} CONFIG REQUIRED) + +find_package(Qt5Designer NO_MODULE) +set_package_properties(Qt5Designer PROPERTIES + PURPOSE "Required to build the Qt Designer plugins" + TYPE OPTIONAL +) + +set(Boost_MINIMUM_VERSION "1.34.0") +find_package(Boost ${Boost_MINIMUM_VERSION}) +set_package_properties(Boost PROPERTIES + DESCRIPTION "Boost C++ Libraries" + URL "http://www.boost.org" + TYPE REQUIRED +) + +set(AKONADI_TESTS_EXPORT AKONADICORE_EXPORT) +configure_file(akonaditests_export.h.in "${CMAKE_CURRENT_BINARY_DIR}/akonaditests_export.h") + +# Make sure the KF5Akonadi_DATA_DIR is absolute before passing it to KF5AkonadiConfig.cmake.in +# otherwise build fails either on OSX CI, or for normal users +if (IS_ABSOLUTE "${KDE_INSTALL_DATADIR_KF5}") + set(KF5Akonadi_DATA_DIR "${KDE_INSTALL_DATADIR_KF5}/akonadi") +else() + set(KF5Akonadi_DATA_DIR "${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_DATADIR_KF5}/akonadi") +endif() + +############### Build Options ############### +option(AKONADI_BUILD_QSQLITE "Build the Sqlite backend." TRUE) +option(ENABLE_ASAN "Build Akonadi with AddressSanitzer - https://code.google.com/p/address-sanitizer/" FALSE) +option(BUILD_TOOLS "Build and install tools for development and testing purposes." TRUE) + +if(BUILD_TESTING) + set(BUILD_TOOLS TRUE) +endif() + +if (CXX_STDLIB) + set (CXX_STDLIB_FLAGS "-stdlib=${CXX_STDLIB}") +endif () + +set(SMI_MIN_VERSION "0.20") +find_package(SharedMimeInfo ${SMI_MIN_VERSION} REQUIRED) + +find_program(XSLTPROC_EXECUTABLE xsltproc) +if(NOT XSLTPROC_EXECUTABLE) + message(FATAL_ERROR "\nThe command line XSLT processor program 'xsltproc' could not be found.\nPlease install xsltproc.\n") +endif() + +find_program(MYSQLD_EXECUTABLE NAMES mysqld + PATHS /usr/sbin /usr/local/sbin /usr/libexec /usr/local/libexec /opt/mysql/libexec /usr/mysql/bin /opt/mysql/sbin + DOC "The mysqld executable path. ONLY needed at runtime" +) + +if(MYSQLD_EXECUTABLE) + message(STATUS "MySQL Server found: ${MYSQLD_EXECUTABLE}") +else() + message(STATUS "MySQL Server wasn't found. it is required to use the MySQL backend.") +endif() + +find_path(POSTGRES_PATH NAMES pg_ctl + HINTS /usr/lib${LIB_SUFFIX}/postgresql/8.4/bin + /usr/lib${LIB_SUFFIX}/postgresql/9.0/bin + /usr/lib${LIB_SUFFIX}/postgresql/9.1/bin + DOC "The pg_ctl executable path. ONLY needed at runtime by the PostgreSQL backend" +) + +if(POSTGRES_PATH) + message(STATUS "PostgreSQL Server found.") +else() + message(STATUS "PostgreSQL wasn't found. it is required to use the Postgres backend.") +endif() + + +if("${DATABASE_BACKEND}" STREQUAL "SQLITE") + set(SQLITE_TYPE "REQUIRED") +else() + set(SQLITE_TYPE "OPTIONAL") +endif() + +if(AKONADI_BUILD_QSQLITE AND Qt5Core_VERSION VERSION_LESS 5.7.0) # API change in 5.7 breaks our sqlite driver + set(SQLITE_MIN_VERSION 3.6.23) + find_package(Sqlite ${SQLITE_MIN_VERSION}) + set_package_properties(Sqlite PROPERTIES + URL "http://www.sqlite.org" + DESCRIPTION "The Sqlite database library" + TYPE ${SQLITE_TYPE} + PURPOSE "Required by the Sqlite backend" + ) +endif() + +find_package(Backtrace) +if(Backtrace_FOUND) + include_directories(${Backtrace_INCLUDE_DIRS}) + add_definitions(-DHAVE_BACKTRACE) +endif() + +find_program(XMLLINT_EXECUTABLE xmllint) +if(NOT XMLLINT_EXECUTABLE) + message(STATUS "xmllint not found, skipping akonadidb.xml schema validation") +endif() + +check_include_files(unistd.h HAVE_UNISTD_H) +if(HAVE_UNISTD_H) + add_definitions(-DHAVE_UNISTD_H) +endif() + +if (ENABLE_ASAN) + find_package(ASan) +endif () + +if (IS_ABSOLUTE "${DBUS_INTERFACES_INSTALL_DIR}") + set(AKONADI_DBUS_INTERFACES_INSTALL_DIR "${DBUS_INTERFACES_INSTALL_DIR}") +else() + set(AKONADI_DBUS_INTERFACES_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${DBUS_INTERFACES_INSTALL_DIR}") +endif() + +if (IS_ABSOLUTE "${KDE_INSTALL_INCLUDEDIR_KF5}") + set(AKONADI_INCLUDE_DIR "${KDE_INSTALL_INCLUDEDIR_KF5}") +else() + set(AKONADI_INCLUDE_DIR "${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_INCLUDEDIR_KF5}") +endif() + +############### Build Options ############### + +include(CTest) # Calls enable_testing(). +include(CTestConfig.cmake) + +if(NOT DEFINED DATABASE_BACKEND) + set(DATABASE_BACKEND "MYSQL" CACHE STRING "The default database backend to use for Akonadi. Can be either MYSQL, POSTGRES or SQLITE") +endif() + +############### CTest options ############### +# Set a timeout value of 1 minute per test +set(DART_TESTING_TIMEOUT 60) + +# CTestCustom.cmake has to be in the CTEST_BINARY_DIR. +# in the KDE build system, this is the same as CMAKE_BINARY_DIR. +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/CTestCustom.cmake ${CMAKE_CURRENT_BINARY_DIR}/CTestCustom.cmake COPYONLY) + +############### Macros ############### + +macro(SET_DEFAULT_DB_BACKEND) + set(_backend ${ARGV0}) + if("${_backend}" STREQUAL "SQLITE") + set(AKONADI_DATABASE_BACKEND QSQLITE3) + set(AKONADI_BUILD_QSQLITE TRUE) + else() + if("${_backend}" STREQUAL "POSTGRES") + set(AKONADI_DATABASE_BACKEND QPSQL) + else() + set(AKONADI_DATABASE_BACKEND QMYSQL) + endif() + endif() + + message(STATUS "Using default db backend ${AKONADI_DATABASE_BACKEND}") + add_definitions(-DAKONADI_DATABASE_BACKEND="${AKONADI_DATABASE_BACKEND}") +endmacro() + +#### DB BACKEND DEFAULT #### +set_default_db_backend(${DATABASE_BACKEND}) + +############### Compilers flags ############### + +option(CMAKE_COMPILE_GCOV "Build with coverage support." FALSE) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_C_COMPILER MATCHES "icc" OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) + set(_ENABLE_EXCEPTIONS -fexceptions) + + # more aggressive warnings and C++11 support + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-long-long -std=iso9899:1990 -Wundef -Wcast-align -Werror-implicit-function-declaration -Wchar-subscripts -Wall -Wextra -Wpointer-arith -Wwrite-strings -Wformat-security -Wmissing-format-attribute -fno-common") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wnon-virtual-dtor -Wundef -Wcast-align -Wchar-subscripts -Wall -Wextra -Wpointer-arith -Wformat-security -fno-common") + + file(WRITE ${CMAKE_BINARY_DIR}/cxx11_check.cpp + "enum Enum { Value = 1 }; + struct Class { + Class(int val) { (void)val; }; + // Delegating constructor + Class(): Class(42) {}; + // New-style enumerator + Class(Enum e = Enum::Value) { (void)e; }; + }; + int main() {} + ") + try_compile(CXX11_SUPPORTED + ${CMAKE_BINARY_DIR}/cxx11_check + ${CMAKE_BINARY_DIR}/cxx11_check.cpp) + if (NOT CXX11_SUPPORTED) + message(FATAL_ERROR "Compiler does not support all required C++11 features") + endif() + + # debugfull target + set(CMAKE_CXX_FLAGS_DEBUGFULL "-g3 -fno-inline" CACHE STRING "Flags used by the C++ compiler during debugfull builds." FORCE) + set(CMAKE_C_FLAGS_DEBUGFULL "-g3 -fno-inline" CACHE STRING "Flags used by the C compiler during debugfull builds." FORCE) + mark_as_advanced(CMAKE_CXX_FLAGS_DEBUGFULL CMAKE_C_FLAGS_DEBUGFULL) + + # Update the documentation string of CMAKE_BUILD_TYPE for ccache & cmake-gui + set(CMAKE_BUILD_TYPE "${CMAKE_BUILD_TYPE}" CACHE STRING + "Choose the type of build, options are: None debugfull Debug Release RelWithDebInfo MinSizeRel." + FORCE) + + if (ASAN_FOUND) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_ASAN}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_ASAN}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${CMAKE_EXE_LINKER_FLAGS_ASAN}" CACHE STRING "Flags used by the linker" FORCE) + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${CMAKE_SHARED_LINKER_FLAGS_ASAN}" CACHE STRING "Flags used by the linker" FORCE) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${CMAKE_SHARED_LINKER_FLAGS_ASAN}" CACHE STRING "Flags used by the linker" FORCE) + add_definitions(-DENABLE_ASAN) + endif() + + # coverage support + if(CMAKE_COMPILE_GCOV) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lprofile_rt" CACHE STRING "Flags used by the linker" FORCE) + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -lprofile_rt" CACHE STRING "Flags used by the linker" FORCE) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -lprofile_rt" CACHE STRING "Flags used by the linker" FORCE) + endif() + endif() +endif() + +if(MSVC) + set(_ENABLE_EXCEPTIONS -EHsc) +endif() + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_ENABLE_EXCEPTIONS}") + +add_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII) +#add_definitions(-DQT_NO_KEYWORDS) +add_definitions(-DQT_USE_QSTRINGBUILDER -DQT_USE_FAST_OPERATOR_PLUS) + +############### Configure files ############# + +configure_file(akonadi-prefix.h.cmake ${Akonadi_BINARY_DIR}/akonadi-prefix.h) +configure_file(config-akonadi.h.cmake ${Akonadi_BINARY_DIR}/config-akonadi.h) + +############### build targets ############### + +add_definitions(-DTRANSLATION_DOMAIN=\"libakonadi5\") +add_definitions("-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII") + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + src +) + +add_subdirectory(src) + +if(BUILD_TOOLS) + # add testrunner (application for managing a self-contained + # test environment) + add_subdirectory(autotests/libs/testrunner) + add_subdirectory(autotests/libs/testresource) + add_subdirectory(autotests/libs/testsearchplugin) +endif() + +if(BUILD_TESTING) + add_subdirectory(autotests) + add_subdirectory(tests) +endif() + + +############### install stuff ############### + +install(FILES akonadi-mime.xml DESTINATION ${XDG_MIME_INSTALL_DIR}) +update_xdg_mimetypes(${XDG_MIME_INSTALL_DIR}) + +feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) + +############### CMake Config Files ############### + +set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5Akonadi") + +ecm_configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/KF5AkonadiConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiConfig.cmake" + INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiConfigVersion.cmake" + "${CMAKE_CURRENT_SOURCE_DIR}/KF5AkonadiMacros.cmake" + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + COMPONENT Devel +) + +install(EXPORT + KF5AkonadiTargets + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + FILE KF5AkonadiTargets.cmake + NAMESPACE KF5::) + +install(FILES akonadi.categories + DESTINATION ${KDE_INSTALL_CONFDIR} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadi_version.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} COMPONENT Devel +) + +if ("${CMAKE_BINARY_DIR}" STREQUAL "${CMAKE_CURRENT_BINARY_DIR}") + feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) +endif() diff --git a/COPYING.LIB b/COPYING.LIB new file mode 100644 index 0000000..2d2d780 --- /dev/null +++ b/COPYING.LIB @@ -0,0 +1,510 @@ + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations +below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it +becomes a de-facto standard. To achieve this, non-free programs must +be allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control +compilation and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least + three years, to give the same user the materials specified in + Subsection 6a, above, for a charge no more than the cost of + performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply, and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License +may add an explicit geographical distribution limitation excluding those +countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms +of the ordinary General Public License). + + To apply these terms, attach the following notices to the library. +It is safest to attach them to the start of each source file to most +effectively convey the exclusion of warranty; and each file should +have at least the "copyright" line and a pointer to where the full +notice is found. + + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or +your school, if any, to sign a "copyright disclaimer" for the library, +if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James + Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/CTestConfig.cmake b/CTestConfig.cmake new file mode 100644 index 0000000..23e8d6e --- /dev/null +++ b/CTestConfig.cmake @@ -0,0 +1,13 @@ +## This file should be placed in the root directory of your project. +## Then modify the CMakeLists.txt file in the root directory of your +## project to incorporate the testing dashboard. +## # The following are required to uses Dart and the Cdash dashboard +## ENABLE_TESTING() +## INCLUDE(CTest) +set(CTEST_PROJECT_NAME "akonadi") +set(CTEST_NIGHTLY_START_TIME "00:00:00 UTC") + +set(CTEST_DROP_METHOD "http") +set(CTEST_DROP_SITE "my.cdash.org") +set(CTEST_DROP_LOCATION "/submit.php?project=akonadi") +set(CTEST_DROP_SITE_CDASH TRUE) diff --git a/CTestCustom.cmake b/CTestCustom.cmake new file mode 100644 index 0000000..0e35273 --- /dev/null +++ b/CTestCustom.cmake @@ -0,0 +1,22 @@ +# This file contains all the specific settings that will be used +# when running 'make Experimental' + +# Change the maximum warnings that will be displayed +# on the report page (default 50) +set(CTEST_CUSTOM_MAXIMUM_NUMBER_OF_WARNINGS 1000) + +# Errors that will be ignored +set(CTEST_CUSTOM_ERROR_EXCEPTION + ${CTEST_CUSTOM_ERROR_EXCEPTION} + "ICECC" + "Segmentation fault" + "GConf Error" + "Client failed to connect to the D-BUS daemon" + "Failed to connect to socket" + "qlist.h.*increases required alignment of target type" + "qmap.h.*increases required alignment of target type" + "qhash.h.*increases required alignment of target type" + ) + +# No coverage for these files (auto-generated, unit tests, etc) +set(CTEST_CUSTOM_COVERAGE_EXCLUDE ".moc$" "moc_" "ui_" "/tests" "/autotests" "qrc_" "adaptor.h$" "adaptor.cpp$" "/src/server/[^/]+interface\\.") diff --git a/CodingStyle.txt b/CodingStyle.txt new file mode 100644 index 0000000..2cd436c --- /dev/null +++ b/CodingStyle.txt @@ -0,0 +1,8 @@ +The all directory "akonadi" is tested for all rules of the coding style. +But of the qsqlite subdirectory which contains a .no_coding_style file. + +The rules and all the scripts are free for download and comments at: + http://techbase.kde.org/Policies/Kdepim_Coding_Style + +Feel free to communicate any comments or/and bugs: + guy dot maurel at kde dot org diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..a860378 --- /dev/null +++ b/INSTALL @@ -0,0 +1,60 @@ +Akonadi's build system uses cmake. + +So to compile Akonadi first create a build dir + + mkdir build + cd build + +then run cmake: + + cmake .. + +(a typical cmake option that is often used is: -DCMAKE_INSTALL_PREFIX=) + +cmake then presents a configuration summary. At this point you may +want to install missing dependancies (if you do, remove the CMakeCache.txt) +and run cmake again. + +Finally build Akonadi: + + make + +And install it (in most cases root privileges are required): + + make install + +That's all :) + +=== Build Options === + +The following options are available when running CMake: + +* AKONADI_BUILD_TESTS (Default: TRUE): Build the Akonadi unit tests +* AKONADI_BUILD_QSQLITE (Default: TRUE): Build the SQLite backend +* KDE_INSTALL_USE_QT_SYS_PATHS (Default: FALSE): Useful for distributions. + Once enabled, the qsqlite3 backend will be installed in the sqlbackends subdirectory of the default Qt plugins directory + specified at Qt build time. +* DATABASE_BACKEND (Default: MYSQL, available: MYSQL, POSTGRES, SQLITE): Define which database driver to use by default. + MYSQL is preferred, SQLITE should be avoided. + +=== Build Requirements === + +Required: + +* CMake (http://www.cmake.org) >= 2.8.12 +* Qt5 >= 5.2.0 (http://qt.nokia.com/downloads) +* Shared-mime-info >= 0.20 (http://freedesktop.org/wiki/Software/shared-mime-info) +* Xsltproc (http://xmlsoft.org/XSLT/downloads.html) + +Optional: + +* Mysqld (http://www.mysql.com) - Optional at build time. You can pass -DMYSQLD_EXECUTABLE=/path/to/mysqld when running CMake instead +* SQlite >= 3.6.23 (http://www.sqlite.org) - Needed if you want to build the Sqlite backend +* Postgresql (http://www.postgres.org) - Optional at build time. You can pass -DPOSTGRES_PATH=/path/to/pg_ctl when running CMake instead + +=== Runtime Requirements === + +* SQlite if you plan to use the SQLite backend (NOT RECOMMENDED for desktop) +* MySQL server >= 5.1.3 (or compatible replacements such as MariaDB) if you plan to use the Mysql backend +* a Postgresql server if you plan to use the Postgres backend + diff --git a/Info.plist.template b/Info.plist.template new file mode 100644 index 0000000..c39ddb9 --- /dev/null +++ b/Info.plist.template @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + LSUIElement + 1 + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + diff --git a/KF5AkonadiConfig.cmake.in b/KF5AkonadiConfig.cmake.in new file mode 100644 index 0000000..b5cb736 --- /dev/null +++ b/KF5AkonadiConfig.cmake.in @@ -0,0 +1,27 @@ +@PACKAGE_INIT@ + +# set the directories +if(NOT AKONADI_INSTALL_DIR) + set(AKONADI_INSTALL_DIR "@CMAKE_INSTALL_PREFIX@") +endif(NOT AKONADI_INSTALL_DIR) + +find_dependency(KF5Completion "@KF5_VERSION@") +find_dependency(KF5JobWidgets "@KF5_VERSION@") +find_dependency(KF5Service "@KF5_VERSION@") +find_dependency(KF5Solid "@KF5_VERSION@") +find_dependency(KF5XmlGui "@KF5_VERSION@") +find_dependency(KF5ItemModels "@KF5_VERSION@") +find_dependency(KF5KDELibs4Support "@KF5_VERSION@") + +find_dependency(Qt5Network "@QT_REQUIRED_VERSION@") + +set_and_check(AKONADI_DBUS_INTERFACES_DIR "@AKONADI_DBUS_INTERFACES_INSTALL_DIR@") +set_and_check(AKONADI_INCLUDE_DIR "@AKONADI_INCLUDE_DIR@") + +find_dependency(Boost "@Boost_MINIMUM_VERSION@") + +include(${CMAKE_CURRENT_LIST_DIR}/KF5AkonadiTargets.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/KF5AkonadiMacros.cmake) + +# The directory where akonadi-xml.xsd and kcfg2dbus.xsl are installed +set(KF5Akonadi_DATA_DIR "@KF5Akonadi_DATA_DIR@") diff --git a/KF5AkonadiMacros.cmake b/KF5AkonadiMacros.cmake new file mode 100644 index 0000000..06ea421 --- /dev/null +++ b/KF5AkonadiMacros.cmake @@ -0,0 +1,100 @@ +# +# Convenience macros to add akonadi testrunner unit-tests +# +# Set AKONADI_RUN_ISOLATED_TESTS to true to enable running the test +# Set AKONADI_RUN_MYSQL_ISOLATED_TESTS to true to run the tests against MySQL +# Set AKONADI_RUN_PGSQL_ISOLATED_TESTS to true to run the tests against PostgreSQL +# Set AKONADI_RUN_SQLITE_ISOLATED_TESTS to true to run the tests against SQLite +# Set AKONADI_TESTS_XML to true if you provided per-test configuration XML files +# +# You still need to provide the test environment, see akonadi/autotests/libs/unittestenv +# copy the unittestenv directory to your unit test directory and update the files +# as necessary + +# add_akonadi_isolated_test_advanced +# +# Arguments: +# source: a CPP file with the test itself +# additionalSources: additional CPP files needed to build the test +# linklibraries: additional libraries to link the test executable against +# +macro(add_akonadi_isolated_test_advanced _source _additionalsources _linklibraries) + set(_test ${_source}) + get_filename_component(_name ${_source} NAME_WE) + add_executable( ${_name} ${_test} ${_additionalsources}) + ecm_mark_as_test(${_name}) + target_link_libraries(${_name} + Qt5::Test Qt5::Gui Qt5::Widgets KF5::KIOCore KF5::AkonadiCore KF5::DBusAddons + ${_linklibraries}) + + # Set the akonaditest path when the macro is used in Akonadi + find_program(_testrunner + NAMES akonaditest akonaditest.exe + PATHS ${CMAKE_CURRENT_BINARY_DIR} ${_akonaditest_DIR} ENV PATH) + if (_testrunner-NOTFOUND) + message(WARNING "Could not locate akonaditest executable, isolated Akonadi tests will fail!") + endif() + + # based on kde4_add_unit_test + if (WIN32) + get_target_property( _loc ${_name} LOCATION ) + set(_executable ${_loc}.bat) + else() + set(_executable ${EXECUTABLE_OUTPUT_PATH}/${_name}) + endif() + if (UNIX) + if (APPLE) + set(_executable ${_executable}.app/Contents/MacOS/${_name}) + else() + set(_executable ${_executable}) + endif() + endif() + + if ( KDEPIMLIBS_TESTS_XML OR AKONADI_TESTS_XML ) + set( MYSQL_EXTRA_OPTIONS_DB -xml -o ${TEST_RESULT_OUTPUT_PATH}/mysql-db-${_name}.xml ) + set( MYSQL_EXTRA_OPTIONS_FS -xml -o ${TEST_RESULT_OUTPUT_PATH}/mysql-fs-${_name}.xml ) + set( POSTGRESL_EXTRA_OPTIONS_DB -xml -o ${TEST_RESULT_OUTPUT_PATH}/postgresql-db-${_name}.xml ) + set( POSTGRESL_EXTRA_OPTIONS_FS -xml -o ${TEST_RESULT_OUTPUT_PATH}/postgresql-fs-${_name}.xml ) + set( SQLITE_EXTRA_OPTIONS -xml -o ${TEST_RESULT_OUTPUT_PATH}/sqlite-${_name}.xml ) + endif() + + if ( KDEPIMLIBS_RUN_MYSQL_ISOLATED_TESTS OR AKONADI_RUN_MYSQL_ISOLATED_TESTS ) + find_program( MYSQLD_EXECUTABLE mysqld /usr/sbin /usr/local/sbin /usr/libexec /usr/local/libexec /opt/mysql/libexec /usr/mysql/bin ) + if ( MYSQLD_EXECUTABLE ) + add_test( akonadi-mysql-db-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-mysql-db.xml ${_executable} + ${MYSQL_EXTRA_OPTIONS_DB} ) + add_test( akonadi-mysql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-mysql-fs.xml ${_executable} + ${MYSQL_EXTRA_OPTIONS_FS} ) + endif() + endif() + + if ( KDEPIMLIBS_RUN_PGSQL_ISOLATED_TESTS OR AKONADI_RUN_PGSQL_ISOLATED_TESTS ) + find_program( POSTGRES_EXECUTABLE postgres ) + if ( POSTGRES_EXECUTABLE ) + add_test( akonadi-postgresql-db-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-postgresql-db.xml ${_executable} + ${POSTGRESL_EXTRA_OPTIONS_DB} ) + add_test( akonadi-postgresql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-postgresql-fs.xml ${_executable} + ${POSTGRESL_EXTRA_OPTIONS_FS} ) + endif() + endif() + + # Always have SQLITE tests + add_test( akonadi-sqlite-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-sqlite-db.xml ${_executable} + ${SQLITE_EXTRA_OPTIONS} ) +endmacro() + + +# add_akonadi_isolated_test +# +# Set AKONADI_RUN_ISOLATED_TESTS to true to enable running the test +# Set AKONADI_RUN_MYSQL_ISOLATED_TESTS to true to run the tests against MySQL +# Set AKONADI_RUN_PGSQL_ISOLATED_TESTS to true to run the tests against PostgreSQL +# Set AKONADI_RUN_SQLITE_ISOLATED_TESTS to true to run the tests against SQLite +# Set AKONADI_TESTS_XML to true to load initial database data from XML files +# +# Arguments: +# source: a CPP file with the test itself +# +macro(add_akonadi_isolated_test _source) + add_akonadi_isolated_test_advanced(${_source} "" "") +endmacro() diff --git a/Mainpage.dox b/Mainpage.dox new file mode 100644 index 0000000..5746f47 --- /dev/null +++ b/Mainpage.dox @@ -0,0 +1,1137 @@ +/** +\mainpage %Akonadi + +Akonadi aims to be an extensible cross-desktop storage service for PIM data +and meta data providing concurrent read, write, and query access. +It provides unique desktop-wide object identification and retrieval. + +Akonadi framework provides two parts: the server, and client libraries to +access the data managed by the server. + +\page akonadi_server Akonadi Server + +

+Overview | +\ref akonadi_server_definitions | +\ref akonadi_server_srclayout +

+ + +This is the API documentation for the Akonadi server. If you are using Akonadi +from within KDE, you almost certainly want the +KDE client library documentation. +This API reference is more useful to people implementing client libraries or +working on the Akonadi server itself. + +For additional information, see the Akonadi website. + +\section akonadi_server_architecture Architecture + + + +The Akonadi framework uses a client/server architecture. The Akonadi server has the following primary tasks: +\li Abstract access to data from arbitrary sources, using toolkit-agnostic protocols and data formats +\li Provide a data cache shared among several clients +\li Provide change notifications and conflict detection +\li Support offline change recording and change replay for remote data + +\subsection akonadi_server_design_principles Design Principles + +The Akonadi architecture is based on the following four design principles: + +\li Functionality is spread over different processes.
+ This separation has the big advantage that if one process crashes because of + a programming error it doesn't affect the other components. That results in + robustness of the whole system. A disadvantage might be that there is an additional + overhead due to inter-process communication. +\li Communication protocol is split into data and control channel.
+ When doing communication between processes you have to differentiate between the type of data + that is being transferred. For a large amount of data a high-performance + protocol should be used and for control data a low-latency protocol. + Matching both requirements in one protocol is mostly impossible and hard to + achieve with currently available software. +\li Separate logic from storage.
+ By separating the logic from the storage, the storage can be used to store data + of any type. In this case, the storage is a kind of service, which is available for + other components of the system. The logic is located in separated components and so + 3rd-party developers can extend the system by providing their own components. +\li Keep communication asynchronous.
+ To allow a non-blocking GUI, all the communication with the back-end and within the + back-end itself must be asynchronous. You can easily provide a synchronous convenience + for the application developer; the back-end, however, must communicate asynchronously. + +\subsection akonadi_server_components Components + +The Akonadi server itself consists of a number of components: +\li The Akonadi control process (\c akonadi_control). It is responsible for managing all other server components +and Akonadi agents. +\li The Akonadi server process (\c akonadiserver). The actual data access and caching server. +\li The Akonadi agent server (\c akonadi_agent_server). Allows running of multiple Akonadi agents in one process. +\li The Akonadi agent launcher (\c akonadi_agent_launcher). A helper process for running Akonadi agents. +\li The Akonadi control tool (\c akonadictl). A tool to start/stop/restart the Akonadi server system and query its status. + This is the only program of these listed here you should ever run manually. +\li The Akonadi protocol library (\c libakonadiprotocolinternals), Contains protocol definitions and protocol parsing methods + useful for client implementations. + +\subsubsection akonadi_server_components_server The Akonadi server process + +The %Akonadi server process (\c akonadiserver) has the following tasks: +\li Provide a transaction-safe data store. +\li Provide operations to add/modify/delete items and collections in the local store, implementing the server side of the ASAP protocol. +\li Cache management of cached remote contents. +\li Manage virtual collections representing search results. +\li Provide change notifications for all known Akonadi objects over D-Bus. + +\subsubsection akonadi_server_components_control The Akonadi server control process + +The %Akondi control process (\c akonadi_control) has the following tasks: +\li Manage and monitor the other server processes. +\li Lifecycle management of agent instances using the various supported agent launch methods. +\li Monitor agent instances and provide crash recovery. +\li Provide D-Bus API to manage agents. +\li Provide change notifications on agent types and agent instances. + + +\section akonadi_server_objects Objects and Data Types + +The %Akonadi server operates on two basic object types, called items and collections. They are comparable to files and directories +and are described in more detail in this section. + +\subsection akonadi_server_objects_items Akonadi Items + +An item is a generic container for whatever you want to store in Akonadi (eg. mails, +events, contacts, etc.). An item consists of some generic information (such as identifier, +mimetype, change date, flags, etc.) and a set of data fields, the item parts. Items +are independent of the type of stored data, the semantics of the actual content is only +known on the client side. + +\subsubsection akonadi_server_objects_items_parts Item Parts + +%Akonadi items can have one or more parts, e.g. an email message consists of the +envelope, the body and possible one or more attachments. Item parts are identified +by an identifier string. There are a few special pre-defined part identifiers (ALL, +ENVELOPE, etc.), but in general the part identifiers are defined by the type specific +extensions (ie. resource, serializer plugin, type specific client library). + +\subsubsection akonadi_server_objects_items_attributes Item Tags + +%Tags are self-contained entities stored in separate database table. A tag is a +relation between multiple items. Tags can have different types (PLAIN, ...) and applications +can define their own type to describe application-specific relations. Tags can also have +attributes to store additional metadata about the relation the tag describes. + +\subsubsection akonadi_server_objects_items_serializer Payload Data Serialization + +Item payload data is typically serialized in a standard format to ensure interoperability between different +client library implementations. However, the %Akonadi server does not enforce any format, +payload data is handled as an opaque binary blob. + +\subsection akonadi_server_objects_collections Collections + +Collections are sets of items. Every item is stored in exactly one +collection, this is sometimes also referred to as the "physical" storage location of the item. +An item might also be visible in several other collections - so called "virtual collections" - +which are defined as the result set of a search query. + +Collections are organized hierarchically, i.e. a collection can have child +collections, thus defining a collection tree. + +Collections are uniquely identified by their identifier in +contrast to their path, which is more robust with regard to renaming and moving. + +\subsubsection akonadi_server_objects_collections_akonadi Collection Properties + +Every collection has a set of supported content types. +These are the mimetypes of items the collection can contain. +Example: A collection of a folder-less iCal file resource would only support +"text/calendar" items, a folder on an IMAP server "message/rfc822" but also +"inode/directory" if it can contain sub-folders. + +There is a cache policy associated with every collection which defines how much +of its content should be kept in the local cache and for how long. + +Additionally, collections can contain an arbitrary set of attributes to represent +various other collection properties such as ACLs, quotas or backend-specific data +used for incremental synchronization. Evaluation of such attributes is the responsibility +of client implementations, the %Akonadi server does not interpret properties +other than content types and cache policies. + +\subsubsection akonadi_server_objects_collections_tree Collection Tree + +There is a single collection tree in Akonadi, consisting of several parts: + +- A root node, id 0 +- One or more top-level collections for each resource. Think of these as mount-points + for the resource. The resources must put their items and sub-collections into their + corresponding top-level collection. +- Resource-dependent sub-collections below the resource top-level collections. + If the resource represents data that is organized in folders (e.g. an IMAP + resource), it can create additional collections below its top-level + collection. These have to be synched with the corresponding backend by the + resource. + Resources which represent folder-less data (e.g. an iCal file) don't need + any sub-collections and put their items directly into the top-level collection. +- A top-level collection containing virtual collections. + +Example: + +\verbatim ++-+ resource-folder1 +| +- sub-folder1 +| +- sub-folder2 +| ... ++-+ resource-folder2 +| ... +| ++-+ Searches + +- search-folder1 + +- search-folder2 + ... +\endverbatim + + +\subsection akonadi_server_objects_identification Object Identification + +\subsubsection akonadi_server_objects_identification_uid Unique Identifier + +Every object stored in %Akonadi (collections and items) has a unique +identifier in the form of an integer value. This identifier cannot be changed in +any way and will stay the same, regardless of any modifications to the referred +object. A unique identifier will never be used twice and is globally unique, +therefore it is possible to retrieve an item without knowing the collection it belongs to. + +\subsubsection akonadi_server_objects_identification_rid Remote Identifier + +Every object can also have an optional so-called remote identifier. This is an +identifier used by the corresponding resource to identify the object on its +backend (e.g., a groupware server). + +The remote identifier can be changed by the owning resource agent only. + +Special case applies for Tags, where each tag can have multiple remote IDs. This fact is +however opaque to resources as each resource is shown only the remote ID that it had +provided when inserting the tag into Akonadi. + +\subsubsection akonadi_server_objects_identification_gid Global Identifier + +Every item can has also so called GID, an identifier specific to the content (payload) +of the item. The GID is extracted from the payload by client serializer when storing the +item in Akonadi. For example, contacts have vCard "UID" field as their GID, emails can +use value of "Message-Id" header. + +\section akonadi_server_protocols Communication Protocols + +For communication within the Akonadi server infrastructure and for communication with Akonadi clients, two communication technologies are used: +\li \em D-Bus Used for management tasks and change notifications. +\li \em ASAP (Akonadi Server Access Protocol), used for high-throughput data transfer. ASAP is based on the well-known IMAP protocol (RFC 3501) + which has been proven it's ability to handle large quantities of data in practice already. + +\todo add protocol documentation + + +\section akonadi_server_interaction Interacting with Akonadi + +There are various possibilities to interact with %Akonadi. + +\section akonadi_server_interaction_client_libraray Akonadi Client Libraries + +Accessing the %Akonadi server using the ASAP and D-Bus interfaces directly is cumbersome. +Therefore you'd usually use a client library implementing the low-level protocol handling +and providing convenient high-level APIs for %Akonadi operations. + +Currently, the most complete implementation is the +KDE %Akonadi client library. + + + +\subsection akonadi_server_interaction_agents Akonadi Agents + +%Akonadi agents are processes which are controlled by the Akonadi server itself. Agents typically +operate autonomously (ie. without much user interaction) on the objects handled by Akonadi, mostly +by reacting to change notifications sent by the %Akonadi server. + +Agents can implement specialized interfaces to provide additional functionality. +The most important ones are the so-called resource agents. + +Resource agents are connectors that provide access to data from an external source, and replay local changes +back to their corresponding backend. + + +\section akonadi_server_implementation Implementation Details + +\subsection akonadi_server_implementation_storage Data and Metadata Storage + +The Akonadi server uses two mechanisms for data storage: +\li A SQL databases for metadata and small payload data +\li Plain files for large payload data + +More details on the SQL database layout can be found here: \ref akonadi_server_database. + +The following SQL databases are supported by the Akonadi server: +\li \em MySQL using the default QtSQL driver shipped with Qt +\li \em Sqlite using the improved QtSQL driver shipped with the Akonadi server +\li \em PostgreSQL using the default QtSQL driver shipped with Qt + +For details on how to configure the various backends, see Akonadi::DataStore. + + + + +\page akonadi_server_definitions Type Definitions + +

+\ref index "Overview" | +\ref Type Definitions | +\ref akonadi_server_srclayout +

+ +To let all components play together nicely, we have to use some common encoding +definitions. + +\li Collection names
+ Collection names and paths are Unicode strings (QString) to allow custom names by the user. +\li Data references
+ The persistent identifier is an unsigned integer and the external URL is + a Unicode string (QString). +\li Transferred data over ASAP
+ The data transferred over ASAP are byte arrays (QByteArray). If Unicode strings are + transferred over ASAP, UTF-8 encoding is applied. +\li Error and status messages
+ Error and status messages are visible to the user, so they have to be + Unicode strings (QString). + + + + +\page akonadi_server_srclayout Source Code Layout + +

+\ref index "Overview" | +\ref akonadi_server_definitions | +\ref Source Code Layout +

+ +The code of the storage and control components is located in the \c server sub-directory. +The different parts are laid out as follows: + +
    +
  • \e control
    + Contains the source code of the \ref akonadi_design_control "control" component. +
  • \e interfaces
    + Contains the D-Bus interface descriptions of the Akonadi components +
  • \e src
    + Contains the source code of the \ref akonadi_design_storage "storage" component. +
  • \e src/handler
    + Contains the source code for the handlers of the single ASAP commands. + See command handlers module +
  • \e src/storage
    + Contains the source code for accessing the storage back-end.
    +
      +
    • entity.{h,cpp}
      + The files contain classes which reflect records in the tables of the database. + They are generated by XSL transformation from akonadidb.xml and entities.xsl +
    • datastore.{h,cpp}
      + The files contain a class which provides the access to the underlaying database tables. +
    +
+ + +\page libakonadi Akonadi client library + +\section libakonadi_intro Introduction + +libakonadi is the client access library for using the Akonadi PIM data server. +All processes accessing Akonadi, including those which communicate with a remote +server (\ref akonadi_design_agents "agents"), are considered clients. + +Functionality provided by libakonadi: + +- \ref libakonadi_objects +- \ref libakonadi_collections +- \ref libakonadi_pimitems +- \ref libakonadi_jobs +- \ref libakonadi_monitor +- \ref libakonadi_serializer +- \ref libakonadi_resource +- \ref libakonadi_integration + +Additional information about Akonadi: + +- Akonadi Server documentation +- \ref akonadi_history +- Website +- Wiki + +Tools for developers: + +- CDash +- Bugtracker + + +\section libakonadi_objects Akonadi Objects + +Akonadi works on two basic object types: collections and items. + +Collections are comparable to folders in a file system and are represented by +the class Akonadi::Collection. Every collection has an associated cache policy +represented by the class Akonadi::CachePolicy which defines what part of its +content is cached for how long. All available ways to work with collections are +listed in the \ref libakonadi_collections "Collections" section. + +Akonadi items are comparable to files in a file system and are represented by +the class Akonadi::Item. Each item represents a single PIM object such as a mail +or a contact. The actual object it represents is its so-called payload. All +available ways to work with items are listed in the \ref libakonadi_pimitems "Items" +section. + +Both items and collections are identified by a persistent unique identifier. +Also, they can contain arbitrary attributes (derived from Akonadi::Attribute) to +attach general or application specific meta data to them. Functionality common +to both is provided by their base class Akonadi::Entity. + + +\section libakonadi_collections Collection retrieval and manipulation + +A collection is represented by the Akonadi::Collection class. + +Classes to retrieve information about collections: + +- Akonadi::CollectionFetchJob +- Akonadi::CollectionStatisticsJob + +Classes to manipulate collections: + +- Akonadi::CollectionCreateJob +- Akonadi::CollectionCopyJob +- Akonadi::CollectionModifyJob +- Akonadi::CollectionDeleteJob + +There is also Akonadi::CollectionModel, which is a self-updating model class which can +be used in combination with Akonadi::CollectionView. Akonadi::CollectionFilterProxyModel +can be used to limit a displayed collection tree to collections supporting a certain +type of PIM items. Akonadi::CollectionPropertiesDialog provides an extensible properties +dialog for collections. Often needed KAction for collection operations are provided by +Akonadi::StandardActionManager. + + +\section libakonadi_pimitems PIM item retrieval and manipulation + +PIM items are represented by classes derived from Akonadi::Item. +Items can be retrieved using Akonadi::ItemFetchJob. + +The following classes are provided to manipulate PIM items: + +- Akonadi::ItemCreateJob +- Akonadi::ItemCopyJob +- Akonadi::ItemModifyJob +- Akonadi::ItemDeleteJob + +Akonadi::ItemModel provides a self-updating model class which can be used to display the content +of a collection. Akonadi::ItemView is the base class for a corresponding view. Often needed KAction +for item operations are provided by Akonadi::StandardActionManager. + + +\section libakonadi_jobs Low-level access to the Akonadi server + +Accessing the Akonadi server is done using job classes derived from Akonadi::Job. The +communication channel with the server is provided by Akonadi::Session. + +To use server-side transactions, the following jobs are provided: + +- Akonadi::TransactionBeginJob +- Akonadi::TransactionCommitJob +- Akonadi::TransactionRollbackJob + +There also is Akonadi::TransactionSequence which can be used to automatically group +a set of jobs into a single transaction. + + +\section libakonadi_monitor Change notifications + +The Akonadi::Monitor class allows you to monitor specific resources, +collections and PIM items for changes. Akonadi::ChangeRecorder augments this +by providing a way to record and replay change notifications. + + +\section libakonadi_serializer PIM item serializer + +The class Akonadi::ItemSerializer is responsible for converting between the stored (binary) representation +of a PIM item and the objects used to handle these items provided by the corresponding libraries (kabc, kcal, etc.). + +Serializer plugins allow you to add support for new kinds of PIM items to Akonadi. +Akonadi::ItemSerializerPlugin can be used as a base class for such a plugin. + + +\section libakonadi_resource Agents and Resources + +Agents are independent processes that watch the Akonadi store for changes and react to them if necessary. +Example: The Nepomuk feeder (kdepim/runtime/agents/nepomukfeeder/) is an agent that watches Akonadi for +new emails, retrieves the new items, and feeds them to Nepomuk for indexing. + +The class Akonadi::AgentBase is the common base class for all agents. It provides commonly needed +functionality such as change monitoring and recording. + +Resources are a special kind of agents. They are used as the actual backend for whatever data the resource represents. +In this situation the akonadi server acts more like a proxy service. It caches data on behalf of its clients +(client here being the Resource), not permanently storing it. The Akonadi server forwards item retrieval requests to the +corresponding resource, if the item is not in the cache. +Example: The imap resource is responsible for storing and fetching emails from an imap server. + +Akonadi::ResourceBase is the base class for them. It provides the +necessary interfaces to the server as well as many convenience functions to make implementing +a new resource as easy as possible. Note that a collection contains items belonging to a single +resource, although there are plans in the future for 'virtual' collections which will contain +the results of a search query spanning multiple resources. + +A resource can support multiple mimetypes. There are two places where a resource can specify +mimetypes: in its desktop files, and in the content mimetypes field of +collections created by it. The ones in the desktop file are used for +filtering agent types, e.g. in the resource creation dialogs. The collection +content mimetypes specify what you can actually put into a collection, which +is not necessarily the same (e.g. the Kolab resource supports contacts and events, but not +in the same folder). + + + +\page libakonadi_integration Integration in your Application + +Akonadi::Control provides ways to ensure that the Akonadi server is running, to monitor its availability +and provide help on server-side errors. A more low-level interface to the Akonadi server is provided +by Akonadi::ServerManager. + +A set of standard actions is provided by Akonadi::StandardActionManager. These provide consistent +look and feel across applications. + + +This library provides classes for KDE applications to communicate with the Akonadi server. The most high-level interface to Akonadi is the Models and Views provided in this library. Ready to use models are provided for use with views to interact with a tree of collections, a list of items in a collection, or a combined tree of Collections and items. + +\subsection collections_and_items Collections and Items + +In the Akonadi concept, Items are individual objects of PIM data, e.g. emails, contacts, events, notes etc. The data in an item is stored in a typed payload. For example, if an Akonadi Item holds a contact, the contact is available as a KABC::Addressee: + +@code + if (item.hasPayload()) + { + KABC::Addressee addr = item.payload(); + // use addr in some way... + } +@endcode + +Additionally, an Item must have a mimetype which corresponds to the type of payload it holds. + +Collections are simply containers of Items. A Collection has a name and a list of mimetypes that it may contain. A collection may for example contain events if it can contain the mimetype 'text/calendar'. A Collection itself (as opposed to its contents) has a mimetype, which is the same for all Collections. A Collection which can itself contain Collections must be able to contain the Collection mimetype. + +@code + Collection col; + // This collection can contain events and nested collections. + col.setContentMimetypes( QStringList() << Collection::mimeType() << "text/calendar" ); +@endcode + +This system makes it simple to create PIM applications. For example, to create an application for viewing and editing events, you simply need to tell %Akonadi to retrieve all items matching the mimetype 'text/calendar'. + +\subsection convenience_mimetype_accessors Convenience Mimetype Accessors + +In order to avoid typos, improve readability, and to encapsulate the correct mimetypes for particular pim items, many of the standard classes have an accessor for the kind of mimetype the can handle. For example, you can use KMime::Message::mimeType() for emails, KABC::Addressee::mimeType() for contacts etc. It makes sense to define a similar static function in your own types. + +@code + col.setContentMimetypes( QStringList() << Collection::mimeType() << KABC::Addressee::mimeType() << KMime::Message::mimeType() ); +@endcode + + +

Models and Views

+%Akonadi models and views are a high level way to interact with the %Akonadi server. Most applications will use these classes. See the EntityTreeModel documentation for more information. + +Models provide an interface for viewing, deleting and moving Items and Collections. New Items can also be created by dropping data of the appropriate type on a model. Additionally, the models are updated automatically if another application changes the data or inserts or deletes items etc. + +%Akonadi provides several models for particular uses, e.g. the MailModel is used for emails and the ContactsModel is used for showing contacts. Additional specific models can be implemented using EntityTreeModel as a base class. + +A typical use of these would be to create a model and use proxy models to make the view show different parts of the model. For example, show a collection tree in on one side and show items in a selected collection in another view. + +@code + mailModel = new MailModel( session, monitor, this); + + collectionTree = new EntityMimeTypeFilterModel(this); + collectionTree->setSourceModel(mailModel); + // Filter out everything that is not a collection. + collectionTree->addMimeTypeInclusionFilter( Collection::mimeType() ); + collectionTree->setHeaderSet(EntityTreeModel::CollectionTreeHeaders); + + collectionView = new EntityTreeView(this); + collectionView->setModel(collectionTree); + + itemList = new EntityMimeTypeFilterModel(this); + itemList->setSourceModel(mailModel); + // Filter out collections + itemList->addMimeTypeExclusionFilter( Collection::mimeType() ); + itemList->setHeaderSet(EntityTreeModel::ItemListHeaders); + + itemView = new EntityTreeView(this); + itemView->setModel(itemList); +@endcode + +\image html dox/mailmodelapp.png "An email application using MailModel" + +The content of the model is determined by the configuration of the Monitor passed into it. The examples below show a use of the EntityTreeModel and some proxy models for a simple heirarchical note collection. As the model is generic, the configuration and proxy models will also work with any other mimetype. + +@code + +// Configure what should be shown in the model: +Monitor *monitor = new Monitor( this ); +monitor->fetchCollection( true ); +monitor->setItemFetchScope( scope ); +monitor->setCollectionMonitored( Collection::root() ); +monitor->setMimeTypeMonitored( MyEntity::mimeType() ); + +Session *session = new Session( QByteArray( "MyEmailApp-" ) + QByteArray::number( qrand() ), this ); +monitor->setSession(session); + +EntityTreeModel *entityTree = new Akonadi::EntityTreeModel(monitor, this); +@endcode + +\image html dox/entitytreemodel.png "A plain EntityTreeModel in a view" + +The EntityTreeModel can be further configured for certain behaviours such as fetching of collections and items. + +To create a model of only a collection tree and no items, set that in the model. This is just like CollectionModel: + +@code +entityTree->setItemPopulationStrategy(EntityTreeModel::NoItemPopulation); +@endcode + + +\image html dox/entitytreemodel-collections.png "A plain EntityTreeModel which does not fetch items." + +Or, create a model of only items and not child collections. This is just like ItemModel: + +@code +entityTree->setRootCollection(myCollection); +entityTree->setCollectionFetchStrategy(EntityTreeModel::FetchNoCollections); +@endcode + +Or, to create a model which includes items and first level collections: + +@code +entityTree->setCollectionFetchStrategy(EntityTreeModel::FetchFirstLevelCollections); +@endcode + +The items in the model can also be inserted lazily for performance reasons. The Collection tree is always built immediately. + +Additionally, a KDescendantsProxyModel may be used to alter how the items in the tree are presented. + +@code +// ... Create an entityTreeModel +KDescendantsProxyModel *descProxy = new KDescendantsProxyModel(this); +descProxy->setSourceModel(entityTree); +view->setModel(descProxy); +@endcode + +\image html dox/descendantentitiesproxymodel.png "A KDescendantsProxyModel wrapping an EntityTreeModel" + +KDescendantsProxyModel can also display ancestors of each Entity in the list. + +@code +// ... Create an entityTreeModel +KDescendantsProxyModel *descProxy = new KDescendantsProxyModel(this); +descProxy->setSourceModel(entityTree); + +// #### This is new +descProxy->setDisplayAncestorData(true, QString(" / ")); + +view->setModel(descProxy); + +@endcode + +\image html dox/descendantentitiesproxymodel-withansecnames.png "A DescendantEntitiesProxyModel with ancestor names." + +This proxy can be combined with a filter to for example remove collections. + +@code +// ... Create an entityTreeModel +DescendantEntitiesProxyModel *descProxy = new DescendantEntitiesProxyModel(this); +descProxy->setSourceModel(entityTree); + +// #### This is new. +EntityMimeTypeFilterModel *filterModel = new EntityMimeTypeFilterModel(this); +filterModel->setSourceModel(descProxy); +filterModel->setExclusionFilter(QStringList() << Collection::mimeType()); + +view->setModel(filterModel); +@endcode + +\image html dox/descendantentitiesproxymodel-colfilter.png "An EntityMimeTypeFilterModel wrapping a DescendantEntitiesProxyModel wrapping an EntityTreeModel" + +It is also possible to show the root item as part of the selectable model: + +@code + +entityTree->setIncludeRootCollection(true); + +@endcode + +\image html dox/entitytreemodel-showroot.png "An EntityTreeModel showing Collection::root" + +By default the displayed name of the root collection is '[*]', because it doesn't require i18n, and is generic. It can be changed too. + +@code +entityTree->setIncludeRootCollection(true); +entityTree->setRootCollectionDisplayName(i18nc("Name of top level for all collections in the application", "[All]")) +@endcode + +\image html dox/entitytreemodel-showrootwithname.png "An EntityTreeModel showing Collection::root with an application specific name." + +These can of course be combined to create an application which uses one EntityTreeModel along with several proxies and views. + +@code +// ... create an EntityTreeModel. +EntityMimeTypeFilterModel *collectionTree = new EntityMimeTypeFilterModel(this); +collectionTree->setSourceModel(entityTree); +// Filter to include collections only: +collectionTree->setInclusionFilter(QStringList() << Collection::mimeType()); +EntityTreeView *treeView = new EntityTreeView(this); +treeView->setModel(collectionTree); + +EntityMimeTypeFilterModel *itemList = new EntityMimeTypeFilterModel(this); +itemList->setSourceModel(entityTree); +// Filter *out* collections +itemList->setExclusionFilter(QStringList() << Collection::mimeType()); +EntityTreeView *listView = new EntityTreeView(this); +listView->setModel(itemList); +@endcode + + +\image html dox/treeandlistapp.png "A single EntityTreeModel with several views and proxies." + +Or to also show items of child collections in the list: + +@code + // ... Create an entityTreeModel + collectionTree = new EntityMimeTypeFilterModel(this); + collectionTree->setSourceModel(entityTree); + + // Include only collections in this proxy model. + collectionTree->addMimeTypeInclusionFilter( Collection::mimeType() ); + + treeview->setModel(collectionTree); + + descendedList = new DescendantEntitiesProxyModel(this); + descendedList->setSourceModel(entityTree); + + itemList = new EntityMimeTypeFilterModel(this); + itemList->setSourceModel(descendedList); + + // Exclude collections from the list view. + itemList->addMimeTypeExclusionFilter( Collection::mimeType() ); + + listView = new EntityTreeView(this); + listView->setModel(itemList); +@endcode + +\image html dox/treeandlistappwithdesclist.png "Showing descendants of all Collections in the list" + +Note that it is important in this case to use the DescendantEntitesProxyModel before the EntityMimeTypeFilterModel. Otherwise, by filtering out the collections first, you would also be filtering out their child items. + +A SelectionProxyModel can be used to simplify managing selection in one view through multiple proxy models to a representation in another view. The selectionModel of the initial view is used to create a proxied model which includes only the selected indexes and their children. + +@code + // ... Create an entityTreeModel + collectionTree = new EntityMimeTypeFilterModel(this); + collectionTree->setSourceModel(entityTree); + + // Include only collections in this proxy model. + collectionTree->addMimeTypeInclusionFilter( Collection::mimeType() ); + + treeview->setModel(collectionTree); + + // SelectionProxyModel can handle complex selections: + treeview->setSelectionMode(QAbstractItemView::ExtendedSelection); + + SelectionProxyModel *selProxy = new SelectionProxyModel(treeview->selectionModel(), this); + selProxy->setSourceModel(entityTree); + + EntityTreeView *selView = new EntityTreeView(splitter); + selView->setModel(selProxy); +@endcode + +\image html dox/selectionproxymodelsimpleselection.png "A Selection in one view creating a model for use with another view." + +The SelectionProxyModel can handle complex selections. + +\image html dox/selectionproxymodelmultipleselection.png "Non-contiguous selection creating a new simple model in a second view." + +If an index and one or more of its descendants are selected, only the top-most selected index (including all of its descendants) are included in the proxy model. (Though this is configurable. See below) + +\image html dox/selectionproxymodelmultipleselection-withdescendant.png "Selecting an item and its descendant." + +SelectionProxyModel allows configuration using the methods setStartWithChildTrees, setOmitDescendants, setIncludeAllSelected. See testapp/proxymodeltestapp to try out the 5 valid configurations. + +Obviously, the SelectionProxyModel may be used in a view, or further processed with other proxy models. See the example_contacts application for example which uses a further DescendantEntitiesProxyModel and EntityMimeTypeFilterModel on top of a SelectionProxyModel. + +The SelectionProxyModel orders its items in the same top-to-bottom order as they appear in the source model. Note that this order may be different to the order in the selection model if there is a QSortFilterProxyModel between the selection and the source model. + +\image html dox/selectionproxymodel-ordered.png "Ordered items in the SelectionProxyModel" + + +

Jobs and Monitors

+ +The lower level way to interact with Akonadi is to use Jobs and Monitors (This is what models use internally). Jobs are used to make changes to akonadi, and in some cases (e.g., a fetch job) emit a signal with data resulting from the job. A Monitor reports changes made to the data stored in Akonadi (e.g., creating, updating, deleting or moving an item or collection ) via signals. + +Typically, an application will configure a monitor to report changes to a particular Collection, mimetype or resource, and then connect to the signals it emits. + +Most applications will use some of the low level api for actions unrelated to a model-tree view, such as creating new items and collections. + +

Tricky details

+ +

Change Conflicts

+It is possible that while an application is editing an item, that item gets updated in akonadi. Akonadi will notify the application that that item has changed via a Monitor signal. It is the responsibility of the application to handle the conflict by for example offering the user a dialog to resolve it. Alternatively, the application could ignore the dataChanged signal for that item, and will get another chance to resolve the conflict when trying to save the result back to akonadi. In that case, the ItemModifyJob will fail and report that the revision number of the item on the server differs from its revision number as reported by the job. Again, it is up to the application to handle this case. + +This is something that every application using akonadi will have to handle. + +

Using Entity::Id as an identifier

+ +Items and Collections have a id() member which is a unique identifier used by akonadi. It can be useful to use the id() as an identifier when storing Collections or Items. + +However, as an item and a collection can have the same id(), if you need to store both Collections and Items together by a simple identifier, conflicts can occur. + +@code + QString getRemoteIdById( Entity::Id id ) + { + // Note: + // m_items is QHash + // m_collections is QHash + if ( m_items.contains( id ) ) + { + // Oops, we could accidentally match a collection here. + return m_items.value( id ).remoteId(); + } else if ( m_collections.contains( id ) ) + { + return m_collections.value( id ).remoteId(); + } + return QString(); + } +@endcode + +In this case, it makes more sense to use a normal qint64 as the internal identifier, and use the sign bit to determine if the identifier refers to a Collection or an Item. This is done in the implementation of EntityTreeModel to tell Collections and Items apart. + +@code + QString getRemoteIdByInternalIdentifier( qint64 internalIdentifier ) + { + // Note: + // m_items is QHash + // m_collections is QHash + + // If the id is negative, it refers to an Item + // Otherwise it refers to a Collection. + + if ( internalIdentifier < 0 ) + { + // Reverse the sign of the id before using it. + return m_items.value( internalIdentifier * -1 ).remoteId(); + } else + { + return m_collections.value( internalIdentifier ).remoteId(); + } + } +@endcode + +

Unordered Lists

+Collection and Item both provide a ::List to represent groups of objects. However the objects in the list are usually not ordered in any particular way, even though the API provides methods to work with an ordered list. It makes more sense to think of it as a Set instead of a list in most cases. + +For example, when using an ItemFetchJob to fetch the items in a collection, the items could be in any order when returned from the job. The order that a Monitor emits notices of changes is also indeterminate. By using a Transaction however, it is sometimes possible to retrieve objects in order. Additionally, using s constructor overload in the CollectionFetchJob it is possible to retrieve collections in a particular order. + +@code + Collection::List getCollections(QList idsToGet) + { + Collection::List getList; + foreach ( Collection::Id id, idsToGet ) { + getList << Collection(id); + } + CollectionFetchJob *job = CollectionFetchJob(getList); + if (job->exec()) + { + // job->collections() is in the same order as the ids in idsToGet. + } + } + +@endcode + +

Resources

+The KDEPIM module includes resources for handling many types of PIM data, such as imap email, vcard files and vcard directories, ical event files etc. These cover many of the sources for your PIM data, but in the case that you need to use data from another source (for example a website providing a contacts storage service and an api), you simply have to write a new resource. + +http://techbase.kde.org/Development/Tutorials/Akonadi/Resources + +

Serializers

+Serializers provide the functionality of converting raw data, for example from a file, to a strongly typed object of PIM data. For example, the addressee serializer reads data from a file and creates a KABC::Addressee object. + +New serializers can also easily be written if the data you are dealing with is not one of the standard PIM data types. + +

Implementation details

+ +

Updating Akonadi Models

+ +@note The details here are only relevant if you are writing a new view using EntityTreeModel, or writing a new model. + +Because communication with akonadi happens asynchronously, and the models only hold a cached copy of the data on the akonadi server, some typical behaviours of models are not followed by Akonadi models. + +For example, when setting data on a model via a view, most models syncronously update their internal store and notify akonadi to update its view of the data by returning true. + +@dot +digraph utmg { + rankdir = LR; + { node [label="",style=invis, height=0, width=0 ]; + V_Set_Data; V_Result; V_Data_Changed; + M_Set_Data; M_Result; + } + { node [shape=box, fillcolor=lightyellow, style=filled,fontsize=26]; + View [label=":View"]; Model [label=":Model"]; + } + { node [style=invis]; + View_EOL; Model_EOL; + } + { + V_Set_Data -> M_Set_Data [label="Set Data"]; + M_Result -> V_Result [label="Success",arrowhead="vee", style="dashed"]; + V_Result -> V_Data_Changed [label="Update View \n[ Success = True ]"]; + } + + // Dashed Vertical lines for object lifetimes. + edge [style=dashed, arrowhead=none]; + { rank = same; View -> V_Set_Data -> V_Result -> V_Data_Changed -> View_EOL; } + { rank = same; Model -> M_Set_Data -> M_Result -> Model_EOL; } + + // Make sure top nodes are in a straight line. + { View -> Model [style=invis]; } + // And the bottom nodes. + { View_EOL -> Model_EOL [style=invis]; } +} +@enddot + +Akonadi models only cache data from the akonadi server. To update data on an Akonadi::Entity stored in a model, the model makes a request to the akonadi server to update the model data. At that point the data cached internally in the model is not updated, so false is always returned from setData. If the request to update data on the akonadi server is successful, an Akonadi::Monitor notifies the model that the data on that item has changed. The model then updates its internal data store and notifies the view that the data has changed. The details of how the Monitor communicates with akonadi are omitted for clarity. + +@dot +digraph utmg { + rankdir = LR; + { node [label="",style=invis, height=0, width=0 ]; + ETV_Set_Data; ETV_Result; ETV_Data_Changed; + ETM_Set_Data; ETM_Result; ETM_Changed; + M_Dummy_1; M_Changed; + AS_Modify; + } + { node [shape=box, fillcolor=lightyellow, style=filled,fontsize=26]; + EntityTreeView [label=":View"]; EntityTreeModel [label=":Model"]; Monitor [label=":Monitor"]; Akonadi_Server [label=":Akonadi"]; + } + { node [style=invis]; + EntityTreeView_EOL; EntityTreeModel_EOL; Monitor_EOL; Akonadi_Server_EOL; + } + { + { rank = same; ETV_Set_Data -> ETM_Set_Data [label="Set Data"]; } + { rank = same; ETM_Result -> ETV_Result [label="False",arrowhead="vee", style="dashed"]; } + { rank = same; ETM_Result -> M_Dummy_1 [style=invis]; } + { rank = same; ETM_Set_Data -> AS_Modify [arrowhead="vee",taillabel="Modify Item", labeldistance=14.0, labelangle=10]; } + { rank = same; M_Changed -> ETM_Changed [arrowhead="vee",label="Item Changed"]; } + { rank = same; ETM_Changed -> ETV_Data_Changed [arrowhead="vee",label="Update View"]; } + } + + // Dashed Vertical lines for object lifetimes. + edge [style=dashed, arrowhead=none]; + { rank = same; EntityTreeView -> ETV_Set_Data -> ETV_Result -> ETV_Data_Changed -> EntityTreeView_EOL; } + { rank = same; EntityTreeModel -> ETM_Set_Data -> ETM_Result -> ETM_Changed -> EntityTreeModel_EOL; } + { rank = same; Monitor -> M_Dummy_1 -> M_Changed -> Monitor_EOL; } + { rank = same; Akonadi_Server -> AS_Modify -> Akonadi_Server_EOL; } + + // Make sure top nodes are in a straight line. + { EntityTreeView -> EntityTreeModel -> Monitor -> Akonadi_Server [style=invis]; } + // And the bottom nodes. + { EntityTreeView_EOL -> EntityTreeModel_EOL -> Monitor_EOL -> Akonadi_Server_EOL [style=invis]; } +} +@enddot + +Similarly, in drag and drop operations, most models would update an internal data store and return true from dropMimeData if the drop is successful. + +@dot +digraph utmg { + rankdir = LR; + { node [label="",style=invis, height=0, width=0 ]; + Left_Phantom; Left_Phantom_DropEvent; + V_DropEvent; V_Result; V_Data_Changed; V_Dummy_1; + M_DropMimeData; M_Result; + } + { node [shape=box, fillcolor=lightyellow, style=filled,fontsize=26]; + View [label=":View"]; Model [label=":Model"]; + } + { node [style=invis]; + Left_Phantom_EOL; + View_EOL; Model_EOL; + } + { + Left_Phantom_DropEvent -> V_DropEvent [label="DropEvent"]; + V_DropEvent -> M_DropMimeData [label="DropMimeData"]; + M_Result -> V_Result [label="Success",arrowhead="vee", style="dashed"]; + V_Result -> V_Data_Changed [label="Update View \n[Success = True]"]; + } + + // Dashed Vertical lines for object lifetimes. + edge [style=dashed, arrowhead=none]; + { rank = same; View -> V_DropEvent -> V_Result -> V_Dummy_1 -> V_Data_Changed -> View_EOL; } + { rank = same; Model -> M_DropMimeData -> M_Result -> Model_EOL; } + + //Phantom line + { rank= same; Left_Phantom -> Left_Phantom_DropEvent -> Left_Phantom_EOL [style=invis]; } + + // Make sure top nodes are in a straight line. + { Left_Phantom -> View -> Model [style=invis]; } + // And the bottom nodes. + { Left_Phantom_EOL -> View_EOL -> Model_EOL [style=invis]; } +} + +@enddot + +Akonadi models, for the same reason as above, always return false from dropMimeData. At the same time a suitable request is sent to the akonadi server to make the changes resulting from the drop (for example, moving or copying an entity, or adding a new entity to a collection etc). If that request is successful, the Akonadi::Monitor notifies the model that the data is changed and the model updates its internal store and notifies the view that the model data is changed. + +@dot + +digraph utmg { + rankdir = LR; + { node [label="",style=invis, height=0, width=0 ]; + Left_Phantom; Left_Phantom_DropEvent; + ETV_DropEvent; ETV_Result; ETV_Data_Changed; + ETM_DropMimeData; ETM_Result; ETM_Changed; + M_Dummy_1; M_Changed; + AS_Modify; + } + { node [shape=box, fillcolor=lightyellow, style=filled,fontsize=26]; + EntityTreeView [label=":View"]; + EntityTreeModel [label=":Model"]; + Monitor [label=":Monitor"]; + Akonadi_Server [label=":Akonadi"]; + } + { node [style=invis]; + Left_Phantom_EOL; + EntityTreeView_EOL; EntityTreeModel_EOL; Monitor_EOL; Akonadi_Server_EOL; + } + + { + { rank = same; Left_Phantom_DropEvent -> ETV_DropEvent [label="DropEvent"]; } + { rank = same; ETV_DropEvent -> ETM_DropMimeData [label="Drop MimeData"]; } + { rank = same; ETM_Result -> ETV_Result [label="False",arrowhead="vee", style="dashed"]; } + { rank = same; ETM_Result -> M_Dummy_1 [style=invis]; } + { rank = same; ETM_DropMimeData -> AS_Modify [arrowhead="vee",taillabel="Action", labeldistance=14.0, labelangle=10]; } + { rank = same; M_Changed -> ETM_Changed [arrowhead="vee",label="Item Changed"]; } + { rank = same; ETM_Changed -> ETV_Data_Changed [arrowhead="vee",label="Update View"]; } + } + + // Dashed Vertical lines for object lifetimes. + edge [style=dashed, arrowhead=none]; + { rank = same; EntityTreeView -> ETV_DropEvent -> ETV_Result -> ETV_Data_Changed -> EntityTreeView_EOL; } + { rank = same; EntityTreeModel -> ETM_DropMimeData -> ETM_Result -> ETM_Changed -> EntityTreeModel_EOL; } + { rank = same; Monitor -> M_Dummy_1 -> M_Changed -> Monitor_EOL; } + { rank = same; Akonadi_Server -> AS_Modify -> Akonadi_Server_EOL; } + + //Phantom line + { rank= same; Left_Phantom -> Left_Phantom_DropEvent -> Left_Phantom_EOL [style=invis]; } + + // Make sure top nodes are in a straight line. + { Left_Phantom -> EntityTreeView -> EntityTreeModel -> Monitor -> Akonadi_Server [style=invis]; } + // And the bottom nodes. + { Left_Phantom_EOL -> EntityTreeView_EOL -> EntityTreeModel_EOL -> Monitor_EOL -> Akonadi_Server_EOL [style=invis]; } +} + + +@enddot + +@section lazy-model-population Lazy Model Population + +@note This page is not part of the %Akonadi API. It is provided as internal documentation for %Akonadi maintainers. It was originally a blog post here: http://steveire.wordpress.com/2009/10/06/cache-invalidation-in-akonadi-models/ +@internal + +If using EntityTreeModel::LazyPopulation with your model, items will be fetched into the model when the collection they are a part of is selected. This ensures that the model is as sparsely populated as possible for performance reasons. As a consequence however, it is necessary to purge unused items from the model too. This is handled automatically when using an Akonadi::SelectionProxyModel. + +The problem is knowing when to invalidate the cache. If no application is currently showing the contents of a Collection, there is no need for the Items in that Collection to be fetched, cached and kept up to date in the model. The effect we would like to achieve is to purge the Items in a Collection when those items are no longer shown anywhere in the application. Generally, that will mean that the Collection is not selected. + +In Qt Model-View, the application data is stored in a model, and there may be one or more views attached to it displaying its contents. The model doesn’t have any knowledge of the views, and so it can’t know whether any particular Collection is selected, and purge its Iitems. + +To solve this, we use the KSelectionProxyModel, which already has a lot of code for managing the selection a user makes in a view. A subclass, Akonadi::SelectionProxyModel implements a reference counting system which increments the refcount of a Collection when it is selected, and decrements it when deselected. If the reference count of a Collection goes down to zero, it is put in a queue to be purged. It is not purged immediately, but queued because if the user is clicking around several Collection in a short time, we don’t want to purge the Collections each click or we’d lose the benefit of the caching. Like other similar optimisation techniques, this violates the object-oriented principle of modularity, but it is worth it for the benefit it brings. The effect can be seen in the akonadiconsole tool by not filtering out the items from the tree. + +In the screenshots below I removed the filtering out of Items in the tree so that the fetching/purging can be seen. In real applications, the Items in the tree on the left would not be visible. + +@image html dox/bufferedcaching1.png "When a Collection is clicked, its Items are put into the model. The rest of the Collections have no items." + +@image html dox/bufferedcaching2.png "The Inbox Collection is selected, so its items are fetched. Personal Contacts is no longer selected, so it is put into a queue to be purged." + +@image html dox/bufferedcaching3.png "Select another Collection and its items are fetched too." + +@image html dox/bufferedcaching4.png "Another Collection is selected, pushing the Personal contacts out of the queue and purging them" + +@image html dox/bufferedcaching6.png "If the Collection is selected again, its Items are refetched" + +For this example, I used a queue length of just two Collections, so that if a Collection was deselected two clicks ago, it will be purged. In real applications, a longer queue length will be used, but it’s harder to illustrate in screenshots. Another unrealistic part of this demo is that this feature will like be used in applications like KMail where Collections can contain tens of thousands of Items and fetching them is an expensive operation. + +This feature should be totally invisible to users and even developers using Akonadi, but it should offset the main disadvantage of using a cache of Items in the EntityTreeModel. + +*/ + + +/** +\page akonadi_history Historical Background + +\section akonadi_history_general General + +During the last 5 years, after the release of KDE 3.0, the requirements of our users +have constantly increased. While it was sufficient that our PIM solution was able to handle 100 contacts, +300 events and maybe 1000 mails in 2001, nowadays users expect the software to be able to +handle a multiple of that. Over the years, the KDE PIM developers tried to catch up with the new +requirements; however, since KDE 3.x had to stay binary compatible, they were limited in their +efforts. + +With the new major release KDE 4.0 it's possible to completely redesign the PIM libraries from +the ground up and use new concepts to face the requirements of 2006 and beyond. + +After some discussion at the annual KDE PIM meeting in Osnabrück in January 2006, the PIM developers +came to the conclusion that a service is needed which acts as a local cache on the user's desktop +and provides search facilities. The name Akonadi comes from a divinity from Ghana and was chosen since +all other nice names were already used by other projects on the Internet ;) + +\section akonadi_history_problems Problems with the implementation of KDE 3.x + +Before digging into the internals of Akonadi, we want to take a look at the implementation of the +old KDE PIM libraries to understand the problems and conceptual shortcomings. + +The main PIM libraries libkabc (contacts) and libkcal (events) where designed at a time when the +address book and calendar were files on the local file system, so there was no reason to think +about access time and mode. The libraries accessed the files synchronously and loaded all data of the +file into memory to be able to perform search queries on the data set. It worked well for local files, +but over time plug-ins for loading data from groupware servers were written, so the synchronous access blocked +applications which used libkabc/libkcal, and loading all 2000 contacts from a server is not only +time consuming but also needs a lot of memory to store them locally. The KDE PIM developers tried to +address the first issue by adding an asynchronous API, but it was not well implemented and was difficult to use. +In the end, the design decisions caused the following problems: + +\li Bad Performance +\li High Memory Consumption + +Another important but missing thing in the libraries was support for notifications and locking. +The former was partly implemented (at least reflected by the API) but only implemented in the local +file plug-in, so it was in practice unusable. The latter was also partly implemented but never really tested and +lead to deadlocks sometimes, so the following problems appeared as well: + +\li Missing Notifications +\li Missing Locking + +The main aim of Akonadi is to solve these issues and make use of the goodies which the new design brings. +*/ + + +/** + +\defgroup AkonadiMacros Akonadi Macros + +*/ + +// DOXYGEN_EXCLUDE = src/qsqlite src/akonadicontrol src/akonadictl autotests tests +// DOXYGEN_PROJECTNAME=Akonadi +// DOXYGEN_PROJECTVERSION=5.1.52 +// DOXYGEN_REFERENCES = kdecore kdeui +// DOXYGEN_SET_RECURSIVE = YES +// vim:ts=4:sw=4:expandtab:filetype=doxygen diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..c06a320 --- /dev/null +++ b/NEWS @@ -0,0 +1,529 @@ +1.13.0 10-August-2014 +---------------------------------------------- +- Fixed virtual collections statistics +- Fixed tag RID fetch +- Fixed HRID-based fetches +- Fixed race condition in StorageDebugger +- Use FindBacktrace.cmake from CMake 3.0 instead of our own detection + +1.12.90 07-July-2014 +---------------------------------------------- +- MERGE command for faster synchronization +- Optimizations in various commands handlers +- SELECT command is obsolete now +- Performance and concurrency improvements in QSQLITE3 driver +- Introduced Collection sync preferences as an improvement over the IMAP-based subscription model +- Disable filesystem copy-on-write for DB files when running on Btrfs +- Introduced direct streaming of external parts +- Fixed SearchManager DBus interface not being registered to DBus +- Fixed handling of tags in AK-APPEND and MERGE commands +- Various fixes in virtual collections handling + +1.12.1 07-April-2014 +---------------------------------------------- +- Fixed deadlock in SearchManager +- Fixed notification emission when appending items +- Fixed ItemRetriever ignoring changeSince argument +- Fixed X-AKAPPEND command response +- Fixed RID-based FETCH +- Fixed data loss in case of long-lasting copy or move operations + +1.12.0 25-March-2014 +---------------------------------------------- +- Improved 'akonadictl status' command output +- Fixed indexing of items in collections with short cache expiration +- Fixed building Akonadi in subdirectory +- Fixed deadlock in SearchManager +- Fixed runtime warnings + +1.11.90 19-March-2014 +---------------------------------------------- +- Fixed collection scheduling +- Fixed indexing of expired items from local resources +- Fixed database schema update with PostgreSQL +- Fixes in searching and search updates + +1.11.80 28-February-2014 +---------------------------------------------- +- Server-search support +- Search plugins support +- Tags support +- Fixes and improvements in search +- Fixes in protocol parser +- Fixed inter-resource moves +- Fixed .desktop files parsing +- Optimized collections tasks scheduling +- Optimized flags handling +- Optimized appending new items via AK-APPEND +- Handle database transactions deadlocks and timeouts +- Improved PostgreSQL support +- Soprano is now an optional dependency +- Removed MySQL Embedded support + +1.11.0 28-November-2013 +---------------------------------------------- +- fix joined UPDATE queries failing with SQLite + +1.10.80 05-November-2013 +---------------------------------------------- +- Servser-side notification filtering +- GID support +- Export custom agent properties to clients +- Faster Akonadi shutdown +- Improved and faster database schema check on start +- Enabled C++11 support +- Optimize some SQL queries +- Store only relative paths to external payload files in database + +1.10.3 04-October-2013 +---------------------------------------------- +- Fix support for latest PostgreSQL +- Check MySQL version at runtime, require at least 5.1 +- Fix crash when destroying DataStore with backends other than MySQL +- Fix problem with too long socket paths +- Send dummy queries to MySQL to keep the connection alive +- Fix crash when no flags are changed + +1.10.2 23-July-2013 +---------------------------------------------- +- Fix PostgreSQL support (once more) + +1.10.1 22-July-2013 +---------------------------------------------- +- Fix PostgreSQL support +- Optimize appending flags to items +- Introduce CHANGEDSINCE parameter to FETCH command + +1.10.0 09-July-2013 +---------------------------------------------- +- Memory optimizations +- Fix a runtime error on Windows + +1.9.80 10-June-2013 +---------------------------------------------- +- Update item access time less often. +- Don't try to start akonadiserver if mysqld is not installed +- Allow to fetch available items even if there are errors in some of the items. +- Properly restrict the external part removal to the deleted collection. +- Support checking the cache for payloads in the FETCH command. +- Add infrastructure to track client capabilities. +- Allow to disable the cache verification on retrieval. +- fsck: move orphaned pim items to lost+found, delete orphaned pim item flags. +- Introduce NotificationMessageV2 that supports batch operations on set of entities. +- Fix build with Boost >= 1.53. +- Fix a runtime issue with MySQL >= 5.6 (MySQL >= 5.1.3 is now the minimum version). + +1.9.2 05-May-2013 +--------------------------------------------- +- Add option to FETCH to ignore external retrieval failures. +- Properly restrict external payload removal. +- Add buildsystem option to choose between Qt4 and Qt5. + +1.9.1 02-March-2013 +--------------------------------------------- +- Disable query cache for Sqlite. +- Handle missing mysqld better. +- Ignore my.cnf settings when using the internal MySQL server. + +1.9.0 23-December-2012 +--------------------------------------------- +- Respect collection cache policy refresh interval for collection tree sync. +- Fix initialization of PostgreSQL database. +- Correctly count items flags in virtual collections. +- Notify parent virtual collections about item changes. +- Require CMake >= 2.8.8. +- Remove dependency to Automoc4. +- Support Qt 5. + +1.8.80 12-November-2012 +--------------------------------------------- +- Recover from lost external payload files. +- Improve the virtual collections handling. +- Notify clients about database schema updates. +- Reduce item access time updates. +- Make use of referential integrity if supported by the database backend. +- Add prepared query cache. +- Many code and queries optimizations. + +1.8.1 14-October-2012 +--------------------------------------------- +- Fix payload loss on some move/copy scenarios. +- Improve error reporting for failed item retrievals. + +1.8.0 25-July-2012 +--------------------------------------------- +- Fix deadlock in ad-hoc Nepomuk searches. + +1.7.95 11-July-2012 +--------------------------------------------- +- Fix Nepomuk queries getting stuck if Nepomuk service crashes. +- Fix unecessary remote retrieval of already cached item parts. +- Reset RID/RREV during cross-resource collection moves. +- Increase timeout for remote item retrieval. + +1.7.90 08-June-2012 +--------------------------------------------- +- Fix handling of large SPARQL queries. +- Support cleanup of orphaned resources in the consistency checker. +- Support compilation with Clang. + +1.7.2 31-March-2012 +--------------------------------------------- +- Fix and optimize searching via Nepomuk. + +1.7.1 03-March-2012 +--------------------------------------------- +- Don't truncate SPARQL queries in virtual collections. +- Optimize change notifications for deleted collection attributes. +- Fix possible data loss during item copy/move operations. + +1.7.0 23-January-2012 +--------------------------------------------- +- Fix search result retrieval from Nepomuk. + +1.6.90 20-December-2011 +--------------------------------------------- +- Support for PostgreSQL >= 9. +- Improve RFC 3501 compatibility in LOGIN and non-silent SELECT commands. +- Add support for running multiple instance concurrently in the same user session. +- Update agent interface to include collectionTreeSynchronized signal. +- Add consistency checker system. +- Add support for database vacuuming. +- Various optimizations to reduce the number of SQL queries. + +1.6.2 03-October-2011 +--------------------------------------------- +- Do not update item revision if only the RID or RREV changed. +- Fix usage of wrong ids for part filenames. +- Only set item dirty flag if the payload changed. +- Only drop content mimetype for unsubscribed collections in LIST/LSUB. + +1.6.1 15-September-2011 +--------------------------------------------- +- Fix crash on agent launcher exit. +- Fix valgrind-ing agents running in the agent launcher. +- Fix restarting of agents in broken state. +- Fix pipe naming on multi-user Windows systems. +- Raise MySQL timeout. + +1.6.0 10-July-2011 +--------------------------------------------- +- Enable external payload storage unconditionally. +- Treat single UID/RID fetches as error if the result set is empty. + +1.5.80 21-May-2011 +--------------------------------------------- +- WinCE database performance improvements. +- Include destination resource in move notifications. +- Fix crash in protocol parser. +- Fix possible race on accessing table caches. +- Use QStringBuilder if available. +- Improved notification message API. + +1.5.3 07-May-2011 +--------------------------------------------- +- Fix crash when copying collections into themselves. + +1.5.2 05-April-2011 +--------------------------------------------- +- Fix XdgBaseDirs reporting duplicated paths. +- Use correct database name when using internal MySQL. + +1.5.1 28-February-2011 +--------------------------------------------- +- Unbreak searching with Nepomuk 4.6. + +1.5.0 22-January-2011 +--------------------------------------------- +- Fix Boost related build issues on Windows. +- Hide akonadi_agent_launcher from Mac OS X dock. + +1.4.95 07-January-2011 +--------------------------------------------- +- Optimize notification compression. +- Consider ignore flag when calculating collection statistics. +- Fix item payload size calculation. +- Improved FETCH response order heuristic. +- Fix Strigi-based persistent search folders. +- Fix error propagation in FETCH command handler. + +1.4.90 20-December-2010 +--------------------------------------------- +- Set agent status for crashed instances. +- Allow to restart crashed agent instances. +- Automatically recover from loss of the resource table. +- Allow to specify the query language in persistent search commands. +- Fix leak of notification sources. + +1.4.85 18-December-2010 +--------------------------------------------- +- Fix agent server startup race. +- Allow to globally enable/disable the agent server. +- Fix autostart of agents running in the agent server. +- Fix agent configuration when running in the agent server. +- Fix agent server shutdown crash. +- Put sockets into /tmp to support AFS/NFS home directories. +- Fix access rights on persistent search folders. +- Add support for sub-collection tree syncs in resource interface. + +1.4.80 21-November-2010 +--------------------------------------------- +- Experimental support for MeeGo. +- Return changed revision numbers in STORE response. +- Fix Nepomuk searches mixing up items and email attachments. +- Experimental Strigi search backend. +- Compensate for Nepomuk D-Bus API breakage. +- Fix parsing of serialization format version. +- Optimize collection statistics queries. +- Optimize protocol output generation. +- Optimize protocol parsing. +- Build-time configurable default database backend. +- Fix ancestor chain quoting. +- Fix finding of components on Windows in install location. +- New subscription interface for change notifications. +- Support for in-process agents and agent server. +- Support for Sqlite. +- Experimental support for ODBC-based database backends. +- Support Windows CE. + +1.4.1 22-October-2010 +--------------------------------------------- +- Improve range query performance. +- Fix MySQL database upgrade happening too early. +- Fix MySQL database upgrade setting wrong priviledges. +- Fix non-index access slowing down server startup. +- ASAP parser performance optimizations +- Respect SocketDirectory setting also for database sockets. +- Allow $USER placeholder in SocketDirectory setting. +- Fix ASAP parser failing on non-zero serialization format versions. + +1.4.0 31-July-2010 +--------------------------------------------- +- Add change notification for collection subscription state changes. +- Enable filesystem payload store by default. +- Fix unicode folder name encoding regression. + +1.3.90 04-July-2010 +--------------------------------------------- +- Reset RIDs on inter-resource moves. +- Optimize disk space usage with internal MySQL. +- Improve error reporting of the Akonadi remote debugging server. +- Fix moving collections into the collection root. +- Report PostgreSQL database errors in english independent of locale settings. +- Fix unicode collection name encoding. +- Optimize cache pruning with filesystem payload store. +- Fix automatic migration between database and filesystem payload store. + +1.3.85 09-June-2010 +--------------------------------------------- +- Avoid unneeded full resource sync when using sync-on-demand cache policies. +- Fix crash when using D-Bus session bus in a secondary thread. +- Reduce emission of unneccessary change notifications. +- Fix empty filename use in fs backend. + +1.3.80 27-May-2010 +--------------------------------------------- +- Fix unicode collection name encoding. +- Support HRID-based FETCH commands. +- Fix Nepomuk-based persistent searches when Nepomuk was not running during Akonadi startup. +- Fix compilation on Windows CE. +- Optimize item retrieval queries. +- Support modification of existing persistent searches. +- Support different query languages for persistent searches. +- Fix PostgreSQL shutdown. +- Add initial support for Sqlite. +- Fix premature command abortion. +- Fix parsing of cascaded lists. +- Support for mysql_update_db. +- Support for mysql_install_db. +- Improved protocol tracing for akonadiconsole. +- Support MySQL backend on Maemo. +- Allow RID changes only to the owning resource. +- Add Akonadi remote debugging server. +- Add support for marking chaced payloads as invalid. +- Add support for remove revision property. +- Fix MySQL connection loss after 8 hours of inactivity. +- Fix D-Bus race on server startup. +- Fix internal MySQL on Windows. +- Fix config and data file location on Windows. +- Fix PostgreSQL startup when using internal server. +- Refactor database configuration abstraction. + +1.3.1 09-February-2010 +--------------------------------------------- +- Fix D-Bus connection leak in Nepomuk search backend. +- Disable slow query logging by default for internal MySQL. + +1.3.0 20-January-2010 +--------------------------------------------- +- Work around D-Bus bug that could cause SEARCH to hang. + +1.2.90 06-January-2010 +--------------------------------------------- +- Fix change notifications for search results. +- Fix database creation with PostgreSQL. +- Fix copying of item flags. +- Fix internal MySQL shutdown. +- Support PostgreSQL in internal mode. +- Fix table name case mismatch. + +1.2.80 01-December-2009 +--------------------------------------------- +- Support for collection content type filtering as part of LIST. +- Adapt to Nepomuk query service changes. +- Experimental support for PostgreSQL. +- Support for preprocessor agents. +- Support for distributed searching. +- Support for agents creating virtual collections. +- Protocol parser fixes for non-Linux/non-KDE clients. +- Support for single-shot searches using the Nepomuk query service. +- Support HRID-based LIST operations. +- Support RID-based MOVE, COLMOVE, LINK and UNLINK opertions. +- Respect cache-only retrieval also regarding on-demand syncing. +- Add configuration accepted/rejected signals to the agent interface. +- Fix change notification compression when using modified parts sets. +- Use one retrieval pipeline per resource. +- Reduce unecessary change notification on flag changes. +- Fix RID quoting. +- Fix resource creating race for autostarted agents. +- Create new database also when using external db servers. +- Return the created result collection when creating a persistent search. + +1.2.1 28-August-2009 +--------------------------------------------- +- Fix item creation with RID's containing a ']'. +- Fix ASAP parser not reading the entire command. + +1.2.0 28-June-2009 +--------------------------------------------- +- Fix attribute joining in collection list results. +- Buildsystem fixes for Mac OS. +- Do not show a console window for akonadi_control on Windows. + +1.1.95 23-June-2009 +--------------------------------------------- +- Fix item size handling. +- Add support for retrieving collection statistics as part + of the AKLIST/AKLSUB commands. +- Add support for collection size statistics. +- Build fixes for Windows. +- Support RID-based operations for CREATE, MODIFY and DELETE. +- Avoid emitting unecessary change notifications when + modifying items or collections. +- Add COLMOVE command. +- Reduce number of database writes when modifying a collection. +- Fix parsing of attributes containing CR or LF characters. + +1.1.90 03-June-2009 +--------------------------------------------- +- Return the storage location for items in FETCH responses +- Fix remode identifier encoding problems +- Fix infinite loop when parsing RID lists +- Fix parsing errors on stray newlines +- Support RID-based operations for STORE and MOVE +- Fix race on resource creation +- Provide modified item parts in change notifications +- Build system fixes + +1.1.85 05-May-2009 +--------------------------------------------- +- Improved CMake scripts so it is possible to detect + the Akonadi version in projects that depend on it. +- Simplified the check for existance of tables. +- Add a dedicated item deletion command, to get rid of + the old STORE/EXPUNGE which was extremely inefficient. +- Some fixes to support sqlite in the future. +- Soprano is required now. +- Qt 4.5.0 is required now. +- Support for collection retrieval by remote identifier. +- Support for item retrieval based on the remote identifier. +- Less useless debug output. +- Fixed leak on socket error. +- Various smaller bug fixes, see ChangeLog for a list. +- Support for writing large payloads to a file. +- New Item retrieval code. +- Added a streaming IMAP parser, and ported code the use it. +- Add support for manually restarting an agent instance. + +1.1.2 30-Apr-2009 +--------------------------------------------- +- Avoid DBUS lockups, reported at: https://bugs.kde.org/182198 +- Update user mysql.conf only if global/local one's are newer + +1.1.1 21-Jan-2009 +--------------------------------------------- +- Fix code that was not executed in a release build. +- Require CMake 2.6.0 which fixes boost detection. +- Don't try to restart an agent that has been deleted. + +1.1.0 03-Jan-2009 +--------------------------------------------- +- Restart agents when their executable changed. +- Buildsystem fixes to find and link boost on all platforms. +- Improvements to the startup to prevent partial startup. +- Include revision number in the version string when building from SVN. +- Shut down when we lost the connection to the D-Bus session bus. +- add some basic handling of command line args. +- Add a D-Bus call to flush the notification queue. +- Automatically fix world-writeable mySQL config files. +- Fix for FreeBSD mysql path. + +1.0.81 16-Dec-2008 +--------------------------------------------- +- Restore protocol backward compatibility with Akonadi 1.0.x servers. +- Build system fixes. +- Fix compiler warnings. +- Fall back to the default server path if the configured one points + to a non-existing file. + +1.0.80 19-Nov-2008 +--------------------------------------------- +- Query agent status information asynchronously and answer all queries from + cached values, reduces the risk of an agents blocking the Akonadi server. +- Increase mysql limits to more realistical values. +- Don't mark all new items as recent. +- Changes so it can store the size of an item. +- Better error detection. +- Prevent translated month names in the protocol. +- Some build fixes. +- Handle multiline output correctly. +- Terminate the control process when the server process failed to start. +- Add the ability to debug or valgrind a resource right from the + beginning, similar to the way this can be done with KIO slaves. +- Fix fetching of linked items in arbitrary collections. +- Add notification support for item references in virtual collections. +- Add LINK/UNLINK commands to edit references to items in virtual collections. +- Add a way to notify agents that their configuration has been changed remotely. +- Make sure that all modification times are stored in UTC time zone. +- Unquoted date time with a lenght of 26 characters was not parsed properly. +- Add serverside timestamp support for items. + +1.0.0 22-July-2008 +--------------------------------------------- +- First official stable release +- Bugfix: Unquoted date time with a lenght of + 26 characters was not parsed properly. +- Add serverside timestamp support for items. +- Build system fixes (windows & automoc) + +0.82.0 18-June-2008 +--------------------------------------------- +- Several build and installation fixes for windows and mac. +- Some improvements in the build system. +- Add item part namespaces. +- Implemented all the fetch modes advertised in ItemFetchScope. +- Notify already running clieants about all found types during startup. + +0.81.0 10-May-2008 +--------------------------------------------- +- Fix bug where full part was not fetched when a partial part was available already. +- Collection parsing optimalisation. +- Optimization for quoted string parsing. +- Use org.freedesktop namespace, instead of org.kde for the dbus interfaces. +- Add support for version numbers for database and protocol. +- Fixed foreach misusage. +- Depend on external automoc package instead of a copy. + +0.80.0 24-Apr-2008 +--------------------------------------------- +- Initial release diff --git a/README b/README new file mode 100644 index 0000000..b244440 --- /dev/null +++ b/README @@ -0,0 +1,41 @@ +Akonadi +======== + +What is Akonadi? +------------------ +Akonadi is a PIM layer, which provides an asynchronous API to access all kind +of PIM data (e.g. mails, contacts, events, todos etc.). + +It consists of several processes (generally called the Akonadi server) and a +library (called client library) which encapsulates the communication +between the client and the server. + +This directory contains the sources of the Akonadi server and all the infrastructure +that is needed to build the client libraries and the application which want to make +use of Akonadi. + +Structure +---------- + + * cmake/ + Contains the cmake checks that are needed to build the server. + + * interfaces/ + Contains the dbus interface descriptions that are used by the + client library to control the Akonadi server or request status + information. + + * libs/ + Contains the sources of a private library which provides utils + that are used by both, the Akonadi server and the client library. + + * server/ + + + +See INSTALL for installation instructions. + +See Mainpage.dox for more information. + +See libakonadi.xmi for UML documentation generated with Umbrello. + diff --git a/README.sqlite b/README.sqlite new file mode 100644 index 0000000..5ce5774 --- /dev/null +++ b/README.sqlite @@ -0,0 +1,54 @@ +== PREFACE == + +The reason we have our own QtSql Sqlite driver here is because the one shipped +with Qt misses a bunch of multi-threading fixes crucial for Akonadi. Of course, +these changes should be pushed upstream eventually. + +== INSTALL == +When Sqlite is found, the custom driver will be build and installed in: + +${CMAKE_INSTALL_PREFIX}/lib/plugins/sqldrivers or the sqldrivers subdirectory +of the default Qt plugins path specified at Qt build time if you enable the +KDE_INSTALL_USE_QT_SYS_PATHS option when running CMake. + +The next thing you have to do is add that path (if it isn't already) to your +QT_PLUGIN_PATH environment variable. + +Now you should be able to configure the QSQLITE3 driver in akonadiserverrc. + +== PROBLEMS == + +One of the problematic code paths seems to be: + +server/src/handler/fetch.cpp:161-201 + +In this part the code is iterating over the results of an still active SELECT +query and during this iteration it also tries to do INSERT/UPDATE queries. This +means that there is probably a SHARED lock for the reading and a PENDING lock +for the writing queries. For sqlite to be able to write to the db it needs an +EXCLUSIVE lock. A PENDING lock only gets inclusive when all SHARED locks are +gone. + +A possible solution might be to store the results of the SELECT query into +memory, close the query and than start the inserts/updates. + + +== SQLITE INFO == + +From www.sqlite.org: (see qsqlite/src/qsql_sqlite.cpp:525-529) +Run-time selection of threading mode + +If single-thread mode has not been selected at compile-time or start-time, then +individual database connections can be created as either multi-thread or +serialized. It is not possible to downgrade an individual database connection +to single-thread mode. Nor is it possible to escalate an individual database +connection if the compile-time or start-time mode is single-thread. + +The threading mode for an individual database connection is determined by flags +given as the third argument to sqlite3_open_v2(). The SQLITE_OPEN_NOMUTEX flag +causes the database connection to be in the multi-thread mode and the +SQLITE_OPEN_FULLMUTEX flag causes the connection to be in serialized mode. If +neither flag is specified or if sqlite3_open() or sqlite3_open16() are used +instead of sqlite3_open_v2(), then the default mode determined by the +compile-time and start-time settings is used. + diff --git a/akonadi-mime.xml b/akonadi-mime.xml new file mode 100644 index 0000000..13fcc44 --- /dev/null +++ b/akonadi-mime.xml @@ -0,0 +1,35 @@ + + + + + + + iCal Calendar Event Component + + + + iCal Calendar FreeBusy Component + + + + iCal Calendar Journal Component + + + + iCal Calendar TODO Component + + + Virtual Akonadi Collection + + diff --git a/akonadi-prefix.h.cmake b/akonadi-prefix.h.cmake new file mode 100644 index 0000000..276e724 --- /dev/null +++ b/akonadi-prefix.h.cmake @@ -0,0 +1,7 @@ +/* This file contains all the paths that change when changing the installation prefix */ + +#define AKONADIPREFIX "${CMAKE_INSTALL_PREFIX}" +#define AKONADIDATA "${SHARE_INSTALL_PREFIX}" +#define AKONADICONFIG "${CONFIG_INSTALL_DIR}" +#define AKONADILIB "${LIB_INSTALL_DIR}" +#define AKONADIBUNDLEPATH "${AKONADI_BUNDLE_PATH}" diff --git a/akonadi.categories b/akonadi.categories new file mode 100644 index 0000000..0c06a10 --- /dev/null +++ b/akonadi.categories @@ -0,0 +1,5 @@ +log_akonadiagentserver akonadi (Akonadi Agent Server) +log_akonadiserver akonadi (Akonadi Server) +log_akonadiagentbase akonadi (Akonadi AgentBase Library) +log_akonadiwidgets akonadi (Akonadi Widget Library) +log_akonadiprivate akonadi (Akonadi Private Library) diff --git a/akonaditests_export.h.in b/akonaditests_export.h.in new file mode 100644 index 0000000..654c008 --- /dev/null +++ b/akonaditests_export.h.in @@ -0,0 +1,2 @@ +#include "akonadicore_export.h" +#define AKONADI_TESTS_EXPORT @AKONADI_TESTS_EXPORT@ diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 index 0000000..a88fd97 --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,4 @@ +add_subdirectory(private) +add_subdirectory(server) +add_subdirectory(libs) +add_subdirectory(akonadicontrol) diff --git a/autotests/akonadicontrol/CMakeLists.txt b/autotests/akonadicontrol/CMakeLists.txt new file mode 100644 index 0000000..dc18aae --- /dev/null +++ b/autotests/akonadicontrol/CMakeLists.txt @@ -0,0 +1,27 @@ +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) + +include_directories(${Akonadi_SOURCE_DIR}/src/akonadicontrol + ${Akonadi_BINARY_DIR}/src/akonadicontrol) + +macro(add_unit_test _source) + set(_test ${_source} + ${Akonadi_SOURCE_DIR}/src/akonadicontrol/agenttype.cpp + ) + get_filename_component(_name ${_source} NAME_WE) + add_executable(${_name} ${_test}) + add_test(AkonadiControl-${_name} ${_name}) + if (ENABLE_ASAN) + set_tests_properties(AkonadiControl-${_name} PROPERTIES + ENVIRONMENT ASAN_OPTIONS=symbolize=1 + ) + endif() + target_link_libraries(${_name} + akonadi_shared + KF5AkonadiPrivate + Qt5::Test + KF5::ConfigCore + ${CMAKE_EXE_LINKER_FLAGS_ASAN} + ) +endmacro() + +add_unit_test(agenttypetest.cpp) diff --git a/autotests/akonadicontrol/agenttypetest.cpp b/autotests/akonadicontrol/agenttypetest.cpp new file mode 100644 index 0000000..51982ea --- /dev/null +++ b/autotests/akonadicontrol/agenttypetest.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 Elvis Angelaccio + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include + +#include + +Q_DECLARE_METATYPE(AgentType) + +class AgentTypeTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void testLoad_data(); + void testLoad(); +}; + +void AgentTypeTest::testLoad_data() +{ + AgentType googleContactsResource; + googleContactsResource.exec = QStringLiteral("akonadi_googlecontacts_resource"); + googleContactsResource.mimeTypes = QStringList {QStringLiteral("text/directory"), QString()}; + googleContactsResource.capabilities = QStringList {AgentType::CapabilityResource}; + googleContactsResource.instanceCounter = 0; + googleContactsResource.identifier = QStringLiteral("akonadi_googlecontacts_resource"); + googleContactsResource.custom = QVariantMap { + {QStringLiteral("KAccounts"), QStringList {QStringLiteral("google-contacts"), QStringLiteral("google-calendar")}} + }; + googleContactsResource.launchMethod = AgentType::Process; + // We test an UTF-8 name within quotes. + googleContactsResource.name = QStringLiteral("\"Контакти Google\""); + // We also check whether an unquoted string with a comma is not parsed as a QStringList. See bug #330010 + googleContactsResource.comment = QStringLiteral("Доступ до ваших записів контактів, Google з KDE"); + googleContactsResource.icon = QStringLiteral("im-google"); + + + QTest::addColumn("fileName"); + QTest::addColumn("expectedAgentType"); + + QTest::newRow("google contacts resource") << QFINDTESTDATA("data/akonaditestresource.desktop") << googleContactsResource; +} + +void AgentTypeTest::testLoad() +{ + QFETCH(QString, fileName); + QFETCH(AgentType, expectedAgentType); + + AgentType agentType; + QLocale::setDefault(QLocale::Ukrainian); + QVERIFY(agentType.load(fileName, Q_NULLPTR)); + + QCOMPARE(agentType.exec, expectedAgentType.exec); + QCOMPARE(agentType.mimeTypes, expectedAgentType.mimeTypes); + QCOMPARE(agentType.capabilities, expectedAgentType.capabilities); + QCOMPARE(agentType.instanceCounter, expectedAgentType.instanceCounter); + QCOMPARE(agentType.identifier, expectedAgentType.identifier); + QCOMPARE(agentType.custom, expectedAgentType.custom); + QCOMPARE(agentType.launchMethod, expectedAgentType.launchMethod); + QCOMPARE(agentType.name, expectedAgentType.name); + QCOMPARE(agentType.comment, expectedAgentType.comment); + QCOMPARE(agentType.icon, expectedAgentType.icon); +} + +AKTEST_MAIN(AgentTypeTest) + +#include "agenttypetest.moc" diff --git a/autotests/akonadicontrol/data/akonaditestresource.desktop b/autotests/akonadicontrol/data/akonaditestresource.desktop new file mode 100644 index 0000000..dfa1684 --- /dev/null +++ b/autotests/akonadicontrol/data/akonaditestresource.desktop @@ -0,0 +1,94 @@ +[Desktop Entry] +Name=Google Contacts +Name[bg]=Контакти в Google +Name[bs]=Google kontakti +Name[ca]=Contactes de Google +Name[ca@valencia]=Contactes de Google +Name[cs]=Kontakty Google +Name[da]=Google-kontakter +Name[de]=Google-Kontakte +Name[el]=Google Επαφές +Name[en_GB]=Google Contacts +Name[es]=Contactos Google +Name[et]=Google'i kontaktid +Name[fi]=Google-yhteystiedot +Name[fr]=Contacts Google +Name[ga]=Teagmhálacha Google +Name[gl]=Google Contacts +Name[hu]=Google névjegyek +Name[ia]=Contactos de Google +Name[it]=Contatti Google +Name[kk]=Google контакттары +Name[km]=ទំនាក់ទំនង Google +Name[ko]=Google 연락처 +Name[lt]=Google kontaktai +Name[lv]=Google kontakti +Name[nb]=Google-kontakter +Name[nds]=Google-Kontakten +Name[nl]=Google contactpersonen +Name[pl]=Kontakty Google +Name[pt]=Contactos do Google +Name[pt_BR]=Contatos do Google +Name[ru]=Контакты Google +Name[sk]=Google kontakty +Name[sl]=Stiki Google +Name[sr]=Гуглови контакти +Name[sr@ijekavian]=Гуглови контакти +Name[sr@ijekavianlatin]=Googleovi kontakti +Name[sr@latin]=Googleovi kontakti +Name[sv]=Google kontakter +Name[tr]=Google Kişileri +Name[ug]=Google ئالاقەداشلىرى +Name[uk]="Контакти Google" +Name[x-test]=xxGoogle Contactsxx +Name[zh_CN]=Google 联系人 +Name[zh_TW]=Google 聯絡人 +Comment=Access your Google Contacts from KDE +Comment[bg]=Достъп до контактите ви в Google от KDE +Comment[bs]=Pristupite svojim Google kontaktima iz KDE +Comment[ca]=Accediu als contactes de Google des del KDE +Comment[ca@valencia]=Accediu als contactes de Google des del KDE +Comment[da]=Tilgå dine Google-kontakter fra KDE +Comment[de]=Greifen Sie in KDE auf Google-Kontakte zu +Comment[el]=Αποκτήστε πρόσβαση στις Google επαφές σας από το KDE +Comment[en_GB]=Access your Google Contacts from KDE +Comment[es]=Acceda a sus contactos Google desde KDE +Comment[et]=Oma Google'i kontaktide kasutamine otse KDE-st +Comment[fi]=Google-yhteystietoihin pääsy KDE:sta +Comment[fr]=Accès à vos contacts Google depuis KDE +Comment[gl]=Acceda aos seus contactos de Google desde KDE. +Comment[hu]=A Google névjegyeinek elérése a KDE-ből +Comment[ia]=Accede a tu Contactos de Google ab KDE +Comment[it]=Accedi ai tuoi contatti Google da KDE +Comment[kk]=Google контакттарына KDE-ден қатынау +Comment[km]=ចូល​ដំណើរការ​ទំនាក់ទំនង Google របស់​អ្នក​ពី KDE +Comment[ko]=KDE에서 Google 연락처에 접근하기 +Comment[lt]=Pasiekite savo Google kontaktus iš KDE +Comment[lv]=Piekļūstiet saviem Google kontaktiem no KDE +Comment[nb]=Bruk dine Google-kontakter fra KDE +Comment[nds]=Ut KDE op Dien Google-Kontakten togriepen +Comment[nl]=Heb toegang tot uw Google contactpersonen vanuit KDE +Comment[pl]=Uzyskaj dostęp do Kontaktów Google z KDE +Comment[pt]=Aceda aos seus contactos da Google a partir do KDE +Comment[pt_BR]=Acesse seus contatos do Google a partir do KDE +Comment[ru]=Доступ к контактам Google из KDE +Comment[sk]=Pristupuje k vašim Google kontaktom z KDE +Comment[sl]=Dostopajte do svojih stikov Google +Comment[sr]=Приступите својим контактима на Гуглу из КДЕ‑а +Comment[sr@ijekavian]=Приступите својим контактима на Гуглу из КДЕ‑а +Comment[sr@ijekavianlatin]=Pristupite svojim kontaktima na Googleu iz KDE‑a +Comment[sr@latin]=Pristupite svojim kontaktima na Googleu iz KDE‑a +Comment[sv]=Kom åt Google kontakter från KDE +Comment[tr]=Google Kişilerinize KDE'den erişin +Comment[uk]=Доступ до ваших записів контактів, Google з KDE +Comment[x-test]=xxAccess your Google Contacts from KDExx +Comment[zh_CN]=在 KDE 中访问您的 Google 联系人 +Comment[zh_TW]=用 KDE 存取您的 Google 聯絡人 +Type=AkonadiResource +Exec=akonadi_googlecontacts_resource +X-Akonadi-MimeTypes=text/directory, +X-Akonadi-Capabilities=Resource +X-Akonadi-Identifier=akonadi_googlecontacts_resource +X-Akonadi-Custom-KAccounts=google-contacts,google-calendar +Icon=im-google + diff --git a/autotests/libs/CMakeLists.txt b/autotests/libs/CMakeLists.txt new file mode 100644 index 0000000..94d1b55 --- /dev/null +++ b/autotests/libs/CMakeLists.txt @@ -0,0 +1,158 @@ +set(QT_REQUIRED_VERSION "5.4.0") +find_package(Qt5Test ${QT_REQUIRED_VERSION} CONFIG REQUIRED) +find_package(Qt5DBus ${QT_REQUIRED_VERSION} CONFIG REQUIRED) +include(ECMAddTests) + + +if(${EXECUTABLE_OUTPUT_PATH}) + set( PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH} ) +else() + set( PREVIOUS_EXEC_OUTPUT_PATH . ) +endif() +set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) +set( TEST_RESULT_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}/testresults ) +file(MAKE_DIRECTORY ${TEST_RESULT_OUTPUT_PATH}) + +option(AKONADI_TESTS_XML "Use XML files for the test results, instead of plain text." FALSE) +option(AKONADI_RUN_SQLITE_ISOLATED_TESTS "Run isolated tests with sqlite3 as backend" TRUE) + +kde_enable_exceptions() + +include_directories( + ${Boost_INCLUDE_DIR} +) + +# convenience macro to add akonadi qtestlib unit-tests +macro(add_akonadi_test _source) + set(_test ${_source} ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp) + get_filename_component(_name ${_source} NAME_WE) + add_executable( ${_name} ${_test} ) + add_test( ${_name} ${_name} ) + ecm_mark_as_test(akonadi-${_name}) + set_tests_properties(${_name} PROPERTIES ENVIRONMENT "QT_HASH_SEED=1;QT_NO_CPU_FEATURE=sse4.2") + target_link_libraries(${_name} akonaditestfake Qt5::Test KF5::AkonadiPrivate KF5::DBusAddons KF5::I18n) +endmacro() + +# convenience macro to add akonadi qtestlib unit-tests +macro(add_akonadi_test_widgets _source) + set(_test + ${_source} + ${CMAKE_BINARY_DIR}/src/widgets/akonadiwidgets_debug.cpp + ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp + ) + get_filename_component(_name ${_source} NAME_WE) + add_executable( ${_name} ${_test} ) + add_test( ${_name} ${_name} ) + ecm_mark_as_test(akonadi-${_name}) + set_tests_properties(${_name} PROPERTIES ENVIRONMENT "QT_HASH_SEED=1;QT_NO_CPU_FEATURE=sse4.2") + target_link_libraries(${_name} akonaditestfake Qt5::Test KF5::AkonadiWidgets KF5::AkonadiPrivate KF5::DBusAddons) +endmacro() + + +include(../../KF5AkonadiMacros.cmake) + +# akonadi test fake library +set(akonaditestfake_xml ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml) +set_source_files_properties(${akonaditestfake_xml} PROPERTIES INCLUDE "protocol_p.h") +qt5_add_dbus_interface( akonaditestfake_srcs ${akonaditestfake_xml} notificationsourceinterface ) + +add_library(akonaditestfake SHARED + ${akonaditestfake_srcs} + fakeakonadiservercommand.cpp + fakesession.cpp + fakemonitor.cpp + fakeserverdata.cpp + modelspy.cpp + fakeentitycache.cpp + inspectablemonitor.cpp + inspectablechangerecorder.cpp +) + +generate_export_header(akonaditestfake BASE_NAME akonaditestfake) + +target_link_libraries(akonaditestfake Qt5::DBus KF5::AkonadiCore Qt5::Test Qt5::Widgets KF5::DBusAddons KF5::AkonadiPrivate) + +add_executable(akonadi-firstrun + ../../src/core/firstrun.cpp + firstrunner.cpp + ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp +) +target_link_libraries( akonadi-firstrun Qt5::Test Qt5::Core KF5::AkonadiCore KF5::DBusAddons KF5::ConfigCore Qt5::Widgets) + +# qtestlib unit tests +add_akonadi_test(imapparsertest.cpp) +# It need KIMAP 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(monitornotificationtest.cpp) +add_akonadi_test(collectionutilstest.cpp) +add_akonadi_test(entitydisplayattributetest.cpp) +add_akonadi_test(proxymodelstest.cpp) +add_akonadi_test(newmailnotifierattributetest.cpp) +add_akonadi_test(pop3resourceattributetest.cpp) +add_akonadi_test_widgets(actionstatemanagertest.cpp) +add_akonadi_test(tagmodeltest.cpp) + +add_akonadi_test(sharedvaluepooltest.cpp) +add_akonadi_test(jobtest.cpp) +add_akonadi_test(tagtest_simple.cpp) +# PORT FROM QJSON add_akonadi_test(searchquerytest.cpp) + +# qtestlib tests that need non-exported stuff from +#add_executable( resourceschedulertest resourceschedulertest.cpp ../src/agentbase/resourcescheduler.cpp ) +#add_test( resourceschedulertest resourceschedulertest ) +#ecm_mark_as_test(akonadi-resourceschedulertest) +#target_link_libraries(resourceschedulertest Qt5::Test KF5::AkonadiAgentBase) + + +# testrunner tests +add_akonadi_isolated_test(testenvironmenttest.cpp) +add_akonadi_isolated_test(autoincrementtest.cpp) +add_akonadi_isolated_test(attributefactorytest.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_advanced(monitorfiltertest.cpp "" "KF5::AkonadiPrivate") +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(itemcopytest.cpp) +add_akonadi_isolated_test(itemmovetest.cpp) +add_akonadi_isolated_test(collectioncopytest.cpp) +add_akonadi_isolated_test(collectionmovetest.cpp) +add_akonadi_isolated_test_advanced(collectionsynctest.cpp "${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp" "KF5::I18n") +add_akonadi_isolated_test(itemsynctest.cpp) +add_akonadi_isolated_test(linktest.cpp) +add_akonadi_isolated_test(cachetest.cpp) + +# FIXME: This is very unstable on Jenkins +#add_akonadi_isolated_test(servermanagertest.cpp) + +add_akonadi_isolated_test_advanced(tagselectwidgettest.cpp "" "KF5::AkonadiWidgets") + + +# Having a benchmark is cool if you have any reference to compare against, but this +# benchmark takes over 40 seconds and does not have any real value to us atm. Major +# performance regressions would be spotted by devs anyway, so disabling for now. +#add_akonadi_isolated_test(itembenchmark.cpp) +#add_akonadi_isolated_test(collectioncreator.cpp) + +add_akonadi_isolated_test(gidtest.cpp) +add_akonadi_isolated_test(lazypopulationtest.cpp) +add_akonadi_isolated_test(favoriteproxytest.cpp) +add_akonadi_isolated_test_advanced(itemsearchjobtest.cpp testsearchplugin/testsearchplugin.cpp "") +add_akonadi_isolated_test(tagtest.cpp) +add_akonadi_isolated_test(tagsynctest.cpp) +add_akonadi_isolated_test(relationtest.cpp) +add_akonadi_isolated_test(etmpopulationtest.cpp) diff --git a/autotests/libs/actionstatemanagertest.cpp b/autotests/libs/actionstatemanagertest.cpp new file mode 100644 index 0000000..869e6c9 --- /dev/null +++ b/autotests/libs/actionstatemanagertest.cpp @@ -0,0 +1,632 @@ +/* + Copyright (c) 2010 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. +*/ + +#include +#include "collection.h" + +#include "../src/widgets/actionstatemanager_p.h" +#include "../src/widgets/standardactionmanager.h" + +#define QT_NO_CLIPBOARD // allow running without GUI +#include "../src/widgets/actionstatemanager.cpp" +#undef QT_NO_CLIPBOARD + +#include "../src/core/pastehelper.cpp" + +typedef QHash StateMap; +Q_DECLARE_METATYPE(StateMap) + +using namespace Akonadi; + +class ActionStateManagerTest; + +class UnitActionStateManager : public ActionStateManager +{ +public: + UnitActionStateManager(ActionStateManagerTest *receiver); + +protected: + bool hasResourceCapability(const Collection &collection, const QString &capability) const Q_DECL_OVERRIDE; + +private: + ActionStateManagerTest *mReceiver; +}; + +class ActionStateManagerTest : public QObject +{ + Q_OBJECT + +public: + ActionStateManagerTest() + { + rootCollection = Collection::root(); + const QString dummyMimeType(QStringLiteral("text/dummy")); + + resourceCollectionOne.setId(1); + resourceCollectionOne.setName(QStringLiteral("resourceCollectionOne")); + resourceCollectionOne.setRights(Collection::ReadOnly); + resourceCollectionOne.setParentCollection(rootCollection); + resourceCollectionOne.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + folderCollectionOne.setId(10); + folderCollectionOne.setName(QStringLiteral("folderCollectionOne")); + folderCollectionOne.setRights(Collection::ReadOnly); + folderCollectionOne.setParentCollection(resourceCollectionOne); + folderCollectionOne.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + resourceCollectionTwo.setId(2); + resourceCollectionTwo.setName(QStringLiteral("resourceCollectionTwo")); + resourceCollectionTwo.setRights(Collection::AllRights); + resourceCollectionTwo.setParentCollection(rootCollection); + resourceCollectionTwo.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + folderCollectionTwo.setId(20); + folderCollectionTwo.setName(QStringLiteral("folderCollectionTwo")); + folderCollectionTwo.setRights(Collection::AllRights); + folderCollectionTwo.setParentCollection(resourceCollectionTwo); + folderCollectionTwo.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + resourceCollectionThree.setId(3); + resourceCollectionThree.setName(QStringLiteral("resourceCollectionThree")); + resourceCollectionThree.setRights(Collection::AllRights); + resourceCollectionThree.setParentCollection(rootCollection); + resourceCollectionThree.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + folderCollectionThree.setId(30); + folderCollectionThree.setName(QStringLiteral("folderCollectionThree")); + folderCollectionThree.setRights(Collection::AllRights); + folderCollectionThree.setParentCollection(resourceCollectionThree); + folderCollectionThree.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + folderCollectionThree.setId(31); + folderCollectionThree.setName(QStringLiteral("folderCollectionThreeOne")); + folderCollectionThree.setRights(Collection::AllRights); + folderCollectionThree.setParentCollection(resourceCollectionThree); + + mCapabilityMap.insert(QStringLiteral("NoConfig"), Collection::List() << resourceCollectionThree); + mFavoriteCollectionMap.insert(folderCollectionThree.id()); + } + + bool hasResourceCapability(const Collection &collection, const QString &capability) const + { + return mCapabilityMap.value(capability).contains(collection); + } + +public Q_SLOTS: + void enableAction(int type, bool enable) + { + mStateMap.insert(static_cast(type), enable); + } + + void updatePluralLabel(int type, int count) + { + Q_UNUSED(type); + Q_UNUSED(count); + } + + bool isFavoriteCollection(const Akonadi::Collection &collection) + { + return mFavoriteCollectionMap.contains(collection.id()); + } + + void updateAlternatingAction(int action) + { + Q_UNUSED(action); + } + +private Q_SLOTS: + + void init() + { + mStateMap.clear(); + } + + void testCollectionSelected_data() + { + QTest::addColumn("collections"); + QTest::addColumn("stateMap"); + + { + Collection::List collectionList; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, false); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, false); + map.insert(StandardActionManager::CollectionProperties, false); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, false); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, false); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, false); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("nothing selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << rootCollection; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, false); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, false); + map.insert(StandardActionManager::CollectionProperties, false); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, false); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, false); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, false); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("root collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << resourceCollectionOne; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, true); + map.insert(StandardActionManager::ResourceProperties, true); + map.insert(StandardActionManager::SynchronizeResources, true); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, true); + + QTest::newRow("read-only resource collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << resourceCollectionTwo; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, true); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, true); + map.insert(StandardActionManager::ResourceProperties, true); + map.insert(StandardActionManager::SynchronizeResources, true); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, true); + + QTest::newRow("writable resource collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << resourceCollectionThree; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, true); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, true); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, true); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, true); + + QTest::newRow("non-configurable resource collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << folderCollectionOne; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("read-only folder collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << folderCollectionTwo; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, true); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, true); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, true); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, true); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, true); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, true); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, true); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("writable folder collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << folderCollectionThree; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, true); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, true); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, true); + map.insert(StandardActionManager::RenameFavoriteCollection, true); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, true); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, true); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, true); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, true); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, true); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("favorite writable folder collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << folderCollectionThreeOne; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); // content mimetype is missing + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, true); + map.insert(StandardActionManager::SynchronizeCollections, false); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); // content mimetype is missing + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, true); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, true); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, true); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, true); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, true); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("structural folder collection selected") << collectionList << map; + } + + // multiple collections + { + Collection::List collectionList; + collectionList << rootCollection << resourceCollectionTwo; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, false); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, false); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, false); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, false); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, false); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("root collection and writable resource collection selected") << collectionList << map; + } + } + + void testCollectionSelected() + { + QFETCH(Collection::List, collections); + QFETCH(StateMap, stateMap); + + UnitActionStateManager manager(this); + manager.updateState(collections, Item::List()); + + QCOMPARE(stateMap.count(), mStateMap.count()); + + QHashIterator it(stateMap); + while (it.hasNext()) { + it.next(); + //qDebug() << it.key(); + QVERIFY(mStateMap.contains(it.key())); + QCOMPARE(it.value(), mStateMap.value(it.key())); + } + } + +private: + /** + * The structure of our fake collections: + * + * rootCollection + * | + * +- resourceCollectionOne + * | | + * | `folderCollectionOne + * | + * +- resourceCollectionTwo + * | | + * | `folderCollectionTwo + * | + * `- resourceCollectionThree + * | + * +-folderCollectionThree + * | + * `-folderCollectionThreeOne + */ + Collection rootCollection; + Collection resourceCollectionOne; + Collection resourceCollectionTwo; + Collection resourceCollectionThree; + Collection folderCollectionOne; + Collection folderCollectionTwo; + Collection folderCollectionThree; + Collection folderCollectionThreeOne; + + StateMap mStateMap; + QHash mCapabilityMap; + QSet mFavoriteCollectionMap; +}; + +UnitActionStateManager::UnitActionStateManager(ActionStateManagerTest *receiver) + : mReceiver(receiver) +{ + setReceiver(receiver); +} + +bool UnitActionStateManager::hasResourceCapability(const Collection &collection, const QString &capability) const +{ + return mReceiver->hasResourceCapability(collection, capability); +} + +QTEST_AKONADIMAIN(ActionStateManagerTest) + +#include "actionstatemanagertest.moc" diff --git a/autotests/libs/attributefactorytest.cpp b/autotests/libs/attributefactorytest.cpp new file mode 100644 index 0000000..858204a --- /dev/null +++ b/autotests/libs/attributefactorytest.cpp @@ -0,0 +1,96 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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 "attributefactorytest.h" +#include "collectionpathresolver.h" +#include "testattribute.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(AttributeFactoryTest) + +static Collection res1; + +void AttributeFactoryTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("res1"), this); + AKVERIFYEXEC(resolver); + res1 = Collection(resolver->collection()); +} + +void AttributeFactoryTest::testUnknownAttribute() +{ + // The attribute is currently not registered. + Item item; + item.setMimeType(QStringLiteral("text/directory")); + item.setPayload("payload"); + TestAttribute *ta = new TestAttribute; + QVERIFY(AttributeFactory::createAttribute(ta->type())); // DefaultAttribute + ta->data = "lalala"; + item.addAttribute(ta); + ItemCreateJob *cjob = new ItemCreateJob(item, res1); + AKVERIFYEXEC(cjob); + int id = cjob->item().id(); + item = Item(id); + ItemFetchJob *fjob = new ItemFetchJob(item); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().fetchAllAttributes(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items().first(); + QVERIFY(item.hasAttribute()); // has DefaultAttribute + ta = item.attribute(); + QVERIFY(!ta); // but can't cast it to TestAttribute +} + +void AttributeFactoryTest::testRegisteredAttribute() +{ + AttributeFactory::registerAttribute(); + + Item item; + item.setMimeType(QStringLiteral("text/directory")); + item.setPayload("payload"); + TestAttribute *ta = new TestAttribute; + QVERIFY(AttributeFactory::createAttribute(ta->type()) != 0); + ta->data = "lalala"; + item.addAttribute(ta); + ItemCreateJob *cjob = new ItemCreateJob(item, res1); + AKVERIFYEXEC(cjob); + int id = cjob->item().id(); + item = Item(id); + ItemFetchJob *fjob = new ItemFetchJob(item); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().fetchAllAttributes(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items().first(); + QVERIFY(item.hasAttribute()); + ta = item.attribute(); + QVERIFY(ta); + QCOMPARE(ta->data, QByteArray("lalala")); +} diff --git a/autotests/libs/attributefactorytest.h b/autotests/libs/attributefactorytest.h new file mode 100644 index 0000000..337a758 --- /dev/null +++ b/autotests/libs/attributefactorytest.h @@ -0,0 +1,35 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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 ATTRIBUTEFACTORYTEST_H +#define ATTRIBUTEFACTORYTEST_H + +#include + +class AttributeFactoryTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testUnknownAttribute(); + void testRegisteredAttribute(); + +}; + +#endif diff --git a/autotests/libs/autoincrementtest.cpp b/autotests/libs/autoincrementtest.cpp new file mode 100644 index 0000000..3711c75 --- /dev/null +++ b/autotests/libs/autoincrementtest.cpp @@ -0,0 +1,143 @@ +/* + Copyright (c) 2009 Thomas McGuire + + 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 "autoincrementtest.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "control.h" +#include "item.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" + +#include +#include "test_utils.h" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(AutoIncrementTest) + +static bool isMySQLEnvironment() +{ + return (qgetenv("TESTRUNNER_DB_ENVIRONMENT") == "mysql"); +} + +void AutoIncrementTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + + itemTargetCollection = Collection(collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(itemTargetCollection.isValid()); + + collectionTargetCollection = Collection(collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(collectionTargetCollection.isValid()); +} + +Akonadi::ItemCreateJob *AutoIncrementTest::createItemCreateJob() +{ + QByteArray payload("Hello world"); + Item item(-1); + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload(payload); + return new ItemCreateJob(item, itemTargetCollection); +} + +Akonadi::CollectionCreateJob *AutoIncrementTest::createCollectionCreateJob(int number) +{ + Collection collection; + collection.setParentCollection(collectionTargetCollection); + collection.setName(QStringLiteral("testCollection") + QString::number(number)); + return new CollectionCreateJob(collection); +} + +void AutoIncrementTest::testItemAutoIncrement() +{ + QList itemsToDelete; + Item::Id lastId = -1; + + // Create 20 test items + for (int i = 0; i < 20; i++) { + ItemCreateJob *job = createItemCreateJob(); + AKVERIFYEXEC(job); + Item newItem = job->item(); + QVERIFY(newItem.id() > lastId); + lastId = newItem.id(); + itemsToDelete.append(newItem); + } + + // Delete the 20 items + foreach (const Item &item, itemsToDelete) { + ItemDeleteJob *job = new ItemDeleteJob(item); + AKVERIFYEXEC(job); + } + + // Restart the server, then test item creation again. The new id of the item + // should be higher than all ids before. + restartAkonadiServer(); + ItemCreateJob *job = createItemCreateJob(); + AKVERIFYEXEC(job); + Item newItem = job->item(); + + if (isMySQLEnvironment()) { + QEXPECT_FAIL("", "Server bug: http://bugs.mysql.com/bug.php?id=199", Continue); + } + + QVERIFY(newItem.id() > lastId); + lastId = newItem.id(); +} + +void AutoIncrementTest::testCollectionAutoIncrement() +{ + Collection::List collectionsToDelete; + Collection::Id lastId = -1; + + // Create 20 test collections + for (int i = 0; i < 20; i++) { + CollectionCreateJob *job = createCollectionCreateJob(i); + AKVERIFYEXEC(job); + Collection newCollection = job->collection(); + QVERIFY(newCollection.id() > lastId); + lastId = newCollection.id(); + collectionsToDelete.append(newCollection); + } + + // Delete the 20 collections + foreach (const Collection &collection, collectionsToDelete) { + CollectionDeleteJob *job = new CollectionDeleteJob(collection); + AKVERIFYEXEC(job); + } + + // Restart the server, then test collection creation again. The new id of the collection + // should be higher than all ids before. + restartAkonadiServer(); + + CollectionCreateJob *job = createCollectionCreateJob(0); + AKVERIFYEXEC(job); + Collection newCollection = job->collection(); + + if (isMySQLEnvironment()) { + QEXPECT_FAIL("", "Server bug: http://bugs.mysql.com/bug.php?id=199", Continue); + } + + QVERIFY(newCollection.id() > lastId); + lastId = newCollection.id(); +} diff --git a/autotests/libs/autoincrementtest.h b/autotests/libs/autoincrementtest.h new file mode 100644 index 0000000..7f17ef3 --- /dev/null +++ b/autotests/libs/autoincrementtest.h @@ -0,0 +1,47 @@ +/* + Copyright (c) 2009 Thomas McGuire + + 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 AUTOINCREMENTTEST_H +#define AUTOINCREMENTTEST_H + +#include "collection.h" + +#include + +namespace Akonadi +{ +class CollectionCreateJob; +class ItemCreateJob; +} + +class AutoIncrementTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testItemAutoIncrement(); + void testCollectionAutoIncrement(); + +private: + Akonadi::ItemCreateJob *createItemCreateJob(); + Akonadi::CollectionCreateJob *createCollectionCreateJob(int number); + Akonadi::Collection itemTargetCollection; + Akonadi::Collection collectionTargetCollection; +}; + +#endif diff --git a/autotests/libs/cachetest.cpp b/autotests/libs/cachetest.cpp new file mode 100644 index 0000000..58cacf4 --- /dev/null +++ b/autotests/libs/cachetest.cpp @@ -0,0 +1,163 @@ +/* + 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 "collection.h" +#include "collectionfetchjob.h" +#include "collectionmodifyjob.h" +#include "item.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "agentmanager.h" +#include "agentinstance.h" +#include "itemcopyjob.h" + +#include + +using namespace Akonadi; + +class CacheTest : public QObject +{ + Q_OBJECT +private: + void enableAgent(const QString &id, bool enable) + { + AgentInstance instance; + foreach (AgentInstance agent, Akonadi::AgentManager::self()->instances()) { //krazy:exclude=foreach + if (agent.identifier() == id) { + instance = agent; + break; + } + } + + QVERIFY(instance.isValid()); + instance.setIsOnline(enable); + QTRY_COMPARE(Akonadi::AgentManager::self()->instance(id).isOnline(), enable); + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + void testRetrievalErrorBurst() // caused rare server crashs with old item retrieval code + { + Collection col(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + + enableAgent(QStringLiteral("akonadi_knut_resource_0"), false); + + ItemFetchJob *fetch = new ItemFetchJob(col, this); + fetch->fetchScope().fetchFullPayload(true); + QVERIFY(!fetch->exec()); + } + + void testResourceRetrievalOnFetch_data() + { + QTest::addColumn("item"); + QTest::addColumn("resourceEnabled"); + + QTest::newRow("resource online") << Item(1) << true; + QTest::newRow("resource offline") << Item(2) << false; + } + + void testResourceRetrievalOnFetch() + { + QFETCH(Item, item); + QFETCH(bool, resourceEnabled); + + ItemFetchJob *fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QVERIFY(!item.hasPayload()); + + enableAgent(QStringLiteral("akonadi_knut_resource_0"), resourceEnabled); + + fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + QCOMPARE(fetch->exec(), resourceEnabled); + if (resourceEnabled) { + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QVERIFY(item.hasPayload()); + QVERIFY(item.revision() > 0); // was changed by the resource delivering the payload + } + + fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QCOMPARE(item.hasPayload(), resourceEnabled); + } + + void testResourceRetrievalOnCopy_data() + { + QTest::addColumn("item"); + QTest::addColumn("resourceEnabled"); + + QTest::newRow("online") << Item(3) << true; + QTest::newRow("offline") << Item(4) << false; + } + + void testResourceRetrievalOnCopy() + { + QFETCH(Item, item); + QFETCH(bool, resourceEnabled); + + ItemFetchJob *fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QVERIFY(!item.hasPayload()); + + enableAgent(QStringLiteral("akonadi_knut_resource_0"), resourceEnabled); + + Collection dest(collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(dest.isValid()); + + ItemCopyJob *copy = new ItemCopyJob(item, dest, this); + QCOMPARE(copy->exec(), resourceEnabled); + + fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QCOMPARE(item.hasPayload(), resourceEnabled); + } + +}; + +QTEST_AKONADIMAIN(CacheTest) + +#include "cachetest.moc" diff --git a/autotests/libs/changerecordertest.cpp b/autotests/libs/changerecordertest.cpp new file mode 100644 index 0000000..3795573 --- /dev/null +++ b/autotests/libs/changerecordertest.cpp @@ -0,0 +1,189 @@ +/* + 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 "testattribute.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace Akonadi; + +Q_DECLARE_METATYPE(QSet) + +class ChangeRecorderTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() + { + qRegisterMetaType(); + qRegisterMetaType >(); + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + + settings = new QSettings(QStringLiteral("kde.org"), QStringLiteral("akonadi-changerecordertest"), this); + } + + // After each test + void cleanup() + { + // See ChangeRecorderPrivate::notificationsFileName() + QFile::remove(settings->fileName() + QStringLiteral("_changes.dat")); + } + + void testChangeRecorder_data() + { + QTest::addColumn("actions"); + + QTest::newRow("nothingToReplay") << (QStringList() << QStringLiteral("rn")); + QTest::newRow("nothingOneNothing") << (QStringList() << QStringLiteral("rn") << QStringLiteral("c2") << QStringLiteral("r2") << QStringLiteral("rn")); + QTest::newRow("multipleItems") << (QStringList() << QStringLiteral("c1") << QStringLiteral("c2") << QStringLiteral("c3") << QStringLiteral("r1") << QStringLiteral("c4") << QStringLiteral("r2") << QStringLiteral("r3") << QStringLiteral("r4") << QStringLiteral("rn")); + QTest::newRow("reload") << (QStringList() << QStringLiteral("c1") << QStringLiteral("c1") << QStringLiteral("c3") << QStringLiteral("reload") << QStringLiteral("r1") << QStringLiteral("r1") << QStringLiteral("r3") << QStringLiteral("rn")); + QTest::newRow("more") << (QStringList() << QStringLiteral("c1") << QStringLiteral("c2") << QStringLiteral("c3") << QStringLiteral("reload") << QStringLiteral("r1") << QStringLiteral("reload") << QStringLiteral("c4") << QStringLiteral("reload") << QStringLiteral("r2") << QStringLiteral("reload") << QStringLiteral("r3") << QStringLiteral("r4") << QStringLiteral("rn")); + //FIXME: Due to the event compression in the server we simply expect a removal signal + // QTest::newRow("modifyThenDelete") << (QStringList() << "c1" << "d1" << "r1" << "rn"); + } + + void testChangeRecorder() + { + QFETCH(QStringList, actions); + QString lastAction; + + ChangeRecorder *rec = createChangeRecorder(); + QVERIFY(rec->isEmpty()); + Q_FOREACH (const QString &action, actions) { + qDebug() << action; + if (action == QStringLiteral("rn")) { + replayNextAndExpectNothing(rec); + } else if (action == QStringLiteral("reload")) { + // Check saving and loading from disk + delete rec; + rec = createChangeRecorder(); + } else if (action.at(0) == QLatin1Char('c')) { + // c1 = "trigger change on item 1" + const int id = action.mid(1).toInt(); + Q_ASSERT(id); + triggerChange(id); + if (action != lastAction) { + // enter event loop and wait for change notifications from the server + QVERIFY(AkonadiTest::akWaitForSignal(rec, SIGNAL(changesAdded()), 1000)); + } + } else if (action.at(0) == QLatin1Char('d')) { + // d1 = "delete item 1" + const int id = action.mid(1).toInt(); + Q_ASSERT(id); + triggerDelete(id); + QTest::qWait(500); + } else if (action.at(0) == QLatin1Char('r')) { + // r1 = "replayNext and expect to get itemChanged(1)" + const int id = action.mid(1).toInt(); + Q_ASSERT(id); + replayNextAndProcess(rec, id); + } else { + QVERIFY2(false, qPrintable(QStringLiteral("Unsupported: ") + action)); + } + lastAction = action; + } + QVERIFY(rec->isEmpty()); + delete rec; + } + +private: + void triggerChange(Akonadi::Item::Id uid) + { + static int s_num = 0; + Item item(uid); + TestAttribute *attr = item.attribute(Item::AddIfMissing); + attr->data = QByteArray::number(++s_num); + ItemModifyJob *job = new ItemModifyJob(item); + job->disableRevisionCheck(); + AKVERIFYEXEC(job); + } + + void triggerDelete(Akonadi::Item::Id uid) + { + Item item(uid); + ItemDeleteJob *job = new ItemDeleteJob(item); + AKVERIFYEXEC(job); + } + + void replayNextAndProcess(ChangeRecorder *rec, Akonadi::Item::Id expectedUid) + { + QSignalSpy nothingSpy(rec, SIGNAL(nothingToReplay())); + QVERIFY(nothingSpy.isValid()); + QSignalSpy itemChangedSpy(rec, SIGNAL(itemChanged(Akonadi::Item,QSet))); + QVERIFY(itemChangedSpy.isValid()); + + rec->replayNext(); + if (itemChangedSpy.isEmpty()) { + QVERIFY(AkonadiTest::akWaitForSignal(rec, SIGNAL(itemChanged(Akonadi::Item,QSet)), 1000)); + } + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.at(0).at(0).value().id(), expectedUid); + + rec->changeProcessed(); + + QCOMPARE(nothingSpy.count(), 0); + } + + void replayNextAndExpectNothing(ChangeRecorder *rec) + { + QSignalSpy nothingSpy(rec, SIGNAL(nothingToReplay())); + QVERIFY(nothingSpy.isValid()); + QSignalSpy itemChangedSpy(rec, SIGNAL(itemChanged(Akonadi::Item,QSet))); + QVERIFY(itemChangedSpy.isValid()); + + rec->replayNext(); // emits nothingToReplay immediately + + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(nothingSpy.count(), 1); + } + + ChangeRecorder *createChangeRecorder() const + { + ChangeRecorder *rec = new ChangeRecorder(); + rec->setConfig(settings); + rec->setAllMonitored(); + rec->itemFetchScope().fetchFullPayload(); + rec->itemFetchScope().fetchAllAttributes(); + rec->itemFetchScope().setCacheOnly(true); + + // Ensure we listen to a signal, otherwise MonitorPrivate::isLazilyIgnored will ignore notifications + QSignalSpy *spy = new QSignalSpy(rec, SIGNAL(itemChanged(Akonadi::Item,QSet))); + spy->setParent(rec); + + return rec; + } + + QSettings *settings; + +}; + +QTEST_AKONADIMAIN(ChangeRecorderTest) + +#include "changerecordertest.moc" diff --git a/autotests/libs/collectionattributetest.cpp b/autotests/libs/collectionattributetest.cpp new file mode 100644 index 0000000..7cdfa12 --- /dev/null +++ b/autotests/libs/collectionattributetest.cpp @@ -0,0 +1,227 @@ +/* + 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 "collectionattributetest.h" +#include "collectionpathresolver.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(CollectionAttributeTest) + +class TestAttribute : public Attribute +{ +public: + TestAttribute() + : Attribute() + { + } + TestAttribute(const QByteArray &data) + : mData(data) + { + } + TestAttribute *clone() const Q_DECL_OVERRIDE + { + return new TestAttribute(mData); + } + QByteArray type() const Q_DECL_OVERRIDE + { + return "TESTATTRIBUTE"; + } + QByteArray serialized() const Q_DECL_OVERRIDE + { + return mData; + } + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE { + mData = data; + } +private: + QByteArray mData; +}; + +static int parentColId = -1; + +void CollectionAttributeTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AttributeFactory::registerAttribute(); + + CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("res3"), this); + AKVERIFYEXEC(resolver); + parentColId = resolver->collection(); + QVERIFY(parentColId > 0); +} + +void CollectionAttributeTest::testAttributes_data() +{ + QTest::addColumn("attr1"); + QTest::addColumn("attr2"); + + QTest::newRow("basic") << QByteArray("foo") << QByteArray("bar"); + QTest::newRow("empty1") << QByteArray("") << QByteArray("non-empty"); + QTest::newRow("empty2") << QByteArray("non-empty") << QByteArray(""); + QTest::newRow("space") << QByteArray("foo bar") << QByteArray("bar foo"); + QTest::newRow("newline") << QByteArray("\n") << QByteArray("no newline"); + QTest::newRow("newline2") << QByteArray(" \\\n\\\nnn") << QByteArray("no newline"); + QTest::newRow("cr") << QByteArray("\r") << QByteArray("\\\r\n"); + QTest::newRow("quotes") << QByteArray("\"quoted \\ test\"") << QByteArray("single \" quote \\"); + QTest::newRow("parenthesis") << QByteArray(")") << QByteArray("("); + QTest::newRow("binary") << QByteArray("\000") << QByteArray("\001"); +} + +void CollectionAttributeTest::testAttributes() +{ + QFETCH(QByteArray, attr1); + QFETCH(QByteArray, attr2); + + // add a custom attribute + TestAttribute *attr = new TestAttribute(); + attr->deserialize(attr1); + Collection col; + col.setName(QStringLiteral("attribute test")); + col.setParentCollection(Collection(parentColId)); + col.addAttribute(attr); + CollectionCreateJob *create = new CollectionCreateJob(col, this); + AKVERIFYEXEC(create); + col = create->collection(); + QVERIFY(col.isValid()); + + attr = col.attribute(); + QVERIFY(attr != 0); + QCOMPARE(attr->serialized(), QByteArray(attr1)); + + CollectionFetchJob *list = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(list); + QCOMPARE(list->collections().count(), 1); + col = list->collections().first(); + + QVERIFY(col.isValid()); + attr = col.attribute(); + QVERIFY(attr != 0); + QCOMPARE(attr->serialized(), QByteArray(attr1)); + + TestAttribute *attrB = new TestAttribute(); + attrB->deserialize(attr2); + col.addAttribute(attrB); + attrB = col.attribute(); + QVERIFY(attrB != 0); + QCOMPARE(attrB->serialized(), QByteArray(attr2)); + + attrB->deserialize(attr1); + col.addAttribute(attrB); + attrB = col.attribute(); + QVERIFY(attrB != 0); + QCOMPARE(attrB->serialized(), QByteArray(attr1)); + + // modify a custom attribute + col.attribute(Collection::AddIfMissing)->deserialize(attr2); + CollectionModifyJob *modify = new CollectionModifyJob(col, this); + AKVERIFYEXEC(modify); + + list = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(list); + QCOMPARE(list->collections().count(), 1); + col = list->collections().first(); + + QVERIFY(col.isValid()); + attr = col.attribute(); + QVERIFY(attr != 0); + QCOMPARE(attr->serialized(), QByteArray(attr2)); + + // delete a custom attribute + col.removeAttribute(); + modify = new CollectionModifyJob(col, this); + AKVERIFYEXEC(modify); + + list = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(list); + QCOMPARE(list->collections().count(), 1); + col = list->collections().first(); + + QVERIFY(col.isValid()); + attr = col.attribute(); + QVERIFY(attr == 0); + + // cleanup + CollectionDeleteJob *del = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(del); + +} + +void CollectionAttributeTest::testDefaultAttributes() +{ + Collection col; + QCOMPARE(col.attributes().count(), 0); + Attribute *attr = AttributeFactory::createAttribute("TYPE"); + QVERIFY(attr); + attr->deserialize("VALUE"); + col.addAttribute(attr); + QCOMPARE(col.attributes().count(), 1); + QVERIFY(col.hasAttribute("TYPE")); + QCOMPARE(col.attribute("TYPE")->serialized(), QByteArray("VALUE")); +} + +void CollectionAttributeTest::testCollectionRightsAttribute() +{ + CollectionRightsAttribute attribute; + Collection::Rights rights; + + QCOMPARE(attribute.rights(), rights); + + for (int mask = 0; mask <= Collection::AllRights; ++mask) { + rights = Collection::AllRights; + rights &= mask; + QCOMPARE(rights, mask); + + attribute.setRights(rights); + QCOMPARE(attribute.rights(), rights); + + QByteArray data = attribute.serialized(); + attribute.deserialize(data); + QCOMPARE(attribute.rights(), rights); + } +} + +void CollectionAttributeTest::testCollectionIdentifcationAttribute() +{ + QByteArray id("identifier"); + QByteArray ns("namespace"); + CollectionIdentificationAttribute attribute(id, ns); + QCOMPARE(attribute.identifier(), id); + QCOMPARE(attribute.collectionNamespace(), ns); + + QByteArray result = attribute.serialized(); + CollectionIdentificationAttribute parsed; + parsed.deserialize(result); + qDebug() << parsed.identifier() << parsed.collectionNamespace() << result;; + QCOMPARE(parsed.identifier(), id); + QCOMPARE(parsed.collectionNamespace(), ns); +} diff --git a/autotests/libs/collectionattributetest.h b/autotests/libs/collectionattributetest.h new file mode 100644 index 0000000..a5f4239 --- /dev/null +++ b/autotests/libs/collectionattributetest.h @@ -0,0 +1,37 @@ +/* + 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 COLLECTIONATTRIBUTETEST_H +#define COLLECTIONATTRIBUTETEST_H + +#include + +class CollectionAttributeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testAttributes_data(); + void testAttributes(); + void testDefaultAttributes(); + void testCollectionRightsAttribute(); + void testCollectionIdentifcationAttribute(); +}; + +#endif diff --git a/autotests/libs/collectioncopytest.cpp b/autotests/libs/collectioncopytest.cpp new file mode 100644 index 0000000..81aff45 --- /dev/null +++ b/autotests/libs/collectioncopytest.cpp @@ -0,0 +1,134 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "test_utils.h" + +using namespace Akonadi; + +class CollectionCopyTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + + Control::start(); + // switch target resources offline to reduce interference from them + foreach (Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances()) { //krazy:exclude=foreach + if (agent.identifier() == QStringLiteral("akonadi_knut_resource_2")) { + agent.setIsOnline(false); + } + } + } + + void testCopy() + { + const Collection target(collectionIdFromPath(QStringLiteral("res3"))); + Collection source(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(target.isValid()); + QVERIFY(source.isValid()); + + // obtain reference listing + CollectionFetchJob *fetch = new CollectionFetchJob(source, CollectionFetchJob::Base); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->collections().count(), 1); + source = fetch->collections().first(); + QVERIFY(source.isValid()); + + fetch = new CollectionFetchJob(source, CollectionFetchJob::Recursive); + AKVERIFYEXEC(fetch); + QMap referenceData; + Collection::List cols = fetch->collections(); + cols << source; + foreach (const Collection &c, cols) { + ItemFetchJob *job = new ItemFetchJob(c, this); + AKVERIFYEXEC(job); + referenceData.insert(c, job->items()); + } + + // actually copy the collection + CollectionCopyJob *copy = new CollectionCopyJob(source, target); + AKVERIFYEXEC(copy); + + // list destination and check if everything has arrived + CollectionFetchJob *list = new CollectionFetchJob(target, CollectionFetchJob::Recursive); + AKVERIFYEXEC(list); + cols = list->collections(); + QCOMPARE(cols.count(), referenceData.count()); + for (QMap::ConstIterator it = referenceData.constBegin(); it != referenceData.constEnd(); ++it) { + QVERIFY(!cols.contains(it.key())); + Collection col; + foreach (const Collection &c, cols) { + if (it.key().name() == c.name()) { + col = c; + } + } + + QVERIFY(col.isValid()); + QCOMPARE(col.resource(), QStringLiteral("akonadi_knut_resource_2")); + QVERIFY(col.remoteId().isEmpty()); + ItemFetchJob *job = new ItemFetchJob(col, this); + job->fetchScope().fetchFullPayload(); + job->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), it.value().count()); + foreach (const Item &item, job->items()) { + QVERIFY(!it.value().contains(item)); + QVERIFY(item.remoteId().isEmpty()); + QVERIFY(item.hasPayload()); + } + } + } + + void testIlleagalCopy() + { + // invalid source + CollectionCopyJob *copy = new CollectionCopyJob(Collection(), Collection(1)); + QVERIFY(!copy->exec()); + + // non-existing source + copy = new CollectionCopyJob(Collection(INT_MAX), Collection(1)); + QVERIFY(!copy->exec()); + + // invalid target + copy = new CollectionCopyJob(Collection(1), Collection()); + QVERIFY(!copy->exec()); + + // non-existing target + copy = new CollectionCopyJob(Collection(1), Collection(INT_MAX)); + QVERIFY(!copy->exec()); + } + +}; + +QTEST_AKONADIMAIN(CollectionCopyTest) + +#include "collectioncopytest.moc" diff --git a/autotests/libs/collectioncreator.cpp b/autotests/libs/collectioncreator.cpp new file mode 100644 index 0000000..6e34ce2 --- /dev/null +++ b/autotests/libs/collectioncreator.cpp @@ -0,0 +1,89 @@ +/* + Copyright (c) 2006, 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 "agentinstance.h" +#include "agentmanager.h" +#include "collectioncreatejob.h" +#include "collectionpathresolver.h" +#include "transactionjobs.h" + +#include "qtest_akonadi.h" +#include "test_utils.h" + +using namespace Akonadi; + +class CollectionCreator : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + } + + void createCollections_data() + { + QTest::addColumn("count"); + QTest::addColumn("useTransaction"); + + QList counts = QList() << 1 << 10 << 100 << 1000; + QList transactions = QList() << false << true; + foreach (int count, counts) { + foreach (bool transaction, transactions) { //krazy:exclude=foreach + QTest::newRow(QString::fromLatin1("%1-%2").arg(count).arg(transaction ? QLatin1String("trans") : QLatin1String("notrans")).toLatin1().constData()) + << count << transaction; + } + } + } + + void createCollections() + { + QFETCH(int, count); + QFETCH(bool, useTransaction); + + const Collection parent(collectionIdFromPath(QLatin1String("res3"))); + QVERIFY(parent.isValid()); + + static int index = 0; + Job *lastJob = 0; + QBENCHMARK { + if (useTransaction) + { + lastJob = new TransactionBeginJob(this); + } + for (int i = 0; i < count; ++i) + { + Collection col; + col.setParentCollection(parent); + col.setName(QLatin1String("col") + QString::number(++index)); + lastJob = new CollectionCreateJob(col, this); + } + if (useTransaction) + { + lastJob = new TransactionCommitJob(this); + } + AkonadiTest::akWaitForSignal(lastJob, SIGNAL(result(KJob*)), 15000); + } + } +}; + +QTEST_AKONADIMAIN(CollectionCreator) + +#include "collectioncreator.moc" diff --git a/autotests/libs/collectionjobtest.cpp b/autotests/libs/collectionjobtest.cpp new file mode 100644 index 0000000..6f0ce45 --- /dev/null +++ b/autotests/libs/collectionjobtest.cpp @@ -0,0 +1,950 @@ +/* + 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 "collectionjobtest.h" + +#include + +#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" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(CollectionJobTest, NoGUI) + +void CollectionJobTest::initTestCase() +{ + qRegisterMetaType(); + AttributeFactory::registerAttribute(); + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); +} + +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); + AKVERIFYEXEC(job); + 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(), QStringLiteral("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(), QStringLiteral("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()); + AKVERIFYEXEC(job); + 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()); +} + +class ResultSignalTester : public QObject +{ + Q_OBJECT +public: + QStringList receivedSignals; +public Q_SLOTS: + void onCollectionsReceived(const Akonadi::Collection::List &) + { + receivedSignals << QStringLiteral("collectionsReceived"); + } + + void onCollectionRetrievalDone(KJob *) + { + receivedSignals << QStringLiteral("result"); + } +}; + +void CollectionJobTest::testSignalOrder() +{ + Akonadi::Collection::List toFetch; + toFetch << Collection(res1ColId); + toFetch << Collection(res2ColId); + CollectionFetchJob *job = new CollectionFetchJob(toFetch, CollectionFetchJob::Recursive); + ResultSignalTester spy; + connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), &spy, SLOT(onCollectionsReceived(Akonadi::Collection::List))); + connect(job, SIGNAL(result(KJob*)), &spy, SLOT(onCollectionRetrievalDone(KJob*))); + AKVERIFYEXEC(job); + + QCOMPARE(spy.receivedSignals.size(), 2); + QCOMPARE(spy.receivedSignals.at(0), QStringLiteral("collectionsReceived")); + QCOMPARE(spy.receivedSignals.at(1), QStringLiteral("result")); +} + +void CollectionJobTest::testNonRecursiveFolderList() +{ + CollectionFetchJob *job = new CollectionFetchJob(Collection(res1ColId), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + 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); + AKVERIFYEXEC(job); + Collection::List list = job->collections(); + + QCOMPARE(list.count(), 0); +} + +void CollectionJobTest::testSearchFolderList() +{ + CollectionFetchJob *job = new CollectionFetchJob(Collection(searchColId), CollectionFetchJob::FirstLevel); + AKVERIFYEXEC(job); + 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"); + AKVERIFYEXEC(job); + + 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"); + AKVERIFYEXEC(job); + + 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); + + // non-existing mimetype + job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + job->fetchScope().setContentMimeTypes(QStringList() << "something/non-existing"); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 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.parentCollection().setId(res2ColId); // Sibling of collection 'foo2' + col.setName("foo2 "); + QTest::newRow("name of an sibling with an additional ending space") << col << true; + + col.setName("Bla"); + col.parentCollection().setId(2); + QTest::newRow("already existing with different case") << col << true; + + CollectionPathResolver *resolver = new CollectionPathResolver("res2/foo2", this); + AKVERIFYEXEC(resolver); + 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); + AKVERIFYEXEC(statistics); + + CollectionStatistics s = statistics->statistics(); + QCOMPARE(s.count(), 0ll); + QCOMPARE(s.unreadCount(), 0ll); + + // folder with attributes and content + CollectionPathResolver *resolver = new CollectionPathResolver("res1/foo", this);; + AKVERIFYEXEC(resolver); + statistics = new CollectionStatisticsJob(Collection(resolver->collection()), this); + AKVERIFYEXEC(statistics); + + 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_data() +{ + QTest::addColumn("folderName"); + + QTest::newRow("Umlaut") << QString::fromUtf8("ä"); + QTest::newRow("Garbage") << QString::fromUtf8("đ→³}đþøæſð"); + QTest::newRow("Utf8") << QString::fromUtf8("日本語"); +} + +void CollectionJobTest::testUtf8CollectionName() +{ + QFETCH(QString, folderName); + + // create collection + Collection col; + col.parentCollection().setId(res3ColId); + col.setName(folderName); + CollectionCreateJob *create = new CollectionCreateJob(col, this); + AKVERIFYEXEC(create); + col = create->collection(); + QVERIFY(col.isValid()); + QCOMPARE(col.name(), folderName); + + // list parent + CollectionFetchJob *list = new CollectionFetchJob(Collection(res3ColId), CollectionFetchJob::Recursive, this); + AKVERIFYEXEC(list); + QCOMPARE(list->collections().count(), 1); + QCOMPARE(list->collections().first(), col); + QCOMPARE(list->collections().first().name(), col.name()); + + // modify collection + col.setContentMimeTypes(QStringList("message/rfc822'")); + CollectionModifyJob *modify = new CollectionModifyJob(col, this); + AKVERIFYEXEC(modify); + + // collection statistics + CollectionStatisticsJob *statistics = new CollectionStatisticsJob(col, this); + AKVERIFYEXEC(statistics); + CollectionStatistics s = statistics->statistics(); + QCOMPARE(s.count(), 0ll); + QCOMPARE(s.unreadCount(), 0ll); + + // delete collection + CollectionDeleteJob *del = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(del); +} + +void CollectionJobTest::testMultiList() +{ + Collection::List req; + req << Collection(res1ColId) << Collection(res2ColId); + CollectionFetchJob *job = new CollectionFetchJob(req, this); + AKVERIFYEXEC(job); + + Collection::List res; + res = job->collections(); + compareLists(res, req); +} + +void CollectionJobTest::testMultiListInvalid() +{ + Collection::List req; + req << Collection(res1ColId) << Collection(1234567) << Collection(res2ColId); + CollectionFetchJob *job = new CollectionFetchJob(req, this); + QVERIFY(!job->exec()); + // not all available collections are fetched + QVERIFY(job->collections().count() != 2); + + job = new CollectionFetchJob(req, this); + job->fetchScope().setIgnoreRetrievalErrors(true); + QVERIFY(!job->exec()); + Collection::List res; + res = job->collections(); + req = Collection::List() << Collection(res1ColId) << Collection(res2ColId); + compareLists(res, req); +} + +void CollectionJobTest::testRecursiveMultiList() +{ + Akonadi::Collection::List toFetch; + toFetch << Collection(res1ColId); + toFetch << Collection(res2ColId); + CollectionFetchJob *job = new CollectionFetchJob(toFetch, CollectionFetchJob::Recursive); + QSignalSpy spy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + QVERIFY(spy.isValid()); + AKVERIFYEXEC(job); + + 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 + 2); + QVERIFY(findCol(list, "foo").isValid()); + QVERIFY(findCol(list, "bar").isValid()); + QVERIFY(findCol(list, "bla").isValid()); //There are two bla folders, but we only check for one. + QVERIFY(findCol(list, "foo2").isValid()); + QVERIFY(findCol(list, "space folder").isValid()); +} + +void CollectionJobTest::testNonOverlappingRootList() +{ + Akonadi::Collection::List toFetch; + toFetch << Collection(res1ColId); + toFetch << Collection(res2ColId); + CollectionFetchJob *job = new CollectionFetchJob(toFetch, CollectionFetchJob::NonOverlappingRoots); + QSignalSpy spy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + QVERIFY(spy.isValid()); + AKVERIFYEXEC(job); + + 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(), 2); + QVERIFY(findCol(list, "res1").isValid()); + QVERIFY(findCol(list, "res2").isValid()); +} + +void CollectionJobTest::testRidFetch() +{ + Collection col; + col.setRemoteId("10"); + + CollectionFetchJob *job = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + job->fetchScope().setResource("akonadi_knut_resource_0"); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().count(), 1); + col = job->collections().first(); + QVERIFY(col.isValid()); + QCOMPARE(col.remoteId(), QString::fromLatin1("10")); +} + +void CollectionJobTest::testRidCreateDelete_data() +{ + QTest::addColumn("remoteId"); + QTest::newRow("ASCII") << QString::fromUtf8("MY REMOTE ID"); + QTest::newRow("LATIN1") << QString::fromUtf8("MY REMÖTE ID"); + QTest::newRow("UTF8") << QString::fromUtf8("MY REMOTE 検索表"); +} + +void CollectionJobTest::testRidCreateDelete() +{ + QFETCH(QString, remoteId); + Collection collection; + collection.setName("rid create"); + collection.parentCollection().setRemoteId("8"); + collection.setRemoteId(remoteId); + + 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); +} + +void CollectionJobTest::testAncestorAttributeRetrieval() +{ + Akonadi::Collection baseCol; + { + baseCol.setParentCollection(Akonadi::Collection(res1ColId)); + baseCol.setName("base"); + baseCol.attribute(Collection::AddIfMissing)->data = "new"; + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(baseCol); + AKVERIFYEXEC(create); + baseCol = create->collection(); + } + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setName("enabled"); + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + + CollectionFetchJob *job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + job->fetchScope().ancestorFetchScope().setFetchIdOnly(false); + job->fetchScope().ancestorFetchScope().fetchAttribute(); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.parentCollection().hasAttribute(), true); + } + + //Cleanup + CollectionDeleteJob *deleteJob = new CollectionDeleteJob(baseCol); + AKVERIFYEXEC(deleteJob); +} + +void CollectionJobTest::testListPreference() +{ + Akonadi::Collection baseCol; + { + baseCol.setParentCollection(Akonadi::Collection(res1ColId)); + baseCol.setName("base"); + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(baseCol); + AKVERIFYEXEC(create); + baseCol = create->collection(); + } + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setEnabled(true); + col.setName("enabled"); + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + + CollectionFetchJob *job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), true); + QCOMPARE(result.localListPreference(Collection::ListDisplay), Collection::ListDefault); + QCOMPARE(result.localListPreference(Collection::ListSync), Collection::ListDefault); + QCOMPARE(result.localListPreference(Collection::ListIndex), Collection::ListDefault); + } + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setName("disabledPref"); + col.setEnabled(true); + col.setLocalListPreference(Collection::ListDisplay, Collection::ListDisabled); + col.setLocalListPreference(Collection::ListSync, Collection::ListDisabled); + col.setLocalListPreference(Collection::ListIndex, Collection::ListDisabled); + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + CollectionFetchJob *job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), true); + QCOMPARE(result.localListPreference(Collection::ListDisplay), Collection::ListDisabled); + QCOMPARE(result.localListPreference(Collection::ListSync), Collection::ListDisabled); + QCOMPARE(result.localListPreference(Collection::ListIndex), Collection::ListDisabled); + } + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setName("enabledPref"); + col.setEnabled(false); + col.setLocalListPreference(Collection::ListDisplay, Collection::ListEnabled); + col.setLocalListPreference(Collection::ListSync, Collection::ListEnabled); + col.setLocalListPreference(Collection::ListIndex, Collection::ListEnabled); + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + CollectionFetchJob *job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), false); + QCOMPARE(result.localListPreference(Collection::ListDisplay), Collection::ListEnabled); + QCOMPARE(result.localListPreference(Collection::ListSync), Collection::ListEnabled); + QCOMPARE(result.localListPreference(Collection::ListIndex), Collection::ListEnabled); + } + + //Check list filter + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::FirstLevel); + job->fetchScope().setListFilter(CollectionFetchScope::Display); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 2); + } + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::FirstLevel); + job->fetchScope().setListFilter(CollectionFetchScope::Sync); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 2); + } + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::FirstLevel); + job->fetchScope().setListFilter(CollectionFetchScope::Index); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 2); + } + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::FirstLevel); + job->fetchScope().setListFilter(CollectionFetchScope::Enabled); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 2); + } + + //Cleanup + CollectionDeleteJob *deleteJob = new CollectionDeleteJob(baseCol); + AKVERIFYEXEC(deleteJob); +} + +void CollectionJobTest::testReference() +{ + Akonadi::Collection baseCol; + { + baseCol.setParentCollection(Akonadi::Collection(res1ColId)); + baseCol.setName("base"); + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(baseCol); + AKVERIFYEXEC(create); + baseCol = create->collection(); + } + + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setName("referenced"); + col.setEnabled(false); + { + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + CollectionFetchJob *job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + col = job->collections().first(); + } + { + col.setReferenced(true); + Akonadi::CollectionModifyJob *modify = new Akonadi::CollectionModifyJob(col); + AKVERIFYEXEC(modify); + CollectionFetchJob *job = new CollectionFetchJob(col, CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), false); + QCOMPARE(result.referenced(), true); + } + { + col.setReferenced(false); + Akonadi::CollectionModifyJob *modify = new Akonadi::CollectionModifyJob(col); + AKVERIFYEXEC(modify); + CollectionFetchJob *job = new CollectionFetchJob(col, CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), false); + QCOMPARE(result.referenced(), false); + } + } + + //Cleanup + CollectionDeleteJob *deleteJob = new CollectionDeleteJob(baseCol); + AKVERIFYEXEC(deleteJob); +} + +#include "collectionjobtest.moc" diff --git a/autotests/libs/collectionjobtest.h b/autotests/libs/collectionjobtest.h new file mode 100644 index 0000000..066c963 --- /dev/null +++ b/autotests/libs/collectionjobtest.h @@ -0,0 +1,60 @@ +/* + 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. +*/ + +#ifndef COLLECTIONJOBTEST_H +#define COLLECTIONJOBTEST_H + +#include + +class CollectionJobTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testTopLevelList(); + void testFolderList(); + void testSignalOrder(); + void testNonRecursiveFolderList(); + void testEmptyFolderList(); + void testSearchFolderList(); + void testResourceFolderList(); + void testMimeTypeFilter(); + void testCreateDeleteFolder_data(); + void testCreateDeleteFolder(); + void testIllegalDeleteFolder(); + void testStatistics(); + void testModify_data(); + void testModify(); + void testIllegalModify(); + void testUtf8CollectionName_data(); + void testUtf8CollectionName(); + void testMultiList(); + void testMultiListInvalid(); + void testRecursiveMultiList(); + void testNonOverlappingRootList(); + void testRidFetch(); + void testRidCreateDelete_data(); + void testRidCreateDelete(); + void testAncestorRetrieval(); + void testAncestorAttributeRetrieval(); + void testListPreference(); + void testReference(); +}; + +#endif diff --git a/autotests/libs/collectionmodifytest.cpp b/autotests/libs/collectionmodifytest.cpp new file mode 100644 index 0000000..24bcf59 --- /dev/null +++ b/autotests/libs/collectionmodifytest.cpp @@ -0,0 +1,81 @@ +/* + * Copyright 2015 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "test_utils.h" +#include "collectioncreatejob.h" +#include "collectionfetchjob.h" +#include "collectionmodifyjob.h" +#include "collectiondeletejob.h" +#include "entitydisplayattribute.h" + +using namespace Akonadi; + +class CollectionModifyTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testModifyCollection() + { + Collection col; + col.setName(QLatin1String("test_collection")); + col.setContentMimeTypes({ Collection::mimeType() }); + col.setParentCollection(Collection(collectionIdFromPath(QLatin1String("res1")))); + col.setRights(Collection::AllRights); + + CollectionCreateJob *cj = new CollectionCreateJob(col, this); + AKVERIFYEXEC(cj); + col = cj->collection(); + QVERIFY(col.isValid()); + + auto attr = col.attribute(Entity::AddIfMissing); + attr->setDisplayName(QLatin1String("Test Collection")); + col.setContentMimeTypes({ Collection::mimeType(), QLatin1String("application/octet-stream") }); + + CollectionModifyJob *mj = new CollectionModifyJob(col, this); + AKVERIFYEXEC(mj); + + CollectionFetchJob *fj = new CollectionFetchJob(col, CollectionFetchJob::Base); + AKVERIFYEXEC(fj); + QCOMPARE(fj->collections().count(), 1); + const Collection actual = fj->collections().at(0); + + QCOMPARE(actual.id(), col.id()); + QCOMPARE(actual.name(), col.name()); + QCOMPARE(actual.displayName(), col.displayName()); + QCOMPARE(actual.contentMimeTypes(), col.contentMimeTypes()); + QCOMPARE(actual.parentCollection(), col.parentCollection()); + QCOMPARE(actual.rights(), col.rights()); + + CollectionDeleteJob *dj = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(dj); + } +}; + +QTEST_AKONADIMAIN(CollectionModifyTest) + +#include "collectionmodifytest.moc" + diff --git a/autotests/libs/collectionmovetest.cpp b/autotests/libs/collectionmovetest.cpp new file mode 100644 index 0000000..0a1bfe7 --- /dev/null +++ b/autotests/libs/collectionmovetest.cpp @@ -0,0 +1,151 @@ +/* + Copyright (c) 2006, 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 "collection.h" +#include "collectionfetchjob.h" +#include "collectionmovejob.h" +#include "item.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" + +#include + +using namespace Akonadi; + +class CollectionMoveTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + void testIllegalMove_data() + { + QTest::addColumn("source"); + QTest::addColumn("destination"); + + const Collection res1(collectionIdFromPath(QStringLiteral("res1"))); + const Collection res1foo(collectionIdFromPath(QStringLiteral("res1/foo"))); + const Collection res1bla(collectionIdFromPath(QStringLiteral("res1/foo/bar/bla"))); + + QTest::newRow("non-existing-target") << res1 << Collection(INT_MAX); + QTest::newRow("root") << Collection::root() << res1; + QTest::newRow("move-into-child") << res1 << res1foo; + QTest::newRow("same-name-in-target") << res1bla << res1foo; + QTest::newRow("non-existing-source") << Collection(INT_MAX) << res1; + } + + void testIllegalMove() + { + QFETCH(Collection, source); + QFETCH(Collection, destination); + QVERIFY(source.isValid()); + QVERIFY(destination.isValid()); + + CollectionMoveJob *mod = new CollectionMoveJob(source, destination, this); + QVERIFY(!mod->exec()); + } + + void testMove_data() + { + QTest::addColumn("source"); + QTest::addColumn("destination"); + QTest::addColumn("crossResource"); + + QTest::newRow("inter-resource") << Collection(collectionIdFromPath(QStringLiteral("res1"))) + << Collection(collectionIdFromPath(QStringLiteral("res2"))) + << true; + QTest::newRow("intra-resource") << Collection(collectionIdFromPath(QStringLiteral("res1/foo/bla"))) + << Collection(collectionIdFromPath(QStringLiteral("res1/foo/bar/bla"))) + << false; + } + + // TODO: test signals + void testMove() + { + QFETCH(Collection, source); + QFETCH(Collection, destination); + QFETCH(bool, crossResource); + QVERIFY(source.isValid()); + QVERIFY(destination.isValid()); + + CollectionFetchJob *fetch = new CollectionFetchJob(source, CollectionFetchJob::Base, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->collections().count(), 1); + source = fetch->collections().first(); + + // obtain reference listing + fetch = new CollectionFetchJob(source, CollectionFetchJob::Recursive); + AKVERIFYEXEC(fetch); + QHash referenceData; + foreach (const Collection &c, fetch->collections()) { + ItemFetchJob *job = new ItemFetchJob(c, this); + AKVERIFYEXEC(job); + referenceData.insert(c, job->items()); + } + + // move collection + CollectionMoveJob *mod = new CollectionMoveJob(source, destination, this); + AKVERIFYEXEC(mod); + + // check if source was modified correctly + CollectionFetchJob *ljob = new CollectionFetchJob(source, CollectionFetchJob::Base); + AKVERIFYEXEC(ljob); + Collection::List list = ljob->collections(); + + QCOMPARE(list.count(), 1); + Collection col = list.first(); + QCOMPARE(col.name(), source.name()); + QCOMPARE(col.parentCollection(), destination); + + // list destination and check if everything is still there + ljob = new CollectionFetchJob(destination, CollectionFetchJob::Recursive); + AKVERIFYEXEC(ljob); + list = ljob->collections(); + + QVERIFY(list.count() >= referenceData.count()); + for (QHash::ConstIterator it = referenceData.constBegin(); it != referenceData.constEnd(); ++it) { + QVERIFY(list.contains(it.key())); + if (crossResource) { + QVERIFY(list[list.indexOf(it.key())].resource() != it.key().resource()); + } else { + QCOMPARE(list[list.indexOf(it.key())].resource(), it.key().resource()); + } + ItemFetchJob *job = new ItemFetchJob(it.key(), this); + job->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), it.value().count()); + foreach (const Item &item, job->items()) { + QVERIFY(it.value().contains(item)); + QVERIFY(item.hasPayload()); + } + } + + // cleanup + mod = new CollectionMoveJob(col, source.parentCollection(), this); + AKVERIFYEXEC(mod); + } +}; + +QTEST_AKONADIMAIN(CollectionMoveTest) + +#include "collectionmovetest.moc" diff --git a/autotests/libs/collectionpathresolvertest.cpp b/autotests/libs/collectionpathresolvertest.cpp new file mode 100644 index 0000000..2b624de --- /dev/null +++ b/autotests/libs/collectionpathresolvertest.cpp @@ -0,0 +1,67 @@ +/* + 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 "collectionpathresolvertest.h" +#include "collectionpathresolver.h" + +#include + +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(CollectionPathResolverTest) + +void CollectionPathResolverTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); +} + +void CollectionPathResolverTest::testPathResolver() +{ + CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("/res1/foo/bar/bla"), this); + AKVERIFYEXEC(resolver); + int col = resolver->collection(); + QVERIFY(col > 0); + + resolver = new CollectionPathResolver(Collection(col), this); + AKVERIFYEXEC(resolver); + QCOMPARE(resolver->path(), QStringLiteral("res1/foo/bar/bla")); +} + +void CollectionPathResolverTest::testRoot() +{ + CollectionPathResolver *resolver = new CollectionPathResolver(CollectionPathResolver::pathDelimiter(), this); + AKVERIFYEXEC(resolver); + QCOMPARE(resolver->collection(), Collection::root().id()); + + resolver = new CollectionPathResolver(Collection::root(), this); + AKVERIFYEXEC(resolver); + QVERIFY(resolver->path().isEmpty()); +} + +void CollectionPathResolverTest::testFailure() +{ + CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("/I/do not/exist"), this); + QVERIFY(!resolver->exec()); + + resolver = new CollectionPathResolver(Collection(INT_MAX), this); + QVERIFY(!resolver->exec()); +} diff --git a/autotests/libs/collectionpathresolvertest.h b/autotests/libs/collectionpathresolvertest.h new file mode 100644 index 0000000..73ec2ac --- /dev/null +++ b/autotests/libs/collectionpathresolvertest.h @@ -0,0 +1,35 @@ +/* + 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 COLLECTIONPATHRESOLVER_TEST_H +#define COLLECTIONPATHRESOLVER_TEST_H + +#include + +class CollectionPathResolverTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testPathResolver(); + void testRoot(); + void testFailure(); +}; + +#endif diff --git a/autotests/libs/collectionsynctest.cpp b/autotests/libs/collectionsynctest.cpp new file mode 100644 index 0000000..32e02ad --- /dev/null +++ b/autotests/libs/collectionsynctest.cpp @@ -0,0 +1,443 @@ +/* + Copyright (c) 2009 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#include "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../src/core/collectionsync.cpp" + +#include + +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +class CollectionSyncTest : public QObject +{ + Q_OBJECT +private: + Collection::List fetchCollections(const QString &res) + { + CollectionFetchJob *fetch = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + fetch->fetchScope().setResource(res); + fetch->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + if (!fetch->exec()) { + qWarning() << "CollectionFetchJob failed!"; + return Collection::List(); + } + return fetch->collections(); + } + + void makeTestData() + { + QTest::addColumn("hierarchicalRIDs"); + QTest::addColumn("resource"); + + QTest::newRow("akonadi_knut_resource_0 global RID") << false << "akonadi_knut_resource_0"; + QTest::newRow("akonadi_knut_resource_1 global RID") << false << "akonadi_knut_resource_1"; + QTest::newRow("akonadi_knut_resource_2 global RID") << false << "akonadi_knut_resource_2"; + + QTest::newRow("akonadi_knut_resource_0 hierarchical RID") << true << "akonadi_knut_resource_0"; + QTest::newRow("akonadi_knut_resource_1 hierarchical RID") << true << "akonadi_knut_resource_1"; + QTest::newRow("akonadi_knut_resource_2 hierarchical RID") << true << "akonadi_knut_resource_2"; + } + + Collection createCollection(const QString &name, const QString &remoteId, const Collection &parent) + { + Collection c; + c.setName(name); + c.setRemoteId(remoteId); + c.setParentCollection(parent); + c.setResource(QStringLiteral("akonadi_knut_resource_0")); + c.setContentMimeTypes(QStringList() << Collection::mimeType()); + return c; + } + + Collection::List prepareBenchmark() + { + Collection::List collections = fetchCollections(QStringLiteral("akonadi_knut_resource_0")); + + ResourceSelectJob *resJob = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + Q_ASSERT(resJob->exec()); + + Collection root; + Q_FOREACH (const Collection &col, collections) { + if (col.parentCollection() == Collection::root()) { + root = col; + break; + } + } + Q_ASSERT(root.isValid()); + + // we must build on top of existing collections, because only resource is + // allowed to create top-level collection + Collection::List baseCollections; + for (int i = 0; i < 20; ++i) { + baseCollections << createCollection(QString::fromLatin1("Base Col %1").arg(i), QString::fromLatin1("/baseCol%1").arg(i), root); + } + collections += baseCollections; + + const Collection shared = createCollection(QStringLiteral("Shared collections"), QStringLiteral("/shared"), root); + baseCollections << shared; + collections << shared; + for (int i = 0; i < 10000; ++i) { + const Collection col = createCollection(QString::fromLatin1("Shared Col %1").arg(i), QString::fromLatin1("/shared%1").arg(i), shared); + collections << col; + for (int j = 0; j < 6; ++j) { + collections << createCollection(QString::fromLatin1("Shared Subcol %1-%2").arg(i).arg(j), QString::fromLatin1("/shared%1-%2").arg(i).arg(j), col); + } + } + return collections; + } + + CollectionSync *prepareBenchmarkSyncer(const Collection::List &collections) + { + CollectionSync *syncer = new CollectionSync(QStringLiteral("akonadi_knut_resource_0")); + connect(syncer, SIGNAL(percent(KJob*,ulong)), this, SLOT(syncBenchmarkProgress(KJob*,ulong))); + syncer->setHierarchicalRemoteIds(false); + syncer->setRemoteCollections(collections); + return syncer; + } + + void cleanupBenchmark(const Collection::List &collections) + { + Collection::List baseCols; + Q_FOREACH (const Collection &col, collections) { + if (col.remoteId().startsWith(QLatin1String("/baseCol")) || col.remoteId() == QLatin1String("/shared")) { + baseCols << col; + } + } + Q_FOREACH (const Collection &col, baseCols) { + CollectionDeleteJob *del = new CollectionDeleteJob(col); + AKVERIFYEXEC(del); + } + } + +public Q_SLOTS: + void syncBenchmarkProgress(KJob *job, ulong percent) + { + Q_UNUSED(job); + qDebug() << "CollectionSync progress:" << percent << "%"; + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + qRegisterMetaType(); + } + + void testFullSync_data() + { + makeTestData(); + } + + void testFullSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + CollectionSync *syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setRemoteCollections(origCols); + AKVERIFYEXEC(syncer); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + } + + void testFullStreamingSync_data() + { + makeTestData(); + } + + void testFullStreamingSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + CollectionSync *syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setAutoDelete(false); + QSignalSpy spy(syncer, SIGNAL(result(KJob*))); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + QTest::qWait(10); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < origCols.count(); ++i) { + Collection::List l; + l << origCols[i]; + syncer->setRemoteCollections(l); + if (i < origCols.count() - 1) { + QTest::qWait(10); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + syncer->retrievalDone(); + QTRY_COMPARE(spy.count(), 1); + QCOMPARE(spy.count(), 1); + KJob *job = spy.at(0).at(0).value(); + QCOMPARE(job, syncer); + QCOMPARE(job->errorText(), QString()); + QCOMPARE(job->error(), 0); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + + delete syncer; + } + + void testIncrementalSync_data() + { + makeTestData(); + } + + void testIncrementalSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + if (resource == QLatin1String("akonadi_knut_resource_2")) { + QSKIP("test requires more than one collection", SkipSingle); + } + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + CollectionSync *syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setRemoteCollections(origCols, Collection::List()); + AKVERIFYEXEC(syncer); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + + // Find leaf collections that we can delete + Collection::List leafCols = resultCols; + for (auto iter = leafCols.begin(); iter != leafCols.end();) { + bool found = false; + Q_FOREACH (const Collection &c, resultCols) { + if (c.parentCollection().id() == iter->id()) { + iter = leafCols.erase(iter); + found = true; + break; + } + } + if (!found) { + ++iter; + } + } + QVERIFY(!leafCols.isEmpty()); + Collection::List delCols; + delCols << leafCols.first(); + resultCols.removeOne(leafCols.first()); + + // ### not implemented yet I guess +#if 0 + Collection colWithOnlyRemoteId; + colWithOnlyRemoteId.setRemoteId(resultCols.front().remoteId()); + delCols << colWithOnlyRemoteId; + resultCols.pop_front(); +#endif + +#if 0 + // ### should this work? + Collection colWithRandomRemoteId; + colWithRandomRemoteId.setRemoteId(KRandom::randomString(100)); + delCols << colWithRandomRemoteId; +#endif + + syncer = new CollectionSync(resource, this); + syncer->setRemoteCollections(resultCols, delCols); + AKVERIFYEXEC(syncer); + + Collection::List resultCols2 = fetchCollections(resource); + QCOMPARE(resultCols2.count(), resultCols.count()); + } + + void testIncrementalStreamingSync_data() + { + makeTestData(); + } + + void testIncrementalStreamingSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + CollectionSync *syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setAutoDelete(false); + QSignalSpy spy(syncer, SIGNAL(result(KJob*))); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + QTest::qWait(10); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < origCols.count(); ++i) { + Collection::List l; + l << origCols[i]; + syncer->setRemoteCollections(l, Collection::List()); + if (i < origCols.count() - 1) { + QTest::qWait(10); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + syncer->retrievalDone(); + QTRY_COMPARE(spy.count(), 1); + KJob *job = spy.at(0).at(0).value(); + QCOMPARE(job, syncer); + QCOMPARE(job->errorText(), QString()); + QCOMPARE(job->error(), 0); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + + delete syncer; + } + + void testEmptyIncrementalSync_data() + { + makeTestData(); + } + + void testEmptyIncrementalSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + CollectionSync *syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setRemoteCollections(Collection::List(), Collection::List()); + AKVERIFYEXEC(syncer); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + } + + void testAttributeChanges_data() + { + QTest::addColumn("keepLocalChanges"); + QTest::newRow("keep local changes") << true; + QTest::newRow("overwrite local changes") << false; + } + + void testAttributeChanges() + { + QFETCH(bool, keepLocalChanges); + const QString resource(QStringLiteral("akonadi_knut_resource_0")); + Collection col = fetchCollections(resource).first(); + col.attribute(Akonadi::Collection::AddIfMissing)->setDisplayName(QStringLiteral("foo")); + col.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType() << QStringLiteral("foo")); + { + CollectionModifyJob *job = new CollectionModifyJob(col); + AKVERIFYEXEC(job); + } + + col.attribute()->setDisplayName(QStringLiteral("default")); + col.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType() << QStringLiteral("default")); + + CollectionSync *syncer = new CollectionSync(resource, this); + if (keepLocalChanges) { + syncer->setKeepLocalChanges(QSet() << "ENTITYDISPLAY" << "CONTENTMIMETYPES"); + } else { + syncer->setKeepLocalChanges(QSet()); + } + + syncer->setRemoteCollections(Collection::List() << col, Collection::List()); + AKVERIFYEXEC(syncer); + + { + CollectionFetchJob *job = new CollectionFetchJob(col, Akonadi::CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Collection resultCol = job->collections().first(); + if (keepLocalChanges) { + QCOMPARE(resultCol.displayName(), QString::fromLatin1("foo")); + QVERIFY(resultCol.contentMimeTypes().contains(QStringLiteral("foo"))); + } else { + QCOMPARE(resultCol.displayName(), QString::fromLatin1("default")); + QVERIFY(resultCol.contentMimeTypes().contains(QStringLiteral("default"))); + } + } + } + +// Disabled by default, because they take ~15 minutes to complete +#if 0 + void benchmarkInitialSync() + { + const Collection::List collections = prepareBenchmark(); + + CollectionSync *syncer = prepareBenchmarkSyncer(collections); + + QBENCHMARK_ONCE { + AKVERIFYEXEC(syncer); + } + + cleanupBenchmark(collections); + } + + void benchmarkIncrementalSync() + { + const Collection::List collections = prepareBenchmark(); + + // First populate Akonadi with Collections + CollectionSync *syncer = prepareBenchmarkSyncer(collections); + AKVERIFYEXEC(syncer); + + // Now create a new syncer to benchmark the incremental sync + syncer = prepareBenchmarkSyncer(collections); + + QBENCHMARK_ONCE { + AKVERIFYEXEC(syncer); + } + + cleanupBenchmark(collections); + } +#endif +}; + +QTEST_AKONADIMAIN(CollectionSyncTest) + +#include "collectionsynctest.moc" diff --git a/autotests/libs/collectionutilstest.cpp b/autotests/libs/collectionutilstest.cpp new file mode 100644 index 0000000..7d8ec94 --- /dev/null +++ b/autotests/libs/collectionutilstest.cpp @@ -0,0 +1,79 @@ +/* + 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 +#include "collection.h" +#include "../collectionutils.h" + +using namespace Akonadi; + +class CollectionUtilsTest : public QObject +{ + Q_OBJECT +private Q_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(QStringLiteral("r1")); + QTest::newRow("one level ok") << c << true; + Collection c2; + c2.setParentCollection(c); + QTest::newRow("two level not ok") << c2 << false; + c2.setRemoteId(QStringLiteral("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); + } + + void testPersistentParentCollection() + { + Collection col1(1); + Collection col2(2); + Collection col3(3); + + col2.setParentCollection(col3); + col1.setParentCollection(col2); + + Collection assigned = col1; + QCOMPARE(assigned.parentCollection(), col2); + QCOMPARE(assigned.parentCollection().parentCollection(), col3); + + Collection copied(col1); + QCOMPARE(copied.parentCollection(), col2); + QCOMPARE(copied.parentCollection().parentCollection(), col3); + } +}; + +QTEST_AKONADIMAIN(CollectionUtilsTest) + +#include "collectionutilstest.moc" diff --git a/autotests/libs/entitycachetest.cpp b/autotests/libs/entitycachetest.cpp new file mode 100644 index 0000000..55a77c4 --- /dev/null +++ b/autotests/libs/entitycachetest.cpp @@ -0,0 +1,171 @@ +/* + 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_p.h" + +#include +#include + +using namespace Akonadi; + +class EntityCacheTest : public QObject +{ + Q_OBJECT +private: + template + void testCache() + { + EntityCache cache(2); + QSignalSpy spy(&cache, SIGNAL(dataAvailable())); + QVERIFY(spy.isValid()); + + QVERIFY(!cache.isCached(1)); + QVERIFY(!cache.isRequested(1)); + QVERIFY(!cache.retrieve(1).isValid()); + + FetchScope scope; + scope.setAncestorRetrieval(FetchScope::All); + + cache.request(1, scope); + QVERIFY(!cache.isCached(1)); + QVERIFY(cache.isRequested(1)); + QVERIFY(!cache.retrieve(1).isValid()); + + QTRY_COMPARE(spy.count(), 1); + QVERIFY(cache.isCached(1)); + QVERIFY(cache.isRequested(1)); + const T e1 = cache.retrieve(1); + QCOMPARE(e1.id(), 1ll); + QVERIFY(e1.parentCollection().isValid()); + QVERIFY(!e1.parentCollection().remoteId().isEmpty() || e1.parentCollection() == Collection::root()); + + spy.clear(); + cache.request(2, FetchScope()); + cache.request(3, FetchScope()); + + QVERIFY(!cache.isCached(1)); + QVERIFY(!cache.isRequested(1)); + QVERIFY(cache.isRequested(2)); + QVERIFY(cache.isRequested(3)); + + cache.invalidate(2); + + QTRY_COMPARE(spy.count(), 2); + QVERIFY(cache.isCached(2)); + QVERIFY(cache.isCached(3)); + + const T e2 = cache.retrieve(2); + const T e3a = cache.retrieve(3); + QCOMPARE(e3a.id(), 3ll); + QVERIFY(!e2.isValid()); + + cache.invalidate(3); + const T e3b = cache.retrieve(3); + QVERIFY(!e3b.isValid()); + + spy.clear(); + // updating a cached entry removes it + cache.update(3, FetchScope()); + cache.update(3, FetchScope()); + QVERIFY(!cache.isCached(3)); + QVERIFY(!cache.isRequested(3)); + QVERIFY(!cache.retrieve(3).isValid()); + + // updating a pending entry re-fetches + cache.request(3, FetchScope()); + cache.update(3, FetchScope()); + QVERIFY(!cache.isCached(3)); + QVERIFY(cache.isRequested(3)); + cache.update(3, FetchScope()); + QVERIFY(!cache.isCached(3)); + QVERIFY(cache.isRequested(3)); + + QTRY_COMPARE(spy.count(), 3); + QVERIFY(cache.isCached(3)); + QVERIFY(cache.retrieve(3).isValid()); + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + void testCacheGeneric_data() + { + QTest::addColumn("collection"); + QTest::newRow("collection") << true; + QTest::newRow("item") << false; + } + + void testCacheGeneric() + { + QFETCH(bool, collection); + if (collection) { + testCache(); + } else { + testCache(); + } + } + + void testListCacheGeneric_data() + { + QTest::addColumn("collection"); + QTest::newRow("collection") << true; + QTest::newRow("item") << false; + } + + void testItemCache() + { + ItemCache cache(1); + QSignalSpy spy(&cache, SIGNAL(dataAvailable())); + QVERIFY(spy.isValid()); + + ItemFetchScope scope; + scope.fetchFullPayload(true); + cache.request(1, scope); + + QTRY_COMPARE(spy.count(), 1); + QVERIFY(cache.isCached(1)); + QVERIFY(cache.isRequested(1)); + const Item item = cache.retrieve(1); + QCOMPARE(item.id(), 1ll); + QVERIFY(item.hasPayload()); + } + + void testListCache_ensureCached() + { + ItemFetchScope scope; + + EntityListCache cache(3); + QSignalSpy spy(&cache, SIGNAL(dataAvailable())); + QVERIFY(spy.isValid()); + + cache.request(QList() << 1 << 2 << 3, scope); + QTRY_COMPARE(spy.count(), 1); + QVERIFY(cache.isCached(QList() << 1 << 2 << 3)); + + cache.ensureCached(QList() << 1 << 2 << 3 << 4, scope); + QTRY_COMPARE(spy.count(), 2); + QVERIFY(cache.isCached(QList() << 1 << 2 << 3 << 4)); + } +}; + +QTEST_AKONADIMAIN(EntityCacheTest) + +#include "entitycachetest.moc" diff --git a/autotests/libs/entitydisplayattributetest.cpp b/autotests/libs/entitydisplayattributetest.cpp new file mode 100644 index 0000000..1fcf9ca --- /dev/null +++ b/autotests/libs/entitydisplayattributetest.cpp @@ -0,0 +1,72 @@ +/* + 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 "entitydisplayattribute.h" + +#include + +#include + +using namespace Akonadi; + +class EntityDisplayAttributeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDeserialize_data() + { + QTest::addColumn("input"); + QTest::addColumn("name"); + QTest::addColumn("icon"); + QTest::addColumn("activeIcon"); + QTest::addColumn("output"); + + QTest::newRow("empty") << QByteArray("(\"\" \"\")") << QString() << QString() << QString() << QByteArray("(\"\" \"\" \"\" ())"); + QTest::newRow("name+icon") << QByteArray("(\"name\" \"icon\")") << QStringLiteral("name") << QStringLiteral("icon") << QString() << QByteArray("(\"name\" \"icon\" \"\" ())"); + QTest::newRow("name+icon+activeIcon") << QByteArray("(\"name\" \"icon\" \"activeIcon\")") << QStringLiteral("name") << QStringLiteral("icon") << QStringLiteral("activeIcon") << QByteArray("(\"name\" \"icon\" \"activeIcon\" ())"); + } + + void testDeserialize() + { + QFETCH(QByteArray, input); + QFETCH(QString, name); + QFETCH(QString, icon); + QFETCH(QString, activeIcon); + QFETCH(QByteArray, output); + + EntityDisplayAttribute *attr = new EntityDisplayAttribute(); + attr->deserialize(input); + QCOMPARE(attr->displayName(), name); + QCOMPARE(attr->iconName(), icon); + QCOMPARE(attr->activeIconName(), activeIcon); + + QCOMPARE(attr->serialized(), output); + + EntityDisplayAttribute *copy = attr->clone(); + QCOMPARE(copy->serialized(), output); + + delete attr; + delete copy; + } +}; + +QTEST_MAIN(EntityDisplayAttributeTest) + +#include "entitydisplayattributetest.moc" + diff --git a/autotests/libs/entitytreemodeltest.cpp b/autotests/libs/entitytreemodeltest.cpp new file mode 100644 index 0000000..ef34ad3 --- /dev/null +++ b/autotests/libs/entitytreemodeltest.cpp @@ -0,0 +1,698 @@ +/* + 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 + +#include "fakeserverdata.h" +#include "fakesession.h" +#include "fakemonitor.h" +#include "modelspy.h" + +#include "imapparser_p.h" + +#include "entitytreemodel.h" +#include +#include + +static const QString serverContent1 = QStringLiteral( + // The format of these lines are first a type, either 'C' or 'I' for Item and collection. + // The dashes show the depth in the heirarchy + // Collections have a list of mimetypes they can contain, followed by an optional + // displayName which is put into the EntityDisplayAttribute, followed by an optional order + // which is the order in which the collections are returned from the job to the ETM. + + "- C (inode/directory) 'Col 1' 4" + "- - C (text/directory, message/rfc822) 'Col 2' 3" + // Items just have the mimetype they contain in the payload. + "- - - I text/directory 'Item 1'" + "- - - I text/directory 'Item 2'" + "- - - I message/rfc822 'Item 3'" + "- - - I message/rfc822 'Item 4'" + "- - C (text/directory) 'Col 3' 3" + "- - - C (text/directory) 'Col 4' 2" + "- - - - C (text/directory) 'Col 5' 1" // <-- First collection to be returned + "- - - - - I text/directory 'Item 5'" + "- - - - - I text/directory 'Item 6'" + "- - - - I text/directory 'Item 7'" + "- - - I text/directory 'Item 8'" + "- - - I text/directory 'Item 9'" + "- - C (message/rfc822) 'Col 6' 3" + "- - - I message/rfc822 'Item 10'" + "- - - I message/rfc822 'Item 11'" + "- - C (text/directory, message/rfc822) 'Col 7' 3" + "- - - I text/directory 'Item 12'" + "- - - I text/directory 'Item 13'" + "- - - I message/rfc822 'Item 14'" + "- - - I message/rfc822 'Item 15'"); + +/** + * This test verifies that the ETM reacts as expected to signals from the monitor. + * + * The tested ETM is only talking to fake components so the reaction of the ETM to each signal can be tested. + * + * WARNING: This test does no handle jobs issued by the model. It simply shortcuts (calls emitResult) them, and the connected + * slots are never executed (because the eventloop is not run after emitResult is called). + * This test therefore only tests the reaction of the model to signals of the monitor and not the overall behaviour. + */ +class EntityTreeModelTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void testInitialFetch(); + void testCollectionMove_data(); + void testCollectionMove(); + void testCollectionAdded_data(); + void testCollectionAdded(); + void testCollectionRemoved_data(); + void testCollectionRemoved(); + void testCollectionChanged_data(); + void testCollectionChanged(); + void testItemMove_data(); + void testItemMove(); + void testItemAdded_data(); + void testItemAdded(); + void testItemRemoved_data(); + void testItemRemoved(); + void testItemChanged_data(); + void testItemChanged(); + void testRemoveCollectionOnChanged(); + +private: + ExpectedSignal getExpectedSignal(SignalType type, int start, int end, const QVariantList newData) + { + return getExpectedSignal(type, start, end, QVariant(), newData); + } + + ExpectedSignal getExpectedSignal(SignalType type, int start, int end, const QVariant &parentData = QVariant(), const QVariantList newData = QVariantList()) + { + ExpectedSignal expectedSignal; + expectedSignal.signalType = type; + expectedSignal.startRow = start; + expectedSignal.endRow = end; + expectedSignal.parentData = parentData; + expectedSignal.newData = newData; + return expectedSignal; + } + + ExpectedSignal getExpectedSignal(SignalType type, int start, int end, const QVariant &sourceParentData, int destRow, const QVariant &destParentData, const QVariantList newData) + { + ExpectedSignal expectedSignal; + expectedSignal.signalType = type; + expectedSignal.startRow = start; + expectedSignal.endRow = end; + expectedSignal.sourceParentData = sourceParentData; + expectedSignal.destRow = destRow; + expectedSignal.parentData = destParentData; + expectedSignal.newData = newData; + return expectedSignal; + } + + QPair populateModel(const QString &serverContent, const QString &mimeType = QString()) + { + FakeMonitor *fakeMonitor = new FakeMonitor(this); + + fakeMonitor->setSession(m_fakeSession); + fakeMonitor->setCollectionMonitored(Collection::root()); + if (!mimeType.isEmpty()) { + fakeMonitor->setMimeTypeMonitored(mimeType); + } + EntityTreeModel *model = new EntityTreeModel(fakeMonitor, this); + + m_modelSpy = new ModelSpy(this); + m_modelSpy->setModel(model); + + FakeServerData *serverData = new FakeServerData(model, m_fakeSession, fakeMonitor); + QList initialFetchResponse = FakeJobResponse::interpret(serverData, serverContent); + serverData->setCommands(initialFetchResponse); + + // Give the model a chance to populate + QTest::qWait(100); + return qMakePair(serverData, model); + } + +private: + ModelSpy *m_modelSpy; + FakeSession *m_fakeSession; + QByteArray m_sessionName; + +}; + +void EntityTreeModelTest::initTestCase() +{ + m_sessionName = "EntityTreeModelTest fake session"; + m_fakeSession = new FakeSession(m_sessionName, FakeSession::EndJobsImmediately); + m_fakeSession->setAsDefaultSession(); + + qRegisterMetaType("QModelIndex"); +} + +void EntityTreeModelTest::testInitialFetch() +{ + FakeMonitor *fakeMonitor = new FakeMonitor(this); + + fakeMonitor->setSession(m_fakeSession); + fakeMonitor->setCollectionMonitored(Collection::root()); + EntityTreeModel *model = new EntityTreeModel(fakeMonitor, this); + + FakeServerData *serverData = new FakeServerData(model, m_fakeSession, fakeMonitor); + QList initialFetchResponse = FakeJobResponse::interpret(serverData, serverContent1); + serverData->setCommands(initialFetchResponse); + + m_modelSpy = new ModelSpy(this); + m_modelSpy->setModel(model); + m_modelSpy->startSpying(); + + QList expectedSignals; + + // First the model gets a signal about the first collection to be returned, which is not a top-level collection. + // It uses the parentCollection heirarchy to put placeholder collections in the model until the root is reached. + // Then it inserts only one row and emits the correct signals. After that, when the other collections + // arrive, dataChanged is emitted for them. + + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 0); + expectedSignals << getExpectedSignal(RowsInserted, 0, 0); + expectedSignals << getExpectedSignal(DataChanged, 0, 0, QVariantList() << QStringLiteral("Col 4")); + expectedSignals << getExpectedSignal(DataChanged, 0, 0, QVariantList() << QStringLiteral("Col 3")); + // New collections are prepended + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 0, QStringLiteral("Collection 1")); + expectedSignals << getExpectedSignal(RowsInserted, 0, 0, QStringLiteral("Collection 1"), QVariantList() << QStringLiteral("Col 2")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 0, QStringLiteral("Collection 1")); + expectedSignals << getExpectedSignal(RowsInserted, 0, 0, QStringLiteral("Collection 1"), QVariantList() << QStringLiteral("Col 6")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 0, QStringLiteral("Collection 1")); + expectedSignals << getExpectedSignal(RowsInserted, 0, 0, QStringLiteral("Collection 1"), QVariantList() << QStringLiteral("Col 7")); + expectedSignals << getExpectedSignal(DataChanged, 0, 0, QVariantList() << QStringLiteral("Col 1")); + // The items in the collections are appended. + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 3, QStringLiteral("Col 2")); + expectedSignals << getExpectedSignal(RowsInserted, 0, 3, QStringLiteral("Col 2")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 1, QStringLiteral("Col 5")); + expectedSignals << getExpectedSignal(RowsInserted, 0, 1, QStringLiteral("Col 5")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 1, 1, QStringLiteral("Col 4")); + expectedSignals << getExpectedSignal(RowsInserted, 1, 1, QStringLiteral("Col 4")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 1, 2, QStringLiteral("Col 3")); + expectedSignals << getExpectedSignal(RowsInserted, 1, 2, QStringLiteral("Col 3")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 1, QStringLiteral("Col 6")); + expectedSignals << getExpectedSignal(RowsInserted, 0, 1, QStringLiteral("Col 6")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 3, QStringLiteral("Col 7")); + expectedSignals << getExpectedSignal(RowsInserted, 0, 3, QStringLiteral("Col 7")); + expectedSignals << getExpectedSignal(DataChanged, 0, 0, QVariantList() << QStringLiteral("Col 1")); + expectedSignals << getExpectedSignal(DataChanged, 3, 3, QVariantList() << QStringLiteral("Col 3")); + expectedSignals << getExpectedSignal(DataChanged, 0, 0, QVariantList() << QStringLiteral("Col 5")); + expectedSignals << getExpectedSignal(DataChanged, 0, 0, QVariantList() << QStringLiteral("Col 4")); + expectedSignals << getExpectedSignal(DataChanged, 2, 2, QVariantList() << QStringLiteral("Col 2")); + expectedSignals << getExpectedSignal(DataChanged, 1, 1, QVariantList() << QStringLiteral("Col 7")); + expectedSignals << getExpectedSignal(DataChanged, 0, 0, QVariantList() << QStringLiteral("Col 6")); + + m_modelSpy->setExpectedSignals(expectedSignals); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(10); + + // We get all the signals we expected. + QTRY_VERIFY(m_modelSpy->expectedSignals().isEmpty()); + + QTest::qWait(10); + // We didn't get signals we didn't expect. + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testCollectionMove_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("movedCollection"); + QTest::addColumn("targetCollection"); + + QTest::newRow("move-collection01") << serverContent1 << "Col 5" << "Col 1"; + QTest::newRow("move-collection02") << serverContent1 << "Col 5" << "Col 2"; + QTest::newRow("move-collection03") << serverContent1 << "Col 5" << "Col 3"; + QTest::newRow("move-collection04") << serverContent1 << "Col 5" << "Col 6"; + QTest::newRow("move-collection05") << serverContent1 << "Col 5" << "Col 7"; + QTest::newRow("move-collection06") << serverContent1 << "Col 3" << "Col 2"; + QTest::newRow("move-collection07") << serverContent1 << "Col 3" << "Col 6"; + QTest::newRow("move-collection08") << serverContent1 << "Col 3" << "Col 7"; + QTest::newRow("move-collection09") << serverContent1 << "Col 7" << "Col 2"; + QTest::newRow("move-collection10") << serverContent1 << "Col 7" << "Col 5"; + QTest::newRow("move-collection11") << serverContent1 << "Col 7" << "Col 4"; + QTest::newRow("move-collection12") << serverContent1 << "Col 7" << "Col 3"; +} + +void EntityTreeModelTest::testCollectionMove() +{ + QFETCH(QString, serverContent); + QFETCH(QString, movedCollection); + QFETCH(QString, targetCollection); + + QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::EntityTreeModel *model = testDrivers.second; + + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, movedCollection, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + QModelIndex movedIndex = list.first(); + QString sourceCollection = movedIndex.parent().data().toString(); + int sourceRow = movedIndex.row(); + + FakeCollectionMovedCommand *moveCommand = new FakeCollectionMovedCommand(movedCollection, sourceCollection, targetCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << moveCommand); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(RowsAboutToBeMoved, sourceRow, sourceRow, sourceCollection, 0, targetCollection, QVariantList() << movedCollection); + expectedSignals << getExpectedSignal(RowsMoved, sourceRow, sourceRow, sourceCollection, 0, targetCollection , QVariantList() << movedCollection); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testCollectionAdded_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("addedCollection"); + QTest::addColumn("parentCollection"); + + QTest::newRow("add-collection01") << serverContent1 << "new Collection" << "Col 1"; + QTest::newRow("add-collection02") << serverContent1 << "new Collection" << "Col 2"; + QTest::newRow("add-collection03") << serverContent1 << "new Collection" << "Col 3"; + QTest::newRow("add-collection04") << serverContent1 << "new Collection" << "Col 4"; + QTest::newRow("add-collection05") << serverContent1 << "new Collection" << "Col 5"; + QTest::newRow("add-collection06") << serverContent1 << "new Collection" << "Col 6"; + QTest::newRow("add-collection07") << serverContent1 << "new Collection" << "Col 7"; +} + +void EntityTreeModelTest::testCollectionAdded() +{ + QFETCH(QString, serverContent); + QFETCH(QString, addedCollection); + QFETCH(QString, parentCollection); + + QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + + FakeCollectionAddedCommand *addCommand = new FakeCollectionAddedCommand(addedCollection, parentCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << addCommand); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 0, parentCollection, QVariantList() << addedCollection); + expectedSignals << getExpectedSignal(RowsInserted, 0, 0, parentCollection, QVariantList() << addedCollection); + //The data changed signal comes from the item fetch job that is triggered because we have ImmediatePopulation enabled + expectedSignals << getExpectedSignal(DataChanged, 0, 0, parentCollection, QVariantList() << addedCollection); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testCollectionRemoved_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("removedCollection"); + + // The test suite doesn't handle this case yet. +// QTest::newRow("remove-collection01") << serverContent1 << "Col 1"; + QTest::newRow("remove-collection02") << serverContent1 << "Col 2"; + QTest::newRow("remove-collection03") << serverContent1 << "Col 3"; + QTest::newRow("remove-collection04") << serverContent1 << "Col 4"; + QTest::newRow("remove-collection05") << serverContent1 << "Col 5"; + QTest::newRow("remove-collection06") << serverContent1 << "Col 6"; + QTest::newRow("remove-collection07") << serverContent1 << "Col 7"; +} + +void EntityTreeModelTest::testCollectionRemoved() +{ + QFETCH(QString, serverContent); + QFETCH(QString, removedCollection); + + QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::EntityTreeModel *model = testDrivers.second; + + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, removedCollection, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + QModelIndex removedIndex = list.first(); + QString parentCollection = removedIndex.parent().data().toString(); + int sourceRow = removedIndex.row(); + + FakeCollectionRemovedCommand *removeCommand = new FakeCollectionRemovedCommand(removedCollection, parentCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << removeCommand); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(RowsAboutToBeRemoved, sourceRow, sourceRow, parentCollection, QVariantList() << removedCollection); + expectedSignals << getExpectedSignal(RowsRemoved, sourceRow, sourceRow, parentCollection , QVariantList() << removedCollection); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testCollectionChanged_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("collectionName"); + QTest::addColumn("monitoredMimeType"); + + QTest::newRow("change-collection01") << serverContent1 << "Col 1" << QString(); + QTest::newRow("change-collection02") << serverContent1 << "Col 2" << QString(); + QTest::newRow("change-collection03") << serverContent1 << "Col 3" << QString(); + QTest::newRow("change-collection04") << serverContent1 << "Col 4" << QString(); + QTest::newRow("change-collection05") << serverContent1 << "Col 5" << QString(); + QTest::newRow("change-collection06") << serverContent1 << "Col 6" << QString(); + QTest::newRow("change-collection07") << serverContent1 << "Col 7" << QString(); + //Don't remove the parent due to a missing mimetype + QTest::newRow("change-collection08") << serverContent1 << "Col 1" << QString::fromLatin1("message/rfc822"); +} + +void EntityTreeModelTest::testCollectionChanged() +{ + QFETCH(QString, serverContent); + QFETCH(QString, collectionName); + QFETCH(QString, monitoredMimeType); + + QPair testDrivers = populateModel(serverContent, monitoredMimeType); + FakeServerData *serverData = testDrivers.first; + Akonadi::EntityTreeModel *model = testDrivers.second; + + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, collectionName, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + QModelIndex changedIndex = list.first(); + QString parentCollection = changedIndex.parent().data().toString(); + qDebug() << parentCollection; + int changedRow = changedIndex.row(); + + FakeCollectionChangedCommand *changeCommand = new FakeCollectionChangedCommand(collectionName, parentCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << changeCommand); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(DataChanged, changedRow, changedRow, parentCollection, QVariantList() << collectionName); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testItemMove_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("movedItem"); + QTest::addColumn("targetCollection"); + + QTest::newRow("move-item01") << serverContent1 << "Item 1" << "Col 7"; + QTest::newRow("move-item02") << serverContent1 << "Item 5" << "Col 4"; // Move item to grandparent. + QTest::newRow("move-item03") << serverContent1 << "Item 7" << "Col 5"; // Move item to sibling. + QTest::newRow("move-item04") << serverContent1 << "Item 8" << "Col 5"; // Move item to nephew + QTest::newRow("move-item05") << serverContent1 << "Item 8" << "Col 6"; // Move item to uncle + QTest::newRow("move-item02") << serverContent1 << "Item 5" << "Col 3"; // Move item to great-grandparent. +} + +void EntityTreeModelTest::testItemMove() +{ + QFETCH(QString, serverContent); + QFETCH(QString, movedItem); + QFETCH(QString, targetCollection); + + QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::EntityTreeModel *model = testDrivers.second; + + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, movedItem, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + QModelIndex movedIndex = list.first(); + QString sourceCollection = movedIndex.parent().data().toString(); + int sourceRow = movedIndex.row(); + + QModelIndexList targetList = model->match(model->index(0, 0), Qt::DisplayRole, targetCollection, 1, Qt::MatchRecursive); + Q_ASSERT(!targetList.isEmpty()); + QModelIndex targetIndex = targetList.first(); + int targetRow = model->rowCount(targetIndex); + + FakeItemMovedCommand *moveCommand = new FakeItemMovedCommand(movedItem, sourceCollection, targetCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << moveCommand); + + QList expectedSignals; + + //Currently moves are implemented as remove + insert in the ETM. + expectedSignals << getExpectedSignal(RowsAboutToBeRemoved, sourceRow, sourceRow, sourceCollection, QVariantList() << movedItem); + expectedSignals << getExpectedSignal(RowsRemoved, sourceRow, sourceRow, sourceCollection, QVariantList() << movedItem); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, targetRow, targetRow, targetCollection, QVariantList() << movedItem); + expectedSignals << getExpectedSignal(RowsInserted, targetRow, targetRow, targetCollection, QVariantList() << movedItem); +// expectedSignals << getExpectedSignal( RowsAboutToBeMoved, sourceRow, sourceRow, sourceCollection, targetRow, targetCollection, QVariantList() << movedItem ); +// expectedSignals << getExpectedSignal( RowsMoved, sourceRow, sourceRow, sourceCollection, targetRow, targetCollection, QVariantList() << movedItem ); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testItemAdded_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("addedItem"); + QTest::addColumn("parentCollection"); + + QTest::newRow("add-item01") << serverContent1 << "new Item" << "Col 1"; + QTest::newRow("add-item02") << serverContent1 << "new Item" << "Col 2"; + QTest::newRow("add-item03") << serverContent1 << "new Item" << "Col 3"; + QTest::newRow("add-item04") << serverContent1 << "new Item" << "Col 4"; + QTest::newRow("add-item05") << serverContent1 << "new Item" << "Col 5"; + QTest::newRow("add-item06") << serverContent1 << "new Item" << "Col 6"; + QTest::newRow("add-item07") << serverContent1 << "new Item" << "Col 7"; +} + +void EntityTreeModelTest::testItemAdded() +{ + QFETCH(QString, serverContent); + QFETCH(QString, addedItem); + QFETCH(QString, parentCollection); + + QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::EntityTreeModel *model = testDrivers.second; + + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, parentCollection, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + QModelIndex parentIndex = list.first(); + int targetRow = model->rowCount(parentIndex); + + FakeItemAddedCommand *addedCommand = new FakeItemAddedCommand(addedItem, parentCollection, serverData); + + m_modelSpy->startSpying(); + + serverData->setCommands(QList() << addedCommand); + QList expectedSignals; + + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, targetRow, targetRow, parentCollection, QVariantList() << addedItem); + expectedSignals << getExpectedSignal(RowsInserted, targetRow, targetRow, parentCollection, QVariantList() << addedItem); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testItemRemoved_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("removedItem"); + + QTest::newRow("remove-item01") << serverContent1 << "Item 1"; + QTest::newRow("remove-item02") << serverContent1 << "Item 2"; + QTest::newRow("remove-item03") << serverContent1 << "Item 3"; + QTest::newRow("remove-item04") << serverContent1 << "Item 4"; + QTest::newRow("remove-item05") << serverContent1 << "Item 5"; + QTest::newRow("remove-item06") << serverContent1 << "Item 6"; + QTest::newRow("remove-item07") << serverContent1 << "Item 7"; + QTest::newRow("remove-item08") << serverContent1 << "Item 8"; + QTest::newRow("remove-item09") << serverContent1 << "Item 9"; + QTest::newRow("remove-item10") << serverContent1 << "Item 10"; + QTest::newRow("remove-item11") << serverContent1 << "Item 11"; + QTest::newRow("remove-item12") << serverContent1 << "Item 12"; + QTest::newRow("remove-item13") << serverContent1 << "Item 13"; + QTest::newRow("remove-item14") << serverContent1 << "Item 14"; + QTest::newRow("remove-item15") << serverContent1 << "Item 15"; +} + +void EntityTreeModelTest::testItemRemoved() +{ + QFETCH(QString, serverContent); + QFETCH(QString, removedItem); + + QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::EntityTreeModel *model = testDrivers.second; + + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, removedItem, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + QModelIndex removedIndex = list.first(); + QString sourceCollection = removedIndex.parent().data().toString(); + int sourceRow = removedIndex.row(); + + FakeItemRemovedCommand *removeCommand = new FakeItemRemovedCommand(removedItem, sourceCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << removeCommand); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(RowsAboutToBeRemoved, sourceRow, sourceRow, sourceCollection, QVariantList() << removedItem); + expectedSignals << getExpectedSignal(RowsRemoved, sourceRow, sourceRow, sourceCollection, QVariantList() << removedItem); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testItemChanged_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("changedItem"); + + QTest::newRow("change-item01") << serverContent1 << "Item 1"; + QTest::newRow("change-item02") << serverContent1 << "Item 2"; + QTest::newRow("change-item03") << serverContent1 << "Item 3"; + QTest::newRow("change-item04") << serverContent1 << "Item 4"; + QTest::newRow("change-item05") << serverContent1 << "Item 5"; + QTest::newRow("change-item06") << serverContent1 << "Item 6"; + QTest::newRow("change-item07") << serverContent1 << "Item 7"; + QTest::newRow("change-item08") << serverContent1 << "Item 8"; + QTest::newRow("change-item09") << serverContent1 << "Item 9"; + QTest::newRow("change-item10") << serverContent1 << "Item 10"; + QTest::newRow("change-item11") << serverContent1 << "Item 11"; + QTest::newRow("change-item12") << serverContent1 << "Item 12"; + QTest::newRow("change-item13") << serverContent1 << "Item 13"; + QTest::newRow("change-item14") << serverContent1 << "Item 14"; + QTest::newRow("change-item15") << serverContent1 << "Item 15"; +} + +void EntityTreeModelTest::testItemChanged() +{ + QFETCH(QString, serverContent); + QFETCH(QString, changedItem); + + QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::EntityTreeModel *model = testDrivers.second; + + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, changedItem, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + QModelIndex changedIndex = list.first(); + QString parentCollection = changedIndex.parent().data().toString(); + int sourceRow = changedIndex.row(); + + FakeItemChangedCommand *changeCommand = new FakeItemChangedCommand(changedItem, parentCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << changeCommand); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(DataChanged, sourceRow, sourceRow, QVariantList() << changedItem); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testRemoveCollectionOnChanged() +{ + const QString serverContent = QStringLiteral( + "- C (inode/directory, text/directory) 'Col 1' 2" + "- - C (text/directory) 'Col 2' 1" + "- - - I text/directory 'Item 1'"); + const QString collectionName = QStringLiteral("Col 2"); + const QString monitoredMimeType = QString::fromLatin1("text/directory"); + + QPair testDrivers = populateModel(serverContent, monitoredMimeType); + FakeServerData *serverData = testDrivers.first; + Akonadi::EntityTreeModel *model = testDrivers.second; + + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, collectionName, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + QModelIndex changedIndex = list.first(); + Akonadi::Collection changedCol = changedIndex.data(Akonadi::EntityTreeModel::CollectionRole).value(); + changedCol.setContentMimeTypes(QStringList() << QStringLiteral("foobar")); + QString parentCollection = changedIndex.parent().data().toString(); + + FakeCollectionChangedCommand *changeCommand = new FakeCollectionChangedCommand(changedCol, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << changeCommand); + + QList expectedSignals; + expectedSignals << getExpectedSignal(RowsAboutToBeRemoved, changedIndex.row(), changedIndex.row(), parentCollection, QVariantList() << collectionName); + expectedSignals << getExpectedSignal(RowsRemoved, changedIndex.row(), changedIndex.row(), parentCollection, QVariantList() << collectionName); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +#include "entitytreemodeltest.moc" + +QTEST_MAIN(EntityTreeModelTest) + diff --git a/autotests/libs/etmpopulationtest.cpp b/autotests/libs/etmpopulationtest.cpp new file mode 100644 index 0000000..9642014 --- /dev/null +++ b/autotests/libs/etmpopulationtest.cpp @@ -0,0 +1,418 @@ +/* + Copyright (c) 2013 Christian Mollekopf + + 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 "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +class ModelSignalSpy : public QObject +{ + Q_OBJECT +public: + explicit ModelSignalSpy(QAbstractItemModel &model) + { + connect(&model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(onRowsInserted(QModelIndex,int,int))); + connect(&model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(onRowsRemoved(QModelIndex,int,int))); + connect(&model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), this, SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int))); + connect(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(onDataChanged(QModelIndex,QModelIndex))); + connect(&model, SIGNAL(layoutChanged()), this, SLOT(onLayoutChanged())); + connect(&model, SIGNAL(modelReset()), this, SLOT(onModelReset())); + } + + QStringList mSignals; + QModelIndex parent; + int start; + int end; + +public Q_SLOTS: + void onRowsInserted(QModelIndex p, int s, int e) + { + qDebug() << "rowsInserted( parent =" << p << ", start = " << s << ", end = " << e << ", data = " << p.data().toString() << ")"; + mSignals << QStringLiteral("rowsInserted"); + parent = p; + start = s; + end = e; + } + void onRowsRemoved(QModelIndex p, int s, int e) + { + qDebug() << "rowsRemoved( parent = " << p << ", start = " << s << ", end = " << e << ")"; + mSignals << QStringLiteral("rowsRemoved"); + parent = p; + start = s; + end = e; + } + void onRowsMoved(QModelIndex, int, int, QModelIndex, int) + { + mSignals << QStringLiteral("rowsMoved"); + } + void onDataChanged(QModelIndex tl, QModelIndex br) + { + qDebug() << "dataChanged( topLeft =" << tl << "(" << tl.data().toString() << "), bottomRight =" << br << "(" << br.data().toString() << ") )"; + mSignals << QStringLiteral("dataChanged"); + } + void onLayoutChanged() + { + mSignals << QStringLiteral("layoutChanged"); + } + void onModelReset() + { + mSignals << QStringLiteral("modelReset"); + } +}; + +class InspectableETM: public EntityTreeModel +{ +public: + explicit InspectableETM(ChangeRecorder *monitor, QObject *parent = 0) + : EntityTreeModel(monitor, parent) {} + EntityTreeModelPrivate *etmPrivate() + { + return d_ptr; + } +}; + +QModelIndex getIndex(const QString &string, EntityTreeModel *model) +{ + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, string, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return QModelIndex(); + } + return list.first(); +} + +Akonadi::Collection createCollection(const QString &name, const Akonadi::Collection &parent, bool enabled = true, const QStringList &mimeTypes = QStringList()) +{ + Akonadi::Collection col; + col.setParentCollection(parent); + col.setName(name); + col.setEnabled(enabled); + col.setContentMimeTypes(mimeTypes); + + CollectionCreateJob *create = new CollectionCreateJob(col); + create->exec(); + if (create->error()) { + qWarning() << create->errorString(); + } + return create->collection(); +} + +/** + * This is a test for the initial population of the ETM. + */ +class EtmPopulationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testMonitoringCollectionsPreset(); + void testMonitoringCollections(); + void testFullPopulation(); + void testAddMonitoringCollections(); + void testRemoveMonitoringCollections(); + void testDisplayFilter(); + void testReferenceCollection(); + void testLoadingOfHiddenCollection(); + void testSwitchFromReferenceToEnabled(); + +private: + Collection res; + QString mainCollectionName; + Collection monitorCol; + Collection col1; + Collection col2; + Collection col3; + Collection col4; +}; + +void EtmPopulationTest::initTestCase() +{ + qRegisterMetaType("Akonadi::Collection::Id"); + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + + res = Collection(collectionIdFromPath(QStringLiteral("res3"))); + + mainCollectionName = QStringLiteral("main"); + monitorCol = createCollection(mainCollectionName, res); + QVERIFY(monitorCol.isValid()); + col1 = createCollection(QStringLiteral("col1"), monitorCol); + QVERIFY(col1.isValid()); + col2 = createCollection(QStringLiteral("col2"), monitorCol); + QVERIFY(col2.isValid()); + col3 = createCollection(QStringLiteral("col3"), monitorCol); + QVERIFY(col3.isValid()); + col4 = createCollection(QStringLiteral("col4"), col2); + QVERIFY(col4.isValid()); +} + +void EtmPopulationTest::testMonitoringCollectionsPreset() +{ + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(col1, true); + changeRecorder->setCollectionMonitored(col2, true); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QTRY_VERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testMonitoringCollections() +{ + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + Akonadi::Collection::List monitored; + monitored << col1 << col2; + model->setCollectionsMonitored(monitored); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QTRY_VERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testFullPopulation() +{ + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + // changeRecorder->setCollectionMonitored(Akonadi::Collection::root()); + changeRecorder->setAllMonitored(true); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QVERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testAddMonitoringCollections() +{ + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(col1, true); + changeRecorder->setCollectionMonitored(col2, true); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + //The main collection may be loaded a little later since it is in the fetchAncestors path + QTRY_VERIFY(getIndex(mainCollectionName, model).isValid()); + + model->setCollectionMonitored(col3, true); + + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QTRY_VERIFY(getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + QVERIFY(getIndex(mainCollectionName, model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col3"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testRemoveMonitoringCollections() +{ + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(col1, true); + changeRecorder->setCollectionMonitored(col2, true); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + //The main collection may be loaded a little later since it is in the fetchAncestors path + QTRY_VERIFY(getIndex(mainCollectionName, model).isValid()); + + model->setCollectionMonitored(col2, false); + + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col2"), model).isValid()); + QVERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col4"), model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testDisplayFilter() +{ + Collection col5 = createCollection(QStringLiteral("col5"), monitorCol, false); + QVERIFY(col5.isValid()); + + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + model->setListFilter(Akonadi::CollectionFetchScope::Display); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QVERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col5"), model).isValid()); + + Akonadi::CollectionDeleteJob *deleteJob = new Akonadi::CollectionDeleteJob(col5); + AKVERIFYEXEC(deleteJob); +} + +void EtmPopulationTest::testReferenceCollection() +{ + Collection col5 = createCollection(QStringLiteral("col5"), monitorCol, false); + QVERIFY(col5.isValid()); + + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + model->setListFilter(Akonadi::CollectionFetchScope::Display); + + QTRY_VERIFY(model->isFullyPopulated()); + QVERIFY(!getIndex(QStringLiteral("col5"), model).isValid()); + //Check that this random other collection is actually available + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + + ModelSignalSpy spy(*model); + + //Reference the collection and it should appear in the model + model->setCollectionReferenced(col5, true); + + QTRY_VERIFY(getIndex(QStringLiteral("col5"), model).isValid()); + QTRY_VERIFY(getIndex(QStringLiteral("col5"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + //Check that this random other collection is still available + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + //Verify the etms collection has been updated accordingly + QTRY_VERIFY(getIndex(QStringLiteral("col5"), model).data(Akonadi::EntityTreeModel::CollectionRole).value().referenced()); + + //Ensure all signals have been delivered to the spy + QTest::qWait(0); + QCOMPARE(spy.mSignals.count(QStringLiteral("rowsInserted")), 1); + //Signals for data-changed signal from the referencing + QCOMPARE(spy.mSignals.count(QStringLiteral("dataChanged")), 2); + + //Dereference the collection and it should dissapear again + model->setCollectionReferenced(col5, false); + QTRY_VERIFY(!getIndex(QStringLiteral("col5"), model).isValid()); + //Check that this random other collection is still available + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + + Akonadi::CollectionDeleteJob *deleteJob = new Akonadi::CollectionDeleteJob(col5); + AKVERIFYEXEC(deleteJob); +} + +/* + * Col5 and it's ancestors should be included although the ancestors don't match the mimetype filter. + */ +void EtmPopulationTest::testLoadingOfHiddenCollection() +{ + Collection col5 = createCollection(QStringLiteral("col5"), monitorCol, false, QStringList() << QStringLiteral("application/test")); + QVERIFY(col5.isValid()); + + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + changeRecorder->setMimeTypeMonitored(QStringLiteral("application/test"), true); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QVERIFY(getIndex(QStringLiteral("col5"), model).isValid()); + + Akonadi::CollectionDeleteJob *deleteJob = new Akonadi::CollectionDeleteJob(col5); + AKVERIFYEXEC(deleteJob); +} + +void EtmPopulationTest::testSwitchFromReferenceToEnabled() +{ + Collection col5 = createCollection(QStringLiteral("col5"), monitorCol, false, QStringList() << QStringLiteral("application/test") << Collection::mimeType()); + QVERIFY(col5.isValid()); + Collection col6 = createCollection(QStringLiteral("col6"), col5, true, QStringList() << QStringLiteral("application/test")); + QVERIFY(col6.isValid()); + + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + model->setListFilter(Akonadi::CollectionFetchScope::Display); + QTRY_VERIFY(model->isFullyPopulated()); + model->setCollectionReferenced(col5, true); + QTRY_VERIFY(getIndex(QStringLiteral("col5"), model).data(Akonadi::EntityTreeModel::CollectionRole).value().referenced()); + + //Dereference and enable the collection + col5.setEnabled(true); + model->setCollectionReferenced(col5, false); + + //Index and child should stay in model since both are enabled + QVERIFY(getIndex(QStringLiteral("col5"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col6"), model).isValid()); + + Akonadi::CollectionDeleteJob *deleteJob = new Akonadi::CollectionDeleteJob(col5); + AKVERIFYEXEC(deleteJob); +} + +#include "etmpopulationtest.moc" + +QTEST_AKONADIMAIN(EtmPopulationTest) + diff --git a/autotests/libs/fakeakonadiservercommand.cpp b/autotests/libs/fakeakonadiservercommand.cpp new file mode 100644 index 0000000..9955e8e --- /dev/null +++ b/autotests/libs/fakeakonadiservercommand.cpp @@ -0,0 +1,505 @@ +/* + Copyright (C) 2009 Stephen Kelly + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "fakeakonadiservercommand.h" + +#include +#include + +#include "fakeserverdata.h" +#include "entitydisplayattribute.h" +#include "tagattribute.h" +#include "vectorhelper.h" + +using namespace Akonadi; + +FakeAkonadiServerCommand::FakeAkonadiServerCommand(FakeAkonadiServerCommand::Type type, FakeServerData *serverData) + : m_type(type) + , m_serverData(serverData) + , m_model(serverData->model()) +{ + connectForwardingSignals(); +} + +bool FakeAkonadiServerCommand::isTagSignal(const QByteArray &signal) const +{ + return signal.startsWith("emit_tag") + || signal.startsWith("emit_monitoredTag"); +} + +bool FakeAkonadiServerCommand::isItemSignal(const QByteArray &signal) const +{ + return signal.startsWith("emit_item") + || signal.startsWith("emit_monitoredItem"); +} + +bool FakeAkonadiServerCommand::isCollectionSignal(const QByteArray &signal) const +{ + return signal.startsWith("emit_collection") + || signal.startsWith("emit_monitoredCollection"); +} + +void FakeAkonadiServerCommand::connectForwardingSignals() +{ + for (int methodIndex = 0; methodIndex < metaObject()->methodCount(); ++methodIndex) { + const QMetaMethod mm = metaObject()->method(methodIndex); + QByteArray signature = mm.methodSignature(); + if (mm.methodType() == QMetaMethod::Signal) { + if ((qobject_cast(m_model) && isTagSignal(signature)) || + (qobject_cast(m_model) && (isCollectionSignal(signature) || isItemSignal(signature)))) + { + const int modelSlotIndex = m_model->metaObject()->indexOfSlot(signature.remove(0, 5).constData()); + Q_ASSERT(modelSlotIndex >= 0); + metaObject()->connect(this, methodIndex, m_model, modelSlotIndex); + } + } + } +} + +Collection FakeAkonadiServerCommand::getCollectionByDisplayName(const QString &displayName) const +{ + Q_ASSERT(qobject_cast(m_model)); + QModelIndexList list = m_model->match(m_model->index(0, 0), Qt::DisplayRole, displayName, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return Collection(); + } + return list.first().data(EntityTreeModel::CollectionRole).value(); +} + +Item FakeAkonadiServerCommand::getItemByDisplayName(const QString &displayName) const +{ + Q_ASSERT(qobject_cast(m_model)); + QModelIndexList list = m_model->match(m_model->index(0, 0), Qt::DisplayRole, displayName, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return Item(); + } + return list.first().data(EntityTreeModel::ItemRole).value(); +} + +Tag FakeAkonadiServerCommand::getTagByDisplayName(const QString &displayName) const +{ + Q_ASSERT(qobject_cast(m_model)); + QModelIndexList list = m_model->match(m_model->index(0, 0), Qt::DisplayRole, displayName, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return Tag(); + } + + return list.first().data(TagModel::TagRole).value(); +} + +void FakeJobResponse::doCommand() +{ + if (m_type == RespondToCollectionFetch) { + emit_collectionsFetched(Akonadi::valuesToVector(m_collections)); + } else if (m_type == RespondToItemFetch) { + setProperty("FetchCollectionId", m_parentCollection.id()); + emit_itemsFetched(Akonadi::valuesToVector(m_items)); + } else if (m_type == RespondToTagFetch) { + emit_tagsFetched(Akonadi::valuesToVector(m_tags)); + } +} + +QList FakeJobResponse::tokenize(const QString &treeString) +{ + QStringList parts = treeString.split(QLatin1Char('-')); + + QList tokens; + const QStringList::const_iterator begin = parts.constBegin(); + const QStringList::const_iterator end = parts.constEnd(); + + QStringList::const_iterator it = begin; + ++it; + for (; it != end; ++it) { + Token token; + if (it->trimmed().isEmpty()) { + token.type = Token::Branch; + } else { + token.type = Token::Leaf; + token.content = it->trimmed(); + } + tokens.append(token); + } + return tokens; +} + +QList FakeJobResponse::interpret(FakeServerData *fakeServerData, const QString &serverData) +{ + QList list; + QList response = parseTreeString(fakeServerData, serverData); + + foreach (FakeJobResponse *command, response) { + list.append(command); + } + return list; +} + +QList FakeJobResponse::parseTreeString(FakeServerData *fakeServerData, const QString &treeString) +{ + int depth = 0; + + QList collectionResponseList; + QHash itemResponseMap; + QList tagResponseList; + + Collection::List recentCollections; + Tag::List recentTags; + + recentCollections.append(Collection::root()); + recentTags.append(Tag()); + + QList tokens = tokenize(treeString); + while (!tokens.isEmpty()) { + Token token = tokens.takeFirst(); + + if (token.type == Token::Branch) { + ++depth; + continue; + } + Q_ASSERT(token.type == Token::Leaf); + parseEntityString(collectionResponseList, itemResponseMap, tagResponseList, + recentCollections, recentTags, fakeServerData, + token.content, depth); + + depth = 0; + } + return collectionResponseList + tagResponseList; +} + +void FakeJobResponse::parseEntityString(QList &collectionResponseList, + QHash &itemResponseMap, + QList &tagResponseList, + Collection::List &recentCollections, + Tag::List &recentTags, + FakeServerData *fakeServerData, + const QString &_entityString, + int depth) +{ + QString entityString = _entityString; + if (entityString.startsWith(QLatin1Char('C'))) { + Collection collection; + entityString.remove(0, 2); + Q_ASSERT(entityString.startsWith(QLatin1Char('('))); + entityString.remove(0, 1); + QStringList parts = entityString.split(QLatin1Char(')')); + + if (!parts.first().isEmpty()) { + QString typesString = parts.takeFirst(); + + QStringList types = typesString.split(QLatin1Char(',')); + types.replaceInStrings(QStringLiteral(" "), QLatin1String("")); + collection.setContentMimeTypes(types); + } else { + parts.removeFirst(); + } + + collection.setId(fakeServerData->nextCollectionId()); + collection.setName(QString::fromLatin1("Collection %1").arg(collection.id())); + collection.setRemoteId(QString::fromLatin1("remoteId %1").arg(collection.id())); + + if (depth == 0) { + collection.setParentCollection(Collection::root()); + } else { + collection.setParentCollection(recentCollections.at(depth)); + } + + if (recentCollections.size() == (depth + 1)) { + recentCollections.append(collection); + } else { + recentCollections[ depth + 1 ] = collection; + } + + int order = 0; + if (!parts.first().isEmpty()) { + QString displayName; + QString optionalSection = parts.first().trimmed(); + if (optionalSection.startsWith(QLatin1Char('\''))) { + optionalSection.remove(0, 1); + QStringList optionalParts = optionalSection.split(QLatin1Char('\'')); + displayName = optionalParts.takeFirst(); + EntityDisplayAttribute *eda = new EntityDisplayAttribute(); + eda->setDisplayName(displayName); + collection.addAttribute(eda); + optionalSection = optionalParts.first(); + } + + QString orderString = optionalSection.trimmed(); + if (!orderString.isEmpty()) { + bool ok; + order = orderString.toInt(&ok); + Q_ASSERT(ok); + } + } else { + order = 1; + } + while (collectionResponseList.size() < order) { + collectionResponseList.append(new FakeJobResponse(recentCollections[ depth ], FakeJobResponse::RespondToCollectionFetch, fakeServerData)); + } + collectionResponseList[ order - 1 ]->appendCollection(collection); + } + if (entityString.startsWith(QLatin1Char('I'))) { + Item item; + int order = 0; + entityString.remove(0, 2); + entityString = entityString.trimmed(); + QString type; + int iFirstSpace = entityString.indexOf(QLatin1Char(' ')); + type = entityString.left(iFirstSpace); + entityString = entityString.remove(0, iFirstSpace + 1).trimmed(); + if (iFirstSpace > 0 && !entityString.isEmpty()) { + QString displayName; + QString optionalSection = entityString; + if (optionalSection.startsWith(QLatin1Char('\''))) { + optionalSection.remove(0, 1); + QStringList optionalParts = optionalSection.split(QLatin1Char('\'')); + displayName = optionalParts.takeFirst(); + EntityDisplayAttribute *eda = new EntityDisplayAttribute(); + eda->setDisplayName(displayName); + item.addAttribute(eda); + optionalSection = optionalParts.first(); + } + QString orderString = optionalSection.trimmed(); + if (!orderString.isEmpty()) { + bool ok; + order = orderString.toInt(&ok); + Q_ASSERT(ok); + } + } else { + type = entityString; + } + Q_UNUSED(order); + + item.setMimeType(type); + item.setId(fakeServerData->nextItemId()); + item.setRemoteId(QString::fromLatin1("RId_%1 %2").arg(item.id()).arg(type)); + item.setParentCollection(recentCollections.at(depth)); + + Collection::Id colId = recentCollections[ depth ].id(); + if (!itemResponseMap.contains(colId)) { + FakeJobResponse *newResponse = new FakeJobResponse(recentCollections[ depth ], FakeJobResponse::RespondToItemFetch, fakeServerData); + itemResponseMap.insert(colId, newResponse); + collectionResponseList.append(newResponse); + } + itemResponseMap[ colId ]->appendItem(item); + } + if (entityString.startsWith(QLatin1Char('T'))) { + Tag tag; + int order = 0; + entityString.remove(0, 2); + entityString = entityString.trimmed(); + int iFirstSpace = entityString.indexOf(QLatin1Char(' ')); + QString type = entityString.left(iFirstSpace); + entityString = entityString.remove(0, iFirstSpace + 1).trimmed(); + tag.setType(type.toLatin1()); + + if (iFirstSpace > 0 && !entityString.isEmpty()) { + QString displayName; + QString optionalSection = entityString; + if (optionalSection.startsWith(QLatin1Char('\''))) { + optionalSection.remove(0, 1); + QStringList optionalParts = optionalSection.split(QLatin1Char('\'')); + displayName = optionalParts.takeFirst(); + TagAttribute *ta = new TagAttribute(); + ta->setDisplayName(displayName); + tag.addAttribute(ta); + optionalSection = optionalParts.first(); + } + QString orderString = optionalSection.trimmed(); + if (!orderString.isEmpty()) { + bool ok; + order = orderString.toInt(&ok); + Q_ASSERT(ok); + } + } else { + type = entityString; + } + + tag.setId(fakeServerData->nextTagId()); + tag.setRemoteId("RID_" + QByteArray::number(tag.id()) + " " + type.toLatin1()); + tag.setType(type.toLatin1()); + + if (depth == 0) { + tag.setParent(Tag()); + } else { + tag.setParent(recentTags.at(depth)); + } + + if (recentTags.size() == (depth + 1)) { + recentTags.append(tag); + } else { + recentTags[ depth + 1 ] = tag; + } + + while (tagResponseList.size() < order) { + tagResponseList.append(new FakeJobResponse(recentTags[depth], FakeJobResponse::RespondToTagFetch, fakeServerData)); + } + tagResponseList[order - 1]->appendTag(tag); + } +} + +void FakeCollectionMovedCommand::doCommand() +{ + Collection collection = getCollectionByDisplayName(m_collectionName); + Collection source = getCollectionByDisplayName(m_sourceName); + Collection target = getCollectionByDisplayName(m_targetName); + + Q_ASSERT(collection.isValid()); + Q_ASSERT(source.isValid()); + Q_ASSERT(target.isValid()); + + collection.setParentCollection(target); + + emit_monitoredCollectionMoved(collection, source, target); +} + +void FakeCollectionAddedCommand::doCommand() +{ + Collection parent = getCollectionByDisplayName(m_parentName); + + Q_ASSERT(parent.isValid()); + + Collection collection; + collection.setId(m_serverData->nextCollectionId()); + collection.setName(QString::fromLatin1("Collection %1").arg(collection.id())); + collection.setRemoteId(QString::fromLatin1("remoteId %1").arg(collection.id())); + collection.setParentCollection(parent); + + EntityDisplayAttribute *eda = new EntityDisplayAttribute(); + eda->setDisplayName(m_collectionName); + collection.addAttribute(eda); + + emit_monitoredCollectionAdded(collection, parent); +} + +void FakeCollectionRemovedCommand::doCommand() +{ + Collection collection = getCollectionByDisplayName(m_collectionName); + + Q_ASSERT(collection.isValid()); + + emit_monitoredCollectionRemoved(collection); +} + +void FakeCollectionChangedCommand::doCommand() +{ + if (m_collection.isValid()) { + emit_monitoredCollectionChanged(m_collection); + return; + } + Collection collection = getCollectionByDisplayName(m_collectionName); + Collection parent = getCollectionByDisplayName(m_parentName); + + Q_ASSERT(collection.isValid()); + + emit_monitoredCollectionChanged(collection); +} + +void FakeItemMovedCommand::doCommand() +{ + Item item = getItemByDisplayName(m_itemName); + Collection source = getCollectionByDisplayName(m_sourceName); + Collection target = getCollectionByDisplayName(m_targetName); + + Q_ASSERT(item.isValid()); + Q_ASSERT(source.isValid()); + Q_ASSERT(target.isValid()); + + item.setParentCollection(target); + + emit_monitoredItemMoved(item, source, target); +} + +void FakeItemAddedCommand::doCommand() +{ + Collection parent = getCollectionByDisplayName(m_parentName); + + Q_ASSERT(parent.isValid()); + + Item item; + item.setId(m_serverData->nextItemId()); + item.setRemoteId(QString::fromLatin1("remoteId %1").arg(item.id())); + item.setParentCollection(parent); + + EntityDisplayAttribute *eda = new EntityDisplayAttribute(); + eda->setDisplayName(m_itemName); + item.addAttribute(eda); + + emit_monitoredItemAdded(item, parent); +} + +void FakeItemRemovedCommand::doCommand() +{ + Item item = getItemByDisplayName(m_itemName); + + Q_ASSERT(item.isValid()); + + emit_monitoredItemRemoved(item); +} + +void FakeItemChangedCommand::doCommand() +{ + Item item = getItemByDisplayName(m_itemName); + Collection parent = getCollectionByDisplayName(m_parentName); + + Q_ASSERT(item.isValid()); + Q_ASSERT(parent.isValid()); + + emit_monitoredItemChanged(item, QSet()); +} + +void FakeTagAddedCommand::doCommand() +{ + const Tag parent = getTagByDisplayName(m_parentName); + + Tag tag; + tag.setId(m_serverData->nextTagId()); + tag.setName(m_tagName); + tag.setRemoteId("remoteId " + QByteArray::number(tag.id())); + tag.setParent(parent); + + emit_monitoredTagAdded(tag); +} + +void FakeTagChangedCommand::doCommand() +{ + const Tag tag = getTagByDisplayName(m_tagName); + + Q_ASSERT(tag.isValid()); + + emit_monitoredTagChanged(tag); +} + +void FakeTagMovedCommand::doCommand() +{ + Tag tag = getTagByDisplayName(m_tagName); + Tag newParent = getTagByDisplayName(m_newParent); + + Q_ASSERT(tag.isValid()); + + tag.setParent(newParent); + + emit_monitoredTagChanged(tag); +} + +void FakeTagRemovedCommand::doCommand() +{ + const Tag tag = getTagByDisplayName(m_tagName); + + Q_ASSERT(tag.isValid()); + + emit_monitoredTagRemoved(tag); +} diff --git a/autotests/libs/fakeakonadiservercommand.h b/autotests/libs/fakeakonadiservercommand.h new file mode 100644 index 0000000..320d8c1 --- /dev/null +++ b/autotests/libs/fakeakonadiservercommand.h @@ -0,0 +1,444 @@ +/* + Copyright (C) 2009 Stephen Kelly + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef FAKE_AKONADI_SERVER_COMMAND_H +#define FAKE_AKONADI_SERVER_COMMAND_H + +#include + +#include "collection.h" +#include "entitytreemodel.h" +#include "item.h" +#include "tagmodel.h" +#include "tag.h" +#include "akonaditestfake_export.h" + +class FakeServerData; + +class AKONADITESTFAKE_EXPORT FakeAkonadiServerCommand : public QObject +{ + Q_OBJECT +public: + enum Type { + Notification, + RespondToCollectionFetch, + RespondToItemFetch, + RespondToTagFetch + }; + + FakeAkonadiServerCommand(Type type, FakeServerData *serverData); + + virtual ~FakeAkonadiServerCommand() + { + } + + Type respondTo() const + { + return m_type; + } + Akonadi::Collection fetchCollection() const + { + return m_parentCollection; + } + + Type m_type; + + virtual void doCommand() = 0; + +Q_SIGNALS: + void emit_itemsFetched(const Akonadi::Item::List &list); + void emit_collectionsFetched(const Akonadi::Collection::List &list); + void emit_tagsFetched(const Akonadi::Tag::List &tags); + + void emit_monitoredCollectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &target); + void emit_monitoredCollectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + void emit_monitoredCollectionRemoved(const Akonadi::Collection &collection); + void emit_monitoredCollectionChanged(const Akonadi::Collection &collection); + + void emit_monitoredItemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &target); + void emit_monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &parent); + void emit_monitoredItemRemoved(const Akonadi::Item &item); + void emit_monitoredItemChanged(const Akonadi::Item &item, const QSet &parts); + + void emit_monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + void emit_monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + void emit_monitoredTagAdded(const Akonadi::Tag &tag); + void emit_monitoredTagChanged(const Akonadi::Tag &tag); + void emit_monitoredTagRemoved(const Akonadi::Tag &tag); +protected: + Akonadi::Collection getCollectionByDisplayName(const QString &displayName) const; + Akonadi::Item getItemByDisplayName(const QString &displayName) const; + Akonadi::Tag getTagByDisplayName(const QString &displayName) const; + + bool isItemSignal(const QByteArray &signature) const; + bool isCollectionSignal(const QByteArray &signature) const; + bool isTagSignal(const QByteArray &signature) const; + +protected: + FakeServerData *m_serverData; + QAbstractItemModel *m_model; + Akonadi::Collection m_parentCollection; + Akonadi::Tag m_parentTag; + QHash m_collections; + QHash m_items; + QHash > m_childElements; + QHash m_tags; + +private: + void connectForwardingSignals(); +}; + +class AKONADITESTFAKE_EXPORT FakeMonitorCommand : public FakeAkonadiServerCommand +{ +public: + explicit FakeMonitorCommand(FakeServerData *serverData) + : FakeAkonadiServerCommand(Notification, serverData) + { + } + virtual ~FakeMonitorCommand() + { + } +}; + +class AKONADITESTFAKE_EXPORT FakeCollectionMovedCommand : public FakeMonitorCommand +{ +public: + FakeCollectionMovedCommand(const QString &collection, const QString &source, const QString &target, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collectionName(collection) + , m_sourceName(source) + , m_targetName(target) + { + } + + virtual ~FakeCollectionMovedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_collectionName; + QString m_sourceName; + QString m_targetName; +}; + +class AKONADITESTFAKE_EXPORT FakeCollectionAddedCommand : public FakeMonitorCommand +{ +public: + FakeCollectionAddedCommand(const QString &collection, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collectionName(collection) + , m_parentName(parent) + { + } + + virtual ~FakeCollectionAddedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_collectionName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeCollectionRemovedCommand : public FakeMonitorCommand +{ +public: + FakeCollectionRemovedCommand(const QString &collection, const QString &source, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collectionName(collection) + , m_parentName(source) + { + } + + virtual ~FakeCollectionRemovedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_collectionName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeCollectionChangedCommand : public FakeMonitorCommand +{ +public: + FakeCollectionChangedCommand(const QString &collection, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collectionName(collection) + , m_parentName(parent) + { + } + + FakeCollectionChangedCommand(const Akonadi::Collection &collection, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collection(collection) + { + } + + virtual ~FakeCollectionChangedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + Akonadi::Collection m_collection; + QString m_collectionName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeItemMovedCommand : public FakeMonitorCommand +{ +public: + FakeItemMovedCommand(const QString &item, const QString &source, const QString &target, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_itemName(item) + , m_sourceName(source) + , m_targetName(target) + { + } + + virtual ~FakeItemMovedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_itemName; + QString m_sourceName; + QString m_targetName; +}; + +class AKONADITESTFAKE_EXPORT FakeItemAddedCommand : public FakeMonitorCommand +{ +public: + FakeItemAddedCommand(const QString &item, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_itemName(item) + , m_parentName(parent) + { + } + + virtual ~FakeItemAddedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_itemName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeItemRemovedCommand : public FakeMonitorCommand +{ +public: + FakeItemRemovedCommand(const QString &item, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_itemName(item) + , m_parentName(parent) + { + } + + virtual ~FakeItemRemovedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_itemName; + QString m_parentName; + FakeServerData *m_serverData; +}; + +class AKONADITESTFAKE_EXPORT FakeItemChangedCommand : public FakeMonitorCommand +{ +public: + FakeItemChangedCommand(const QString &item, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_itemName(item) + , m_parentName(parent) + { + } + + virtual ~FakeItemChangedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_itemName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeTagAddedCommand : public FakeMonitorCommand +{ +public: + FakeTagAddedCommand(const QString &tag, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_tagName(tag) + , m_parentName(parent) + { + } + + virtual ~FakeTagAddedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_tagName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeTagChangedCommand : public FakeMonitorCommand +{ +public: + FakeTagChangedCommand(const QString &tag, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_tagName(tag) + , m_parentName(parent) + { + } + + virtual ~FakeTagChangedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_tagName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeTagMovedCommand : public FakeMonitorCommand +{ +public: + FakeTagMovedCommand(const QString &tag, const QString &oldParent, const QString &newParent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_tagName(tag) + , m_oldParent(oldParent) + , m_newParent(newParent) + { + } + + virtual ~FakeTagMovedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_tagName; + QString m_oldParent; + QString m_newParent; +}; + +class AKONADITESTFAKE_EXPORT FakeTagRemovedCommand : public FakeMonitorCommand +{ +public: + FakeTagRemovedCommand(const QString &tag, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_tagName(tag) + , m_parentName(parent) + { + } + + virtual ~FakeTagRemovedCommand() + { + } + + void doCommand() Q_DECL_OVERRIDE; + +private: + QString m_tagName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeJobResponse : public FakeAkonadiServerCommand +{ + struct Token { + enum Type { + Branch, + Leaf + }; + Type type; + QString content; + }; +public: + FakeJobResponse(const Akonadi::Collection &parentCollection, Type respondTo, FakeServerData *serverData) + : FakeAkonadiServerCommand(respondTo, serverData) + { + m_parentCollection = parentCollection; + } + + FakeJobResponse(const Akonadi::Tag &parentTag, Type respondTo, FakeServerData *serverData) + : FakeAkonadiServerCommand(respondTo, serverData) + { + m_parentTag = parentTag; + } + + virtual ~FakeJobResponse() + { + } + + void appendCollection(const Akonadi::Collection &collection) + { + m_collections.insert(collection.id(), collection); + m_childElements[collection.parentCollection().id()].append(collection.id()); + } + void appendItem(const Akonadi::Item &item) + { + m_items.insert(item.id(), item); + } + + void appendTag(const Akonadi::Tag &tag) + { + m_tags.insert(tag.id(), tag); + } + + void doCommand() Q_DECL_OVERRIDE; + + static QList interpret(FakeServerData *fakeServerData, const QString &input); + +private: + static QList parseTreeString(FakeServerData *fakeServerData, const QString &treeString); + static QList tokenize(const QString &treeString); + static void parseEntityString(QList &collectionResponseList, + QHash &itemResponseMap, + QList &tagResponseList, + Akonadi::Collection::List &recentCollections, + Akonadi::Tag::List &recentTags, + FakeServerData *fakeServerData, + const QString &entityString, + int depth); +}; + +#endif diff --git a/autotests/libs/fakeentitycache.cpp b/autotests/libs/fakeentitycache.cpp new file mode 100644 index 0000000..3e0de68 --- /dev/null +++ b/autotests/libs/fakeentitycache.cpp @@ -0,0 +1,21 @@ +/* + Copyright (c) 2011 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 "fakeentitycache.h" + diff --git a/autotests/libs/fakeentitycache.h b/autotests/libs/fakeentitycache.h new file mode 100644 index 0000000..9cfc8d6 --- /dev/null +++ b/autotests/libs/fakeentitycache.h @@ -0,0 +1,184 @@ +/* + Copyright (c) 2011 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 FAKEENTITYCACHE_H +#define FAKEENTITYCACHE_H + +#include "monitor_p.h" +#include "notificationsource_p.h" +#include "notificationbus_p.h" + +#include "collectionfetchscope.h" +#include "itemfetchscope.h" +#include "akonaditestfake_export.h" +#include "private/protocol_p.h" + +template +class FakeEntityCache : public Cache +{ +public: + FakeEntityCache(Akonadi::Session *session = Q_NULLPTR, QObject *parent = Q_NULLPTR) + : Cache(0, session, parent) + { + } + + void setData(const QHash &data) + { + m_data = data; + } + + void insert(T t) + { + m_data.insert(t.id(), t); + } + + void emitDataAvailable() + { + emit Cache::dataAvailable(); + } + + T retrieve(typename T::Id id) const + { + return m_data.value(id); + } + + void request(typename T::Id id, const typename Cache::FetchScope &scope) + { + Q_UNUSED(id) + Q_UNUSED(scope) + } + + bool ensureCached(typename T::Id id, const typename Cache::FetchScope &scope) + { + Q_UNUSED(scope) + return m_data.contains(id); + } + +private: + QHash m_data; +}; +typedef FakeEntityCache FakeCollectionCache; +typedef FakeEntityCache FakeItemCache; + +class AKONADITESTFAKE_EXPORT FakeNotificationSource : public QObject +{ + Q_OBJECT +public: + FakeNotificationSource(QObject *parent = Q_NULLPTR) + : QObject(parent) + { + } + +public Q_SLOTS: + void setAllMonitored(bool allMonitored) + { + Q_UNUSED(allMonitored) + } + void setMonitoredCollection(qlonglong id, bool monitored) + { + Q_UNUSED(id) + Q_UNUSED(monitored) + } + void setMonitoredItem(qlonglong id, bool monitored) + { + Q_UNUSED(id) + Q_UNUSED(monitored) + } + void setMonitoredResource(const QByteArray &resource, bool monitored) + { + Q_UNUSED(resource) + Q_UNUSED(monitored) + } + void setMonitoredMimeType(const QString &mimeType, bool monitored) + { + Q_UNUSED(mimeType) + Q_UNUSED(monitored) + } + void setIgnoredSession(const QByteArray &session, bool ignored) + { + Q_UNUSED(session) + Q_UNUSED(ignored) + } + + void setSession(const QByteArray &session) + { + Q_UNUSED(session); + } +}; + +class AKONADITESTFAKE_EXPORT FakeNotificationBus : public QObject +{ + Q_OBJECT + +public: + explicit FakeNotificationBus(QObject *parent = Q_NULLPTR) + : QObject(parent) + {} + + virtual ~FakeNotificationBus() + {} + + void emitNotify(const Akonadi::Protocol::ChangeNotification &ntf) + { + Q_EMIT notify(ntf); + } + +Q_SIGNALS: + void notify(const Akonadi::Protocol::ChangeNotification &ntf); +}; + +class FakeMonitorDependeciesFactory : public Akonadi::ChangeNotificationDependenciesFactory +{ +public: + + FakeMonitorDependeciesFactory(FakeItemCache *itemCache_, FakeCollectionCache *collectionCache_) + : Akonadi::ChangeNotificationDependenciesFactory() + , itemCache(itemCache_) + , collectionCache(collectionCache_) + { + } + + Akonadi::NotificationSource *createNotificationSource(QObject *parent) Q_DECL_OVERRIDE { + return new Akonadi::NotificationSource(new FakeNotificationSource(parent)); + } + + QObject *createNotificationBus(QObject *parent, Akonadi::NotificationSource *source) Q_DECL_OVERRIDE { + Q_UNUSED(source); + return new FakeNotificationBus(parent); + } + + Akonadi::CollectionCache *createCollectionCache(int maxCapacity, Akonadi::Session *session) Q_DECL_OVERRIDE { + Q_UNUSED(maxCapacity) + Q_UNUSED(session) + return collectionCache; + } + + Akonadi::ItemCache *createItemCache(int maxCapacity, Akonadi::Session *session) Q_DECL_OVERRIDE { + Q_UNUSED(maxCapacity) + Q_UNUSED(session) + return itemCache; + } + +private: + FakeItemCache *itemCache; + FakeCollectionCache *collectionCache; + +}; + +#endif diff --git a/autotests/libs/fakemonitor.cpp b/autotests/libs/fakemonitor.cpp new file mode 100644 index 0000000..eecbefe --- /dev/null +++ b/autotests/libs/fakemonitor.cpp @@ -0,0 +1,49 @@ +/* + 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 "fakemonitor.h" +#include "changerecorder_p.h" + +#include "entitycache_p.h" + +#include + +using namespace Akonadi; + +class FakeMonitorPrivate : public ChangeRecorderPrivate +{ + Q_DECLARE_PUBLIC(FakeMonitor) +public: + FakeMonitorPrivate(FakeMonitor *monitor) + : ChangeRecorderPrivate(0, monitor) + { + } + + bool connectToNotificationManager() Q_DECL_OVERRIDE { + // Do nothing. This monitor should not connect to the notification manager. + return true; + } + +}; + +FakeMonitor::FakeMonitor(QObject *parent) + : ChangeRecorder(new FakeMonitorPrivate(this), parent) +{ + +} diff --git a/autotests/libs/fakemonitor.h b/autotests/libs/fakemonitor.h new file mode 100644 index 0000000..b758055 --- /dev/null +++ b/autotests/libs/fakemonitor.h @@ -0,0 +1,40 @@ +/* + 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 FAKEMONITOR_H +#define FAKEMONITOR_H + +#include "changerecorder.h" +#include "akonaditestfake_export.h" + +using namespace Akonadi; + +class FakeMonitorPrivate; + +class AKONADITESTFAKE_EXPORT FakeMonitor : public Akonadi::ChangeRecorder +{ + Q_OBJECT +public: + FakeMonitor(QObject *parent = Q_NULLPTR); + +private: + Q_DECLARE_PRIVATE(FakeMonitor) +}; + +#endif diff --git a/autotests/libs/fakeserverdata.cpp b/autotests/libs/fakeserverdata.cpp new file mode 100644 index 0000000..b6f407e --- /dev/null +++ b/autotests/libs/fakeserverdata.cpp @@ -0,0 +1,153 @@ +/* + Copyright (C) 2009 Stephen Kelly + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "fakeserverdata.h" + +#include "itemfetchjob.h" +#include "collectionfetchjob.h" + +#include +#include + +FakeServerData::FakeServerData(EntityTreeModel *model, FakeSession *session, FakeMonitor *monitor, QObject *parent) + : QObject(parent), + m_model(model), + m_session(session), + m_monitor(monitor), + m_nextCollectionId(1), + m_nextItemId(0), + m_nextTagId(1) +{ + // can't use QueuedConnection here, because the Job might self-deleted before + // the slot gets called + connect(session, &FakeSession::jobAdded, + [this](Akonadi::Job * job) { + Collection::Id fetchColId = job->property("FetchCollectionId").toULongLong(); + QTimer::singleShot(0, [this, fetchColId]() { + jobAdded(fetchColId); + }); + }); +} + +FakeServerData::FakeServerData(TagModel *model, FakeSession *session, FakeMonitor *monitor, QObject *parent) + : QObject(parent), + m_model(model), + m_session(session), + m_monitor(monitor), + m_nextCollectionId(1), + m_nextItemId(0), + m_nextTagId(1) +{ + connect(session, &FakeSession::jobAdded, + [this](Akonadi::Job *) { + QTimer::singleShot(0, [this]() { + jobAdded(); + }); + }); +} + + +void FakeServerData::setCommands(QList< FakeAkonadiServerCommand * > list) +{ + m_communicationQueue.clear(); + Q_FOREACH (FakeAkonadiServerCommand *command, list) { + m_communicationQueue << command; + } +} + +void FakeServerData::processNotifications() +{ + while (!m_communicationQueue.isEmpty()) { + FakeAkonadiServerCommand::Type respondTo = m_communicationQueue.head()->respondTo(); + if (respondTo == FakeAkonadiServerCommand::Notification) { + FakeAkonadiServerCommand *command = m_communicationQueue.dequeue(); + command->doCommand(); + } else { + return; + } + } +} + +void FakeServerData::jobAdded(qint64 fetchColId) +{ + returnEntities(fetchColId); +} + +void FakeServerData::jobAdded() +{ + while (!m_communicationQueue.isEmpty() && m_communicationQueue.head()->respondTo() == FakeAkonadiServerCommand::RespondToTagFetch) { + returnTags(); + } + + processNotifications(); +} + +void FakeServerData::returnEntities(Collection::Id fetchColId) +{ + if (!returnCollections(fetchColId)) { + while (!m_communicationQueue.isEmpty() && m_communicationQueue.head()->respondTo() == FakeAkonadiServerCommand::RespondToItemFetch) { + returnItems(fetchColId); + } + } + + processNotifications(); +} + +bool FakeServerData::returnCollections(Collection::Id fetchColId) +{ + if (m_communicationQueue.isEmpty()) { + return true; + } + FakeAkonadiServerCommand::Type commType = m_communicationQueue.head()->respondTo(); + + Collection fetchCollection = m_communicationQueue.head()->fetchCollection(); + + if (commType == FakeAkonadiServerCommand::RespondToCollectionFetch + && fetchColId == fetchCollection.id()) { + FakeAkonadiServerCommand *command = m_communicationQueue.dequeue(); + command->doCommand(); + if (!m_communicationQueue.isEmpty()) { + returnEntities(fetchColId); + } + return true; + } + return false; +} + +void FakeServerData::returnItems(Item::Id fetchColId) +{ + FakeAkonadiServerCommand::Type commType = m_communicationQueue.head()->respondTo(); + + if (commType == FakeAkonadiServerCommand::RespondToItemFetch) { + FakeAkonadiServerCommand *command = m_communicationQueue.dequeue(); + command->doCommand(); + if (!m_communicationQueue.isEmpty()) { + returnEntities(fetchColId); + } + } +} + +void FakeServerData::returnTags() +{ + FakeAkonadiServerCommand::Type commType = m_communicationQueue.head()->respondTo(); + + if (commType == FakeAkonadiServerCommand::RespondToTagFetch) { + FakeAkonadiServerCommand *command = m_communicationQueue.dequeue(); + command->doCommand(); + } +} diff --git a/autotests/libs/fakeserverdata.h b/autotests/libs/fakeserverdata.h new file mode 100644 index 0000000..103bec6 --- /dev/null +++ b/autotests/libs/fakeserverdata.h @@ -0,0 +1,87 @@ +/* + Copyright (C) 2009 Stephen Kelly + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef FAKE_SERVER_DATA_H +#define FAKE_SERVER_DATA_H + +#include +#include + +#include "job.h" +#include "entitytreemodel.h" + +#include "fakesession.h" +#include "fakemonitor.h" +#include "fakeakonadiservercommand.h" +#include "akonaditestfake_export.h" + +using namespace Akonadi; + +class AKONADITESTFAKE_EXPORT FakeServerData : public QObject +{ + Q_OBJECT +public: + FakeServerData(EntityTreeModel *model, FakeSession *session, FakeMonitor *monitor, QObject *parent = Q_NULLPTR); + FakeServerData(TagModel *model, FakeSession *session, FakeMonitor *monitor, QObject *parent = Q_NULLPTR); + + void setCommands(QList list); + + Collection::Id nextCollectionId() const + { + return m_nextCollectionId++; + } + Item::Id nextItemId() const + { + return m_nextItemId++; + } + Tag::Id nextTagId() const + { + return m_nextTagId++; + } + + QAbstractItemModel *model() const + { + return m_model; + } + + void processNotifications(); + +private Q_SLOTS: + void jobAdded(qint64 fetchCollectionId); + void jobAdded(); + +private: + bool returnCollections(Collection::Id fetchColId); + void returnItems(Item::Id fetchColId); + void returnEntities(Collection::Id fetchColId); + void returnTags(); + +private: + QAbstractItemModel *m_model; + FakeSession *m_session; + FakeMonitor *m_monitor; + + QList m_commandList; + QQueue m_communicationQueue; + + mutable Collection::Id m_nextCollectionId; + mutable Item::Id m_nextItemId; + mutable Tag::Id m_nextTagId; +}; + +#endif diff --git a/autotests/libs/fakesession.cpp b/autotests/libs/fakesession.cpp new file mode 100644 index 0000000..bac4f50 --- /dev/null +++ b/autotests/libs/fakesession.cpp @@ -0,0 +1,92 @@ +/* + 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 "fakesession.h" +#include "session_p.h" +#include "job.h" +#include "private/protocol_p.h" + +#include + +class FakeSessionPrivate : public SessionPrivate +{ +public: + FakeSessionPrivate(FakeSession *parent, FakeSession::Mode mode) + : SessionPrivate(parent), q_ptr(parent), m_mode(mode) + { + protocolVersion = Protocol::version(); + } + + /* reimp */ + void init(const QByteArray &id) Q_DECL_OVERRIDE + { + // trimmed down version of the real SessionPrivate::init(), without any server access + if (!id.isEmpty()) { + sessionId = id; + } else { + sessionId = QCoreApplication::instance()->applicationName().toUtf8() + + '-' + QByteArray::number(qrand()); + } + + connected = false; + theNextTag = 1; + jobRunning = false; + + reconnect(); + } + + /* reimp */ + void reconnect() Q_DECL_OVERRIDE + { + if (m_mode == FakeSession::EndJobsImmediately) { + return; + } + + emit q_ptr->reconnected(); + connected = true; + startNext(); + } + + /* reimp */ + void addJob(Job *job) Q_DECL_OVERRIDE + { + emit q_ptr->jobAdded(job); + // Return immediately so that no actual communication happens with the server and + // the started jobs are completed. + if (m_mode == FakeSession::EndJobsImmediately) { + endJob(job); + } else { + SessionPrivate::addJob(job); + } + } + + FakeSession *q_ptr; + FakeSession::Mode m_mode; +}; + +FakeSession::FakeSession(const QByteArray &sessionId, FakeSession::Mode mode, QObject *parent) + : Session(new FakeSessionPrivate(this, mode), sessionId, parent) +{ + +} + +void FakeSession::setAsDefaultSession() +{ + d->setDefaultSession(this); +} diff --git a/autotests/libs/fakesession.h b/autotests/libs/fakesession.h new file mode 100644 index 0000000..ccf6825 --- /dev/null +++ b/autotests/libs/fakesession.h @@ -0,0 +1,51 @@ +/* + 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 FAKESESSION_H +#define FAKESESSION_H + +#include "session.h" +#include "collection.h" +#include "akonaditestfake_export.h" + +using namespace Akonadi; + +class AKONADITESTFAKE_EXPORT FakeSession : public Session +{ + Q_OBJECT +public: + enum Mode { + EndJobsImmediately, + EndJobsManually + }; + + explicit FakeSession(const QByteArray &sessionId = QByteArray(), Mode mode = EndJobsImmediately, QObject *parent = Q_NULLPTR); + + /** Make this the default session returned by Akonadi::Session::defaultSession(). + * Note that ownership is taken over by the thread-local storage. + */ + void setAsDefaultSession(); + +Q_SIGNALS: + void jobAdded(Akonadi::Job *job); + + friend class FakeSessionPrivate; +}; + +#endif diff --git a/autotests/libs/favoriteproxytest.cpp b/autotests/libs/favoriteproxytest.cpp new file mode 100644 index 0000000..e8fed2b --- /dev/null +++ b/autotests/libs/favoriteproxytest.cpp @@ -0,0 +1,243 @@ +/* + Copyright (c) 2013 Christian Mollekopf + + 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 "test_utils.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +class InspectableETM: public EntityTreeModel +{ +public: + explicit InspectableETM(ChangeRecorder *monitor, QObject *parent = Q_NULLPTR) + : EntityTreeModel(monitor, parent) {} + EntityTreeModelPrivate *etmPrivate() + { + return d_ptr; + } + void reset() + { + beginResetModel(); + endResetModel(); + } +}; + +class FavoriteProxyTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testItemAdded(); + void testLoadConfig(); + void testInsertAfterModelCreation(); +private: + InspectableETM *createETM(); +}; + +void FavoriteProxyTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Akonadi::Control::start(); + AkonadiTest::setAllResourcesOffline(); +} + +QModelIndex getIndex(const QString &string, EntityTreeModel *model) +{ + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, string, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return QModelIndex(); + } + return list.first(); +} + +/** + * Since we have no sensible way to figure out if the model is fully populated, + * we use the brute force approach. + */ +bool waitForPopulation(const QModelIndex &idx, EntityTreeModel *model, int count) +{ + for (int i = 0; i < 500; i++) { + if (model->rowCount(idx) >= count) { + return true; + } + QTest::qWait(10); + } + return false; +} + +InspectableETM *FavoriteProxyTest::createETM() +{ + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(Collection::root()); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation); + return model; +} + +/** + * Tests that the item is being referenced when added to the favorite proxy, and dereferenced when removed. + */ +void FavoriteProxyTest::testItemAdded() +{ + Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + + InspectableETM *model = createETM(); + + KConfigGroup configGroup(KSharedConfig::openConfig(), "favoritecollectionsmodeltest"); + + FavoriteCollectionsModel *favoriteModel = new FavoriteCollectionsModel(model, configGroup, this); + + const int numberOfRootCollections = 4; + //Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(res3Index.isValid()); + + const Akonadi::Collection favoriteCollection = res3Index.data(EntityTreeModel::CollectionRole).value(); + QVERIFY(favoriteCollection.isValid()); + + QVERIFY(!model->etmPrivate()->isMonitored(favoriteCollection.id())); + + //Ensure the collection is reference counted after being added to the favorite model + { + favoriteModel->addCollection(favoriteCollection); + //the collection is in the favorites model + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 1); + QTRY_COMPARE(favoriteModel->data(favoriteModel->index(0, 0, QModelIndex()), EntityTreeModel::CollectionIdRole).value(), favoriteCollection.id()); + //the collection got referenced + QTRY_VERIFY(model->etmPrivate()->isMonitored(favoriteCollection.id())); + //the collection is not yet buffered though + QTRY_VERIFY(!model->etmPrivate()->isBuffered(favoriteCollection.id())); + } + + //Survive a reset + { + QSignalSpy resetSpy(model, SIGNAL(modelReset())); + model->reset(); + QTRY_COMPARE(resetSpy.count(), 1); + //the collection is in the favorites model + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 1); + QTRY_COMPARE(favoriteModel->data(favoriteModel->index(0, 0, QModelIndex()), EntityTreeModel::CollectionIdRole).value(), favoriteCollection.id()); + //the collection got referenced + QTRY_VERIFY(model->etmPrivate()->isMonitored(favoriteCollection.id())); + //the collection is not yet buffered though + QTRY_VERIFY(!model->etmPrivate()->isBuffered(favoriteCollection.id())); + } + + //Ensure the collection is no longer reference counted after being added to the favorite model, and moved to the buffer + { + favoriteModel->removeCollection(favoriteCollection); + //moved from being reference counted to being buffered + QTRY_VERIFY(model->etmPrivate()->isBuffered(favoriteCollection.id())); + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 0); + } +} + +void FavoriteProxyTest::testLoadConfig() +{ + InspectableETM *model = createETM(); + + const int numberOfRootCollections = 4; + //Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(res3Index.isValid()); + const Akonadi::Collection favoriteCollection = res3Index.data(EntityTreeModel::CollectionRole).value(); + QVERIFY(favoriteCollection.isValid()); + + KConfigGroup configGroup(KSharedConfig::openConfig(), "favoritecollectionsmodeltest"); + configGroup.writeEntry("FavoriteCollectionIds", QList() << favoriteCollection.id()); + configGroup.writeEntry("FavoriteCollectionLabels", QStringList() << QStringLiteral("label1")); + + FavoriteCollectionsModel *favoriteModel = new FavoriteCollectionsModel(model, configGroup, this); + + { + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 1); + QTRY_COMPARE(favoriteModel->data(favoriteModel->index(0, 0, QModelIndex()), EntityTreeModel::CollectionIdRole).value(), favoriteCollection.id()); + //the collection got referenced + QTRY_VERIFY(model->etmPrivate()->isMonitored(favoriteCollection.id())); + } +} + +class Filter: public QSortFilterProxyModel +{ +public: + bool filterAcceptsRow(int, const QModelIndex &) const Q_DECL_OVERRIDE + { + return accepts; + } + bool accepts; +}; + +void FavoriteProxyTest::testInsertAfterModelCreation() +{ + InspectableETM *model = createETM(); + Filter filter; + filter.accepts = false; + filter.setSourceModel(model); + + const int numberOfRootCollections = 4; + //Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(res3Index.isValid()); + const Akonadi::Collection favoriteCollection = res3Index.data(EntityTreeModel::CollectionRole).value(); + QVERIFY(favoriteCollection.isValid()); + + KConfigGroup configGroup(KSharedConfig::openConfig(), "favoritecollectionsmodeltest2"); + + FavoriteCollectionsModel *favoriteModel = new FavoriteCollectionsModel(&filter, configGroup, this); + + //Make sure the filter is not letting anything through + QTest::qWait(0); + QCOMPARE(filter.rowCount(QModelIndex()), 0); + + //The collection is not in the model yet + favoriteModel->addCollection(favoriteCollection); + filter.accepts = true; + filter.invalidate(); + + { + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 1); + QTRY_COMPARE(favoriteModel->data(favoriteModel->index(0, 0, QModelIndex()), EntityTreeModel::CollectionIdRole).value(), favoriteCollection.id()); + //the collection got referenced + QTRY_VERIFY(model->etmPrivate()->isMonitored(favoriteCollection.id())); + } +} + +#include "favoriteproxytest.moc" + +QTEST_AKONADIMAIN(FavoriteProxyTest) diff --git a/autotests/libs/firstrunner.cpp b/autotests/libs/firstrunner.cpp new file mode 100644 index 0000000..72a3b51 --- /dev/null +++ b/autotests/libs/firstrunner.cpp @@ -0,0 +1,45 @@ +/* + 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 "firstrun_p.h" + +#include +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + KAboutData aboutData(QStringLiteral("akonadi-firstrun"), + QStringLiteral("Test akonadi-firstrun"), + QStringLiteral("0.10")); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser parser; + parser.addVersionOption(); + parser.addHelpOption(); + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + Akonadi::Firstrun *f = new Akonadi::Firstrun(); + QObject::connect(f, &Akonadi::Firstrun::destroyed, &app, &QApplication::quit); + app.exec(); +} diff --git a/autotests/libs/gidtest.cpp b/autotests/libs/gidtest.cpp new file mode 100644 index 0000000..048fe3e --- /dev/null +++ b/autotests/libs/gidtest.cpp @@ -0,0 +1,210 @@ +/* + Copyright (c) 2013 Christian Mollekopf + + 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 "gidtest.h" + +#include "control.h" +#include "testattribute.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "test_utils.h" +#include +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(GidTest) + +bool TestSerializer::deserialize(Akonadi::Item &item, const QByteArray &label, QIODevice &data, int version) +{ + qDebug() << item.id(); + if (label != Akonadi::Item::FullPayload) { + return false; + } + Q_UNUSED(version); + + item.setPayload(data.readAll()); + return true; +} + +void TestSerializer::serialize(const Akonadi::Item &item, const QByteArray &label, QIODevice &data, int &version) +{ + qDebug(); + Q_ASSERT(label == Akonadi::Item::FullPayload); + Q_UNUSED(label); + Q_UNUSED(version); + data.write(item.payload()); +} + +QString TestSerializer::extractGid(const Akonadi::Item &item) const +{ + if (item.gid().isEmpty()) { + return item.url().url(); + } + return item.gid(); +} + +void GidTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + + AkonadiTest::setAllResourcesOffline(); + Akonadi::AgentInstance agent = Akonadi::AgentManager::self()->instance(QStringLiteral("akonadi_knut_resource_0")); + QVERIFY(agent.isValid()); + agent.setIsOnline(true); + + ItemSerializer::overridePluginLookup(new TestSerializer); +} + +void GidTest::testSetAndFetch_data() +{ + QTest::addColumn("input"); + QTest::addColumn("toFetch"); + QTest::addColumn("expected"); + + Item item1(1); + item1.setGid(QStringLiteral("gid1")); + Item item2(2); + item2.setGid(QStringLiteral("gid2")); + Item toFetch; + toFetch.setGid(QStringLiteral("gid1")); + QTest::newRow("single") << (Item::List() << item1) << toFetch << (Item::List() << item1); + QTest::newRow("multi") << (Item::List() << item1 << item2) << toFetch << (Item::List() << item1); + { + Item item3(3); + item2.setGid(QStringLiteral("gid1")); + QTest::newRow("multi") << (Item::List() << item1 << item2 << item3) << toFetch << (Item::List() << item1 << item3); + } +} + +static void fetchAndSetGid(Item item) +{ + ItemFetchJob *prefetchjob = new ItemFetchJob(item); + prefetchjob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(prefetchjob); + Item fetchedItem = prefetchjob->items()[0]; + + //Write the gid to the db + fetchedItem.setGid(item.gid()); + ItemModifyJob *store = new ItemModifyJob(fetchedItem); + store->setUpdateGid(true); + AKVERIFYEXEC(store); +} + +void GidTest::testSetAndFetch() +{ + QFETCH(Item::List, input); + QFETCH(Item, toFetch); + QFETCH(Item::List, expected); + + foreach (Item item, input) { + fetchAndSetGid(item); + } + + ItemFetchJob *fetch = new ItemFetchJob(toFetch, this); + fetch->fetchScope().setFetchGid(true); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), expected.size()); + foreach (Item item, expected) { + QVERIFY(expected.removeOne(item)); + } + QVERIFY(expected.isEmpty()); +} + +void GidTest::testCreate() +{ + CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("res1/foo/bar"), this); + AKVERIFYEXEC(resolver); + int colId = resolver->collection(); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload(QByteArray("test")); + item.setGid(QStringLiteral("createGid")); + ItemCreateJob *createJob = new ItemCreateJob(item, Collection(colId), this); + AKVERIFYEXEC(createJob); + ItemFetchJob *fetch = new ItemFetchJob(item, this); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), 1); +} + +void GidTest::testSetWithIgnorePayload() +{ + Item item(5); + ItemFetchJob *prefetchjob = new ItemFetchJob(item); + prefetchjob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(prefetchjob); + Item fetchedItem = prefetchjob->items()[0]; + QVERIFY(fetchedItem.gid().isEmpty()); + + //Write the gid to the db + fetchedItem.setGid(QStringLiteral("gid5")); + ItemModifyJob *store = new ItemModifyJob(fetchedItem); + store->setIgnorePayload(true); + store->setUpdateGid(true); + AKVERIFYEXEC(store); + Item toFetch; + toFetch.setGid(QStringLiteral("gid5")); + ItemFetchJob *fetch = new ItemFetchJob(toFetch, this); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), 1); + QCOMPARE(fetched.at(0).id(), Item::Id(5)); +} + +void GidTest::testFetchScope() +{ + + CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("res1/foo/bar"), this); + AKVERIFYEXEC(resolver); + int colId = resolver->collection(); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload(QByteArray("test")); + item.setGid(QStringLiteral("createGid2")); + ItemCreateJob *createJob = new ItemCreateJob(item, Collection(colId), this); + AKVERIFYEXEC(createJob); + { + ItemFetchJob *fetch = new ItemFetchJob(item, this); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), 1); + QVERIFY(fetched.at(0).gid().isNull()); + } + { + ItemFetchJob *fetch = new ItemFetchJob(item, this); + fetch->fetchScope().setFetchGid(true); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), 1); + QVERIFY(!fetched.at(0).gid().isNull()); + } +} + diff --git a/autotests/libs/gidtest.h b/autotests/libs/gidtest.h new file mode 100644 index 0000000..f37f1be --- /dev/null +++ b/autotests/libs/gidtest.h @@ -0,0 +1,55 @@ +/* + Copyright (c) 2013 Christian Mollekopf + + 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 GIDTEST_H +#define GIDTEST_H + +#include + +class GidTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testSetAndFetch_data(); + void testSetAndFetch(); + void testCreate(); + void testSetWithIgnorePayload(); + void testFetchScope(); +}; + +#include <../src/core/gidextractorinterface.h> +#include +#include + +class TestSerializer : public QObject, + public Akonadi::ItemSerializerPlugin, + public Akonadi::GidExtractorInterface +{ + Q_OBJECT + Q_INTERFACES(Akonadi::ItemSerializerPlugin) + Q_INTERFACES(Akonadi::GidExtractorInterface) + +public: + bool deserialize(Akonadi::Item &item, const QByteArray &label, QIODevice &data, int version) Q_DECL_OVERRIDE; + void serialize(const Akonadi::Item &item, const QByteArray &label, QIODevice &data, int &version) Q_DECL_OVERRIDE; + QString extractGid(const Akonadi::Item &item) const Q_DECL_OVERRIDE; +}; + +#endif diff --git a/autotests/libs/imapparsertest.cpp b/autotests/libs/imapparsertest.cpp new file mode 100644 index 0000000..428a2f4 --- /dev/null +++ b/autotests/libs/imapparsertest.cpp @@ -0,0 +1,584 @@ +/* + 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 "imapparsertest.h" +#include "private/imapparser_p.h" +#include + +#include +#include +#include + +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QList) + +using namespace Akonadi; + +QTEST_MAIN(ImapParserTest) + +void ImapParserTest::testStripLeadingSpaces() +{ + QByteArray input = " a "; + int result; + + // simple leading spaces at the beginning + result = ImapParser::stripLeadingSpaces(input, 0); + QCOMPARE(result, 2); + + // simple leading spaces in the middle + result = ImapParser::stripLeadingSpaces(input, 1); + QCOMPARE(result, 2); + + // no leading spaces + result = ImapParser::stripLeadingSpaces(input, 2); + QCOMPARE(result, 2); + + // trailing spaces + result = ImapParser::stripLeadingSpaces(input, 3); + QCOMPARE(result, 5); + + // out of bounds access + result = ImapParser::stripLeadingSpaces(input, input.length()); + QCOMPARE(result, input.length()); +} + +void ImapParserTest::testParseQuotedString() +{ + QByteArray input = "\"quoted \\\"NIL\\\" string inside\""; + QByteArray result; + int consumed; + + // the whole thing + consumed = ImapParser::parseQuotedString(input, result, 0); + QCOMPARE(result, QByteArray("quoted \"NIL\" string inside")); + QCOMPARE(consumed, 32); + + // unqoted string + consumed = ImapParser::parseQuotedString(input, result, 1); + QCOMPARE(result, QByteArray("quoted")); + QCOMPARE(consumed, 7); + + // whitespaces in qouted string + consumed = ImapParser::parseQuotedString(input, result, 14); + QCOMPARE(result, QByteArray(" string inside")); + QCOMPARE(consumed, 32); + + // whitespaces before unquoted string + consumed = ImapParser::parseQuotedString(input, result, 15); + QCOMPARE(result, QByteArray("string")); + QCOMPARE(consumed, 24); + + // NIL and emptyness tests + input = "NIL \"NIL\" \"\""; + + // unqoted NIL + consumed = ImapParser::parseQuotedString(input, result, 0); + QVERIFY(result.isNull()); + QCOMPARE(consumed, 3); + + // quoted NIL + consumed = ImapParser::parseQuotedString(input, result, 3); + QCOMPARE(result, QByteArray("NIL")); + QCOMPARE(consumed, 9); + + // quoted empty string + consumed = ImapParser::parseQuotedString(input, result, 9); + QCOMPARE(result, QByteArray("")); + QCOMPARE(consumed, 12); + + // unquoted string at input end + input = "some string"; + consumed = ImapParser::parseQuotedString(input, result, 4); + QCOMPARE(result, QByteArray("string")); + QCOMPARE(consumed, 11); + + // out of bounds access + consumed = ImapParser::parseQuotedString(input, result, input.length()); + QVERIFY(result.isEmpty()); + QCOMPARE(consumed, input.length()); + + // de-quoting + input = "\"\\\"some \\\\ quoted stuff\\\"\""; + consumed = ImapParser::parseQuotedString(input, result, 0); + QCOMPARE(result, QByteArray("\"some \\ quoted stuff\"")); + QCOMPARE(consumed, input.length()); + + // linebreak as separator + input = "LOGOUT\nFOO"; + consumed = ImapParser::parseQuotedString(input, result, 0); + QCOMPARE(result, QByteArray("LOGOUT")); + QCOMPARE(consumed, 6); + +} + +void ImapParserTest::testParseString() +{ + QByteArray input = "\"quoted\" unquoted {7}\nliteral {0}\n empty literal"; + QByteArray result; + int consumed; + + // quoted strings + consumed = ImapParser::parseString(input, result, 0); + QCOMPARE(result, QByteArray("quoted")); + QCOMPARE(consumed, 8); + + // unquoted string + consumed = ImapParser::parseString(input, result, 8); + QCOMPARE(result, QByteArray("unquoted")); + QCOMPARE(consumed, 17); + + // literal string + consumed = ImapParser::parseString(input, result, 17); + QCOMPARE(result, QByteArray("literal")); + QCOMPARE(consumed, 29); + + // empty literal string + consumed = ImapParser::parseString(input, result, 29); + QCOMPARE(result, QByteArray("")); + QCOMPARE(consumed, 34); + + // out of bounds access + consumed = ImapParser::parseString(input, result, input.length()); + QCOMPARE(result, QByteArray()); + QCOMPARE(consumed, input.length()); +} + +void ImapParserTest::testParseParenthesizedList_data() +{ + QTest::addColumn("input"); + QTest::addColumn >("result"); + QTest::addColumn("consumed"); + + QList reference; + + QTest::newRow("null") << QByteArray() << reference << 0; + QTest::newRow("empty") << QByteArray("()") << reference << 2; + QTest::newRow("empty with space") << QByteArray(" ( )") << reference << 4; + QTest::newRow("no list") << QByteArray("some list-less text") << reference << 0; + QTest::newRow("\n") << QByteArray() << reference << 0; + + reference << "entry1"; + reference << "entry2()"; + reference << "(sub list)"; + reference << ")))"; + reference << "entry3"; + QTest::newRow("complex") << QByteArray("(entry1 \"entry2()\" (sub list) \")))\" {6}\nentry3) end") << reference << 47; + + reference.clear(); + reference << "foo"; + reference << "\n\nbar\n"; + reference << "bla"; + QTest::newRow("newline literal") << QByteArray("(foo {6}\n\n\nbar\n bla)") << reference << 20; + + reference.clear(); + reference << "partid"; + reference << "body"; + QTest::newRow("CRLF literal separator") << QByteArray("(partid {4}\r\nbody)") << reference << 18; + + reference.clear(); + reference << "partid"; + reference << "\n\rbody\n\r"; + QTest::newRow("CRLF literal separator 2") << QByteArray("(partid {8}\r\n\n\rbody\n\r)") << reference << 22; + + reference.clear(); + reference << "NAME"; + reference << "net)"; + QTest::newRow("spurious newline") << QByteArray("(NAME \"net)\"\n)") << reference << 14; + + reference.clear(); + reference << "(42 \"net)\")"; + reference << "(0 \"\")"; + QTest::newRow("list of lists") << QByteArray("((42 \"net)\") (0 \"\"))") << reference << 20; +} + +void ImapParserTest::testParseParenthesizedList() +{ + QFETCH(QByteArray, input); + QFETCH(QList, result); + QFETCH(int, consumed); + + QList realResult; + + int reallyConsumed = ImapParser::parseParenthesizedList(input, realResult, 0); + QCOMPARE(realResult, result); + QCOMPARE(reallyConsumed, consumed); + + // briefly also test the other overload + QVarLengthArray realVLAResult; + reallyConsumed = ImapParser::parseParenthesizedList(input, realVLAResult, 0); + QCOMPARE(reallyConsumed, consumed); + + // newline literal (based on itemappendtest bug) + input = "(foo {6}\n\n\nbar\n bla)"; + consumed = ImapParser::parseParenthesizedList(input, result); +} + +void ImapParserTest::testParseNumber() +{ + QByteArray input = " 1a23.4"; + qint64 result; + int pos; + bool ok; + + // empty string + pos = ImapParser::parseNumber(QByteArray(), result, &ok); + QCOMPARE(ok, false); + QCOMPARE(pos, 0); + + // leading spaces at the beginning + pos = ImapParser::parseNumber(input, result, &ok, 0); + QCOMPARE(ok, true); + QCOMPARE(pos, 2); + QCOMPARE(result, 1ll); + + // multiple digits + pos = ImapParser::parseNumber(input, result, &ok, 3); + QCOMPARE(ok, true); + QCOMPARE(pos, 5); + QCOMPARE(result, 23ll); + + // number at input end + pos = ImapParser::parseNumber(input, result, &ok, 6); + QCOMPARE(ok, true); + QCOMPARE(pos, 7); + QCOMPARE(result, 4ll); + + // out of bounds access + pos = ImapParser::parseNumber(input, result, &ok, input.length()); + QCOMPARE(ok, false); + QCOMPARE(pos, input.length()); +} + +void ImapParserTest::testQuote_data() +{ + QTest::addColumn("unquoted"); + QTest::addColumn("quoted"); + + QTest::newRow("empty") << QByteArray("") << QByteArray("\"\""); + QTest::newRow("simple") << QByteArray("bla") << QByteArray("\"bla\""); + QTest::newRow("double-quotes") << QByteArray("\"test\"test\"") << QByteArray("\"\\\"test\\\"test\\\"\""); + QTest::newRow("backslash") << QByteArray("\\") << QByteArray("\"\\\\\""); + QByteArray binaryNonEncoded; + binaryNonEncoded += '\000'; + QByteArray binaryEncoded("\""); + binaryEncoded += '\000'; + binaryEncoded += '"'; + QTest::newRow("binary") << binaryNonEncoded << binaryEncoded; + + QTest::newRow("LF") << QByteArray("\n") << QByteArray("\"\\n\""); + QTest::newRow("CR") << QByteArray("\r") << QByteArray("\"\\r\""); + QTest::newRow("double quote") << QByteArray("\"") << QByteArray("\"\\\"\""); + QTest::newRow("mixed 1") << QByteArray("a\nb\\c") << QByteArray("\"a\\nb\\\\c\""); + QTest::newRow("mixed 2") << QByteArray("\"a\rb\"") << QByteArray("\"\\\"a\\rb\\\"\""); +} + +void ImapParserTest::testQuote() +{ + QFETCH(QByteArray, unquoted); + QFETCH(QByteArray, quoted); + QCOMPARE(ImapParser::quote(unquoted), quoted); +} + +void ImapParserTest::testMessageParser_data() +{ + QTest::addColumn >("input"); + QTest::addColumn("tag"); + QTest::addColumn("data"); + QTest::addColumn("complete"); + QTest::addColumn >("continuations"); + + QList input; + QList continuations; + QTest::newRow("empty") << input << QByteArray() << QByteArray() << false << continuations; + + input << "*"; + QTest::newRow("tag-only") << input << QByteArray("*") << QByteArray() << true << continuations; + + input.clear(); + input << "20 UID FETCH (foo)"; + QTest::newRow("simple") << input << QByteArray("20") + << QByteArray("UID FETCH (foo)") << true << continuations; + + input.clear(); + input << "1 (bla (" << ") blub)"; + QTest::newRow("parenthesis") << input << QByteArray("1") + << QByteArray("(bla () blub)") << true << continuations; + + input.clear(); + input << "1 {3}" << "bla"; + continuations << 0; + QTest::newRow("literal") << input << QByteArray("1") << QByteArray("{3}bla") + << true << continuations; + + input.clear(); + input << "1 FETCH (UID 5 DATA {3}" + << "bla" + << " RID 5)"; + QTest::newRow("parenthesisEnclosedLiteral") << input << QByteArray("1") + << QByteArray("FETCH (UID 5 DATA {3}bla RID 5)") << true << continuations; + + input.clear(); + input << "1 {3}" << "bla {4}" << "blub"; + continuations.clear(); + continuations << 0 << 1; + QTest::newRow("2literal") << input << QByteArray("1") + << QByteArray("{3}bla {4}blub") << true << continuations; + + input.clear(); + input << "1 {4}" << "A{9}"; + continuations.clear(); + continuations << 0; + QTest::newRow("literal in literal") << input << QByteArray("1") + << QByteArray("{4}A{9}") << true << continuations; + + input.clear(); + input << "* FETCH (UID 1 DATA {3}" << "bla" << " ENVELOPE {4}" << "blub" << " RID 5)"; + continuations.clear(); + continuations << 0 << 2; + QTest::newRow("enclosed2literal") << input << QByteArray("*") + << QByteArray("FETCH (UID 1 DATA {3}bla ENVELOPE {4}blub RID 5)") + << true << continuations; + + input.clear(); + input << "1 DATA {0}"; + continuations.clear(); + QTest::newRow("empty literal") << input << QByteArray("1") + << QByteArray("DATA {0}") << true << continuations; +} + +void ImapParserTest::testMessageParser() +{ + QFETCH(QList, input); + QFETCH(QByteArray, tag); + QFETCH(QByteArray, data); + QFETCH(bool, complete); + QFETCH(QList, continuations); + QList cont = continuations; + + ImapParser *parser = new ImapParser(); + QVERIFY(parser->tag().isEmpty()); + QVERIFY(parser->data().isEmpty()); + + for (int i = 0; i < input.count(); ++i) { + bool res = parser->parseNextLine(input.at(i)); + if (i != input.count() - 1) { + QVERIFY(!res); + } else { + QCOMPARE(res, complete); + } + if (parser->continuationStarted()) { + QVERIFY(cont.contains(i)); + cont.removeAll(i); + } + } + + QCOMPARE(parser->tag(), tag); + QCOMPARE(parser->data(), data); + QVERIFY(cont.isEmpty()); + + // try again, this time with a not fresh parser + parser->reset(); + QVERIFY(parser->tag().isEmpty()); + QVERIFY(parser->data().isEmpty()); + cont = continuations; + + for (int i = 0; i < input.count(); ++i) { + bool res = parser->parseNextLine(input.at(i)); + if (i != input.count() - 1) { + QVERIFY(!res); + } else { + QCOMPARE(res, complete); + } + if (parser->continuationStarted()) { + QVERIFY(cont.contains(i)); + cont.removeAll(i); + } + } + + QCOMPARE(parser->tag(), tag); + QCOMPARE(parser->data(), data); + QVERIFY(cont.isEmpty()); + + delete parser; +} + +void ImapParserTest::testParseSequenceSet_data() +{ + QTest::addColumn("data"); + QTest::addColumn("begin"); + QTest::addColumn("result"); + QTest::addColumn("end"); + + QByteArray data(" 1 0:* 3:4,8:* *:5,1"); + + QTest::newRow("empty") << QByteArray() << 0 << ImapInterval::List() << 0; + QTest::newRow("input end") << data << 20 << ImapInterval::List() << 20; + + ImapInterval::List result; + result << ImapInterval(1, 1); + QTest::newRow("single value 1") << data << 0 << result << 2; + QTest::newRow("single value 2") << data << 1 << result << 2; + QTest::newRow("single value 3") << data << 19 << result << 20; + + result.clear(); + result << ImapInterval(); + QTest::newRow("full interval") << data << 2 << result << 6; + + result.clear(); + result << ImapInterval(3, 4) << ImapInterval(8); + QTest::newRow("complex 1") << data << 7 << result << 14; + + result.clear(); + result << ImapInterval(0, 5) << ImapInterval(1, 1); + QTest::newRow("complex 2") << data << 14 << result << 20; +} + +void ImapParserTest::testParseSequenceSet() +{ + QFETCH(QByteArray, data); + QFETCH(int, begin); + QFETCH(ImapInterval::List, result); + QFETCH(int, end); + + ImapSet res; + int pos = ImapParser::parseSequenceSet(data, res, begin); + QCOMPARE(res.intervals(), result); + QCOMPARE(pos, end); +} + +void ImapParserTest::testParseDateTime_data() +{ + QTest::addColumn("data"); + QTest::addColumn("begin"); + QTest::addColumn("result"); + QTest::addColumn("end"); + + QTest::newRow("emtpy") << QByteArray() << 0 << QDateTime() << 0; + + QByteArray data(" \"28-May-2006 01:03:35 +0200\""); + QByteArray data2("22-Jul-2008 16:31:48 +0000"); + + QDateTime dt(QDate(2006, 5, 27), QTime(23, 3, 35), Qt::UTC); + QDateTime dt2(QDate(2008, 7, 22), QTime(16, 31, 48), Qt::UTC); + + QTest::newRow("quoted 1") << data << 0 << dt << 29; + QTest::newRow("quoted 2") << data << 1 << dt << 29; + QTest::newRow("unquoted") << data << 2 << dt << 28; + QTest::newRow("unquoted2") << data2 << 0 << dt2 << 26; + QTest::newRow("invalid") << data << 4 << QDateTime() << 4; +} + +void ImapParserTest::testParseDateTime() +{ + QFETCH(QByteArray, data); + QFETCH(int, begin); + QFETCH(QDateTime, result); + QFETCH(int, end); + + QDateTime actualResult; + int actualEnd = ImapParser::parseDateTime(data, actualResult, begin); + QCOMPARE(actualResult, result); + QCOMPARE(actualEnd, end); +} + +void ImapParserTest::testBulkParser_data() +{ + QTest::addColumn("input"); + QTest::addColumn("data"); + + QTest::newRow("empty") << QByteArray("* PRE {0} POST\n") << QByteArray("PRE {0} POST\n"); + QTest::newRow("small block") << QByteArray("* PRE {2}\nXX POST\n") + << QByteArray("PRE {2}\nXX POST\n"); + QTest::newRow("small block 2") << QByteArray("* (PRE {2}\nXX\n POST)\n") + << QByteArray("(PRE {2}\nXX\n POST)\n"); + QTest::newRow("large block") << QByteArray("* PRE {10}\n0123456789\n") + << QByteArray("PRE {10}\n0123456789\n"); + QTest::newRow("store failure") << QByteArray("3 UID STORE (FOO bar ENV {3}\n(a) HEAD {3}\na\n\n BODY {3}\nabc)\n") + << QByteArray("UID STORE (FOO bar ENV {3}\n(a) HEAD {3}\na\n\n BODY {3}\nabc)\n"); +} + +void ImapParserTest::testBulkParser() +{ + QFETCH(QByteArray, input); + QFETCH(QByteArray, data); + + ImapParser *parser = new ImapParser(); + QBuffer buffer; + buffer.setData(input); + QVERIFY(buffer.open(QBuffer::ReadOnly)); + + // reading continuation as a single block + forever { + if (buffer.atEnd()) + { + break; + } + if (parser->continuationSize() > 0) + { + parser->parseBlock(buffer.read(parser->continuationSize())); + } else if (buffer.canReadLine()) + { + const QByteArray line = buffer.readLine(); + bool res = parser->parseNextLine(line); + QCOMPARE(res, buffer.atEnd()); + } + } + QCOMPARE(parser->data(), data); + + // reading continuations as smaller blocks + buffer.reset(); + parser->reset(); + forever { + if (buffer.atEnd()) + { + break; + } + if (parser->continuationSize() > 4) + { + parser->parseBlock(buffer.read(4)); + } else if (parser->continuationSize() > 0) + { + parser->parseBlock(buffer.read(parser->continuationSize())); + } else if (buffer.canReadLine()) + { + bool res = parser->parseNextLine(buffer.readLine()); + QCOMPARE(res, buffer.atEnd()); + } + } + + delete parser; +} + +void ImapParserTest::testJoin_data() +{ + QTest::addColumn >("list"); + QTest::addColumn("joined"); + QTest::newRow("empty") << QList() << QByteArray(); + QTest::newRow("one") << (QList() << "abab") << QByteArray("abab"); + QTest::newRow("two") << (QList() << "abab" << "cdcd") << QByteArray("abab cdcd"); + QTest::newRow("three") << (QList() << "abab" << "cdcd" << "efef") << QByteArray("abab cdcd efef"); +} + +void ImapParserTest::testJoin() +{ + QFETCH(QList, list); + QFETCH(QByteArray, joined); + QCOMPARE(ImapParser::join(list, " "), joined); +} + diff --git a/autotests/libs/imapparsertest.h b/autotests/libs/imapparsertest.h new file mode 100644 index 0000000..fdd4b71 --- /dev/null +++ b/autotests/libs/imapparsertest.h @@ -0,0 +1,49 @@ +/* + 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. +*/ + +#ifndef IMAPPARSER_TEST_H +#define IMAPPARSER_TEST_H + +#include + +class ImapParserTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testStripLeadingSpaces(); + void testParseQuotedString(); + void testParseString(); + void testParseParenthesizedList_data(); + void testParseParenthesizedList(); + void testParseNumber(); + void testQuote_data(); + void testQuote(); + void testMessageParser_data(); + void testMessageParser(); + void testParseSequenceSet_data(); + void testParseSequenceSet(); + void testParseDateTime_data(); + void testParseDateTime(); + void testBulkParser_data(); + void testBulkParser(); + void testJoin_data(); + void testJoin(); +}; + +#endif diff --git a/autotests/libs/imapsettest.cpp b/autotests/libs/imapsettest.cpp new file mode 100644 index 0000000..85f06cd --- /dev/null +++ b/autotests/libs/imapsettest.cpp @@ -0,0 +1,76 @@ +/* + 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 "imapsettest.h" +#include "private/imapset_p.h" + +#include + +QTEST_MAIN(ImapSetTest) + +Q_DECLARE_METATYPE(QList) + +using namespace Akonadi; + +void ImapSetTest::testAddList_data() +{ + QTest::addColumn >("source"); + QTest::addColumn("intervals"); + QTest::addColumn("seqset"); + + // empty set + QList source; + ImapInterval::List intervals; + QTest::newRow("empty") << source << intervals << QByteArray(); + + // single value + source << 4; + intervals << ImapInterval(4, 4); + QTest::newRow("single value") << source << intervals << QByteArray("4"); + + // single 2-value interval + source << 5; + intervals.clear(); + intervals << ImapInterval(4, 5); + QTest::newRow("single 2 interval") << source << intervals << QByteArray("4:5"); + + // single large interval + source << 6 << 3 << 7 << 2 << 8; + intervals.clear(); + intervals << ImapInterval(2, 8); + QTest::newRow("single 7 interval") << source << intervals << QByteArray("2:8"); + + // double interval + source << 12; + intervals << ImapInterval(12, 12); + QTest::newRow("double interval") << source << intervals << QByteArray("2:8,12"); +} + +void ImapSetTest::testAddList() +{ + QFETCH(QList, source); + QFETCH(ImapInterval::List, intervals); + QFETCH(QByteArray, seqset); + + ImapSet set; + set.add(source); + ImapInterval::List result = set.intervals(); + QCOMPARE(result, intervals); + QCOMPARE(set.toImapSequenceSet(), seqset); +} diff --git a/autotests/libs/imapsettest.h b/autotests/libs/imapsettest.h new file mode 100644 index 0000000..cb06285 --- /dev/null +++ b/autotests/libs/imapsettest.h @@ -0,0 +1,33 @@ +/* + 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_IMAPSETTEST_H +#define AKONADI_IMAPSETTEST_H + +#include + +class ImapSetTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testAddList_data(); + void testAddList(); +}; + +#endif diff --git a/autotests/libs/inspectablechangerecorder.cpp b/autotests/libs/inspectablechangerecorder.cpp new file mode 100644 index 0000000..188b59a --- /dev/null +++ b/autotests/libs/inspectablechangerecorder.cpp @@ -0,0 +1,40 @@ +/* + Copyright (c) 2011 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 "inspectablechangerecorder.h" + +#include "changerecorder_p.h" + +InspectableChangeRecorderPrivate::InspectableChangeRecorderPrivate(FakeMonitorDependeciesFactory *dependenciesFactory, InspectableChangeRecorder *parent) + : ChangeRecorderPrivate(dependenciesFactory, parent) +{ + +} + +InspectableChangeRecorder::InspectableChangeRecorder(FakeMonitorDependeciesFactory *dependenciesFactory, QObject *parent) + : ChangeRecorder(new Akonadi::ChangeRecorderPrivate(dependenciesFactory, this), parent) +{ + QTimer::singleShot(0, this, SLOT(doConnectToNotificationManager())); +} + +void InspectableChangeRecorder::doConnectToNotificationManager() +{ + d_ptr->connectToNotificationManager(); +} + diff --git a/autotests/libs/inspectablechangerecorder.h b/autotests/libs/inspectablechangerecorder.h new file mode 100644 index 0000000..2f3e606 --- /dev/null +++ b/autotests/libs/inspectablechangerecorder.h @@ -0,0 +1,96 @@ +/* + Copyright (c) 2011 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 INSPECTABLECHANGERECORDER_H +#define INSPECTABLECHANGERECORDER_H + +#include "entitycache_p.h" +#include "changerecorder.h" +#include "changerecorder_p.h" + +#include "fakeakonadiservercommand.h" +#include "fakeentitycache.h" +#include "akonaditestfake_export.h" + +class InspectableChangeRecorder; + +class InspectableChangeRecorderPrivate : public Akonadi::ChangeRecorderPrivate +{ +public: + InspectableChangeRecorderPrivate(FakeMonitorDependeciesFactory *dependenciesFactory, InspectableChangeRecorder *parent); + ~InspectableChangeRecorderPrivate() + { + } + + bool emitNotification(const Akonadi::Protocol::ChangeNotification &msg) Q_DECL_OVERRIDE { + // TODO: Check/Log + return Akonadi::ChangeRecorderPrivate::emitNotification(msg); + } +}; + +class AKONADITESTFAKE_EXPORT InspectableChangeRecorder : public Akonadi::ChangeRecorder +{ + Q_OBJECT +public: + InspectableChangeRecorder(FakeMonitorDependeciesFactory *dependenciesFactory, QObject *parent = Q_NULLPTR); + + FakeNotificationSource *notifier() const + { + return qobject_cast(d_ptr->notificationSource->source()); + } + + FakeNotificationBus *notificationBus() const + { + return qobject_cast(d_ptr->notificationBus); + } + + QQueue pendingNotifications() const + { + return d_ptr->pendingNotifications; + } + QQueue pipeline() const + { + return d_ptr->pipeline; + } + +Q_SIGNALS: + void dummySignal(); + +private Q_SLOTS: + void dispatchNotifications() + { + d_ptr->dispatchNotifications(); + } + + void doConnectToNotificationManager(); + +private: + struct MessageStruct { + enum Position { + Queued, + FilterPipelined, + Pipelined, + Emitted + }; + Position position; + }; + QQueue m_messages; +}; + +#endif diff --git a/autotests/libs/inspectablemonitor.cpp b/autotests/libs/inspectablemonitor.cpp new file mode 100644 index 0000000..81053a3 --- /dev/null +++ b/autotests/libs/inspectablemonitor.cpp @@ -0,0 +1,54 @@ +/* + Copyright (c) 2011 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 "inspectablemonitor.h" + +InspectableMonitorPrivate::InspectableMonitorPrivate(FakeMonitorDependeciesFactory *dependenciesFactory, InspectableMonitor *parent) + : Akonadi::MonitorPrivate(dependenciesFactory, parent) +{ +} + +void InspectableMonitor::doConnectToNotificationManager() +{ + d_ptr->connectToNotificationManager(); +} + +InspectableMonitor::InspectableMonitor(FakeMonitorDependeciesFactory *dependenciesFactory, QObject *parent) + : Monitor(new InspectableMonitorPrivate(dependenciesFactory, this), parent) +{ + // Make sure signals don't get optimized away. + // TODO: Make this parametrizable in the test class. + connect(this, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(itemChanged(Akonadi::Item,QSet)), SIGNAL(dummySignal())); + connect(this, SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(itemRemoved(Akonadi::Item)), SIGNAL(dummySignal())); + connect(this, SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(collectionChanged(Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(collectionChanged(Akonadi::Collection,QSet)), SIGNAL(dummySignal())); + connect(this, SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(collectionRemoved(Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), SIGNAL(dummySignal())); + connect(this, SIGNAL(collectionSubscribed(Akonadi::Collection,Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(collectionUnsubscribed(Akonadi::Collection)), SIGNAL(dummySignal())); + + QTimer::singleShot(0, this, SLOT(doConnectToNotificationManager())); +} + diff --git a/autotests/libs/inspectablemonitor.h b/autotests/libs/inspectablemonitor.h new file mode 100644 index 0000000..0528cf3 --- /dev/null +++ b/autotests/libs/inspectablemonitor.h @@ -0,0 +1,96 @@ +/* + Copyright (c) 2011 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 INSPECTABLEMONITOR_H +#define INSPECTABLEMONITOR_H + +#include "entitycache_p.h" +#include "monitor.h" +#include "monitor_p.h" + +#include "fakeakonadiservercommand.h" +#include "fakeentitycache.h" +#include "akonaditestfake_export.h" + +class InspectableMonitor; + +class InspectableMonitorPrivate : public Akonadi::MonitorPrivate +{ +public: + InspectableMonitorPrivate(FakeMonitorDependeciesFactory *dependenciesFactory, InspectableMonitor *parent); + ~InspectableMonitorPrivate() + { + } + + bool emitNotification(const Akonadi::Protocol::ChangeNotification &msg) Q_DECL_OVERRIDE { + // TODO: Check/Log + return Akonadi::MonitorPrivate::emitNotification(msg); + } +}; + +class AKONADITESTFAKE_EXPORT InspectableMonitor : public Akonadi::Monitor +{ + Q_OBJECT +public: + InspectableMonitor(FakeMonitorDependeciesFactory *dependenciesFactory, QObject *parent = Q_NULLPTR); + + FakeNotificationSource *notifier() const + { + return qobject_cast(d_ptr->notificationSource->source()); + } + + FakeNotificationBus *notificationBus() const + { + return qobject_cast(d_ptr->notificationBus); + } + + QQueue pendingNotifications() const + { + return d_ptr->pendingNotifications; + } + QQueue pipeline() const + { + return d_ptr->pipeline; + } + +Q_SIGNALS: + void dummySignal(); + +private Q_SLOTS: + void dispatchNotifications() + { + d_ptr->dispatchNotifications(); + } + + void doConnectToNotificationManager(); + +private: + struct MessageStruct { + enum Position { + Queued, + FilterPipelined, + Pipelined, + Emitted + }; + Position position; + }; + QQueue m_messages; +}; + +#endif diff --git a/autotests/libs/itemappendtest.cpp b/autotests/libs/itemappendtest.cpp new file mode 100644 index 0000000..e1626fe --- /dev/null +++ b/autotests/libs/itemappendtest.cpp @@ -0,0 +1,362 @@ +/* + 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 "itemappendtest.h" + +#include "control.h" +#include "testattribute.h" +#include "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(ItemAppendTest) + +void ItemAppendTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + AttributeFactory::registerAttribute(); +} + +void ItemAppendTest::testItemAppend_data() +{ + QTest::addColumn("remoteId"); + + QTest::newRow("empty") << QString(); + QTest::newRow("non empty") << QStringLiteral("remote-id"); + QTest::newRow("whitespace") << QStringLiteral("remote id"); + QTest::newRow("quotes") << QStringLiteral("\"remote\" id"); + QTest::newRow("brackets") << QStringLiteral("[remote id]"); +} + +void ItemAppendTest::testItemAppend() +{ + const Collection testFolder1(collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(testFolder1.isValid()); + + QFETCH(QString, remoteId); + Item ref; // for cleanup + + Item item(-1); + item.setRemoteId(remoteId); + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setFlag("TestFlag"); + item.setSize(3456); + ItemCreateJob *job = new ItemCreateJob(item, testFolder1, this); + AKVERIFYEXEC(job); + ref = job->item(); + QCOMPARE(ref.parentCollection(), testFolder1); + + ItemFetchJob *fjob = new ItemFetchJob(testFolder1, this); + fjob->fetchScope().setAncestorRetrieval(ItemFetchScope::Parent); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + QCOMPARE(fjob->items()[0], ref); + QCOMPARE(fjob->items()[0].remoteId(), remoteId); + QVERIFY(fjob->items()[0].flags().contains("TestFlag")); + QCOMPARE(fjob->items()[0].parentCollection(), ref.parentCollection()); + + qint64 size = 3456; + QCOMPARE(fjob->items()[0].size(), size); + + ItemDeleteJob *djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); + + fjob = new ItemFetchJob(testFolder1, this); + AKVERIFYEXEC(fjob); + QVERIFY(fjob->items().isEmpty()); +} + +void ItemAppendTest::testContent_data() +{ + QTest::addColumn("data"); + + QTest::newRow("null") << QByteArray(); + QTest::newRow("empty") << QByteArray(""); + QTest::newRow("nullbyte") << QByteArray("\0", 1); + QTest::newRow("nullbyte2") << QByteArray("\0X", 2); + QString utf8string = QString::fromUtf8("äöüß@€µøđ¢©®"); + QTest::newRow("utf8") << utf8string.toUtf8(); + QTest::newRow("newlines") << QByteArray("\nsome\n\nbreaked\ncontent\n\n"); + QByteArray b; + QTest::newRow("big") << b.fill('a', 1 << 20); + QTest::newRow("bignull") << b.fill('\0', 1 << 20); + QTest::newRow("bigcr") << b.fill('\r', 1 << 20); + QTest::newRow("biglf") << b.fill('\n', 1 << 20); +} + +void ItemAppendTest::testContent() +{ + const Collection testFolder1(collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(testFolder1.isValid()); + + QFETCH(QByteArray, data); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + if (!data.isNull()) { + item.setPayload(data); + } + + ItemCreateJob *job = new ItemCreateJob(item, testFolder1, this); + AKVERIFYEXEC(job); + Item ref = job->item(); + + ItemFetchJob *fjob = new ItemFetchJob(testFolder1, this); + fjob->fetchScope().setCacheOnly(true); + fjob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + Item item2 = fjob->items().first(); + QCOMPARE(item2.hasPayload(), !data.isNull()); + if (item2.hasPayload()) { + QCOMPARE(item2.payload(), data); + } + + ItemDeleteJob *djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); +} + +void ItemAppendTest::testNewMimetype() +{ + const Collection col(collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(col.isValid()); + + Item item; + item.setMimeType(QStringLiteral("application/new-type")); + ItemCreateJob *job = new ItemCreateJob(item, col, this); + AKVERIFYEXEC(job); + + item = job->item(); + QVERIFY(item.isValid()); + + ItemFetchJob *fetch = new ItemFetchJob(item, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + QCOMPARE(fetch->items().first().mimeType(), item.mimeType()); +} + +void ItemAppendTest::testIllegalAppend() +{ + const Collection testFolder1(collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(testFolder1.isValid()); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + + // adding item to non-existing collection + ItemCreateJob *job = new ItemCreateJob(item, Collection(INT_MAX), this); + QVERIFY(!job->exec()); + + // adding item into a collection which can't handle items of this type + const Collection col(collectionIdFromPath(QStringLiteral("res1/foo/bla"))); + QVERIFY(col.isValid()); + job = new ItemCreateJob(item, col, this); + QEXPECT_FAIL("", "Test not yet implemented in the server.", Continue); + QVERIFY(!job->exec()); +} + +void ItemAppendTest::testMultipartAppend() +{ + const Collection testFolder1(collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(testFolder1.isValid()); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("body data"); + item.attribute(Item::AddIfMissing)->data = "extra data"; + item.setFlag("TestFlag"); + ItemCreateJob *job = new ItemCreateJob(item, testFolder1, this); + AKVERIFYEXEC(job); + Item ref = job->item(); + + ItemFetchJob *fjob = new ItemFetchJob(ref, this); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items().first(); + QCOMPARE(item.payload(), QByteArray("body data")); + QVERIFY(item.hasAttribute()); + QCOMPARE(item.attribute()->data, QByteArray("extra data")); + QVERIFY(item.flags().contains("TestFlag")); + + ItemDeleteJob *djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); +} + +void ItemAppendTest::testInvalidMultipartAppend() +{ + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("body data"); + item.attribute(Item::AddIfMissing)->data = "extra data"; + item.setFlag("TestFlag"); + ItemCreateJob *job = new ItemCreateJob(item, Collection(-1), this); + QVERIFY(!job->exec()); + + Item item2; + item2.setMimeType(QStringLiteral("application/octet-stream")); + item2.setPayload("more body data"); + item2.attribute(Item::AddIfMissing)->data = "even more extra data"; + item2.setFlag("TestFlag"); + ItemCreateJob *job2 = new ItemCreateJob(item2, Collection(-1), this); + QVERIFY(!job2->exec()); +} + +void ItemAppendTest::testItemSize_data() +{ + QTest::addColumn("item"); + QTest::addColumn("size"); + + Item i(QStringLiteral("application/octet-stream")); + i.setPayload(QByteArray("ABCD")); + + QTest::newRow("auto size") << i << 4ll; + i.setSize(3); + QTest::newRow("too small") << i << 4ll; + i.setSize(10); + QTest::newRow("too large") << i << 10ll; +} + +void ItemAppendTest::testItemSize() +{ + QFETCH(Akonadi::Item, item); + QFETCH(qint64, size); + + const Collection col(collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(col.isValid()); + + ItemCreateJob *create = new ItemCreateJob(item, col, this); + AKVERIFYEXEC(create); + Item newItem = create->item(); + + ItemFetchJob *fetch = new ItemFetchJob(newItem, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + + QCOMPARE(fetch->items().first().size(), size); +} + +void ItemAppendTest::testItemMerge_data() +{ + QTest::addColumn("item1"); + QTest::addColumn("item2"); + QTest::addColumn("mergedItem"); + QTest::addColumn("silent"); + + { + Item i1(QStringLiteral("application/octet-stream")); + i1.setPayload(QByteArray("ABCD")); + i1.setSize(4); + i1.setRemoteId(QStringLiteral("XYZ")); + i1.setGid(QStringLiteral("XYZ")); + i1.setFlag("TestFlag1"); + i1.setRemoteRevision(QStringLiteral("5")); + + Item i2(QStringLiteral("application/octet-stream")); + i2.setPayload(QByteArray("DEFGH")); + i2.setSize(5); + i2.setRemoteId(QStringLiteral("XYZ")); + i2.setGid(QStringLiteral("XYZ")); + i2.setFlag("TestFlag2"); + i2.setRemoteRevision(QStringLiteral("6")); + + Item mergedItem(i2); + mergedItem.setFlag("TestFlag1"); + + QTest::newRow("merge") << i1 << i2 << mergedItem << false; + QTest::newRow("merge (silent)") << i1 << i2 << mergedItem << true; + } + { + Item i1(QStringLiteral("application/octet-stream")); + i1.setPayload(QByteArray("ABCD")); + i1.setSize(4); + i1.setRemoteId(QStringLiteral("RID2")); + i1.setGid(QStringLiteral("GID2")); + i1.setFlag("TestFlag1"); + i1.setRemoteRevision(QStringLiteral("5")); + + Item i2(QStringLiteral("application/octet-stream")); + i2.setRemoteId(QStringLiteral("RID2")); + i2.setGid(QStringLiteral("GID2")); + i2.setFlags(Item::Flags() << "TestFlag2"); + i2.setRemoteRevision(QStringLiteral("6")); + + Item mergedItem(i1); + mergedItem.setFlags(i2.flags()); + mergedItem.setRemoteRevision(i2.remoteRevision()); + + QTest::newRow("overwrite flags, and don't remove existing payload") << i1 << i2 << mergedItem << false; + QTest::newRow("overwrite flags, and don't remove existing payload (silent)") << i1 << i2 << mergedItem << true; + } +} + +void ItemAppendTest::testItemMerge() +{ + QFETCH(Akonadi::Item, item1); + QFETCH(Akonadi::Item, item2); + QFETCH(Akonadi::Item, mergedItem); + QFETCH(bool, silent); + + const Collection col(collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(col.isValid()); + + ItemCreateJob *create = new ItemCreateJob(item1, col, this); + AKVERIFYEXEC(create); + const Item createdItem = create->item(); + + ItemCreateJob *merge = new ItemCreateJob(item2, col, this); + ItemCreateJob::MergeOptions options = ItemCreateJob::GID | ItemCreateJob::RID; + if (silent) { + options |= ItemCreateJob::Silent; + } + merge->setMerge(options); + AKVERIFYEXEC(merge); + + QCOMPARE(merge->item().id(), createdItem.id()); + if (!silent) { + QCOMPARE(merge->item().gid(), mergedItem.gid()); + QCOMPARE(merge->item().remoteId(), mergedItem.remoteId()); + QCOMPARE(merge->item().remoteRevision(), mergedItem.remoteRevision()); + QCOMPARE(merge->item().payloadData(), mergedItem.payloadData()); + QCOMPARE(merge->item().size(), mergedItem.size()); + QCOMPARE(merge->item().flags(), mergedItem.flags()); + } + + if (merge->item().id() != createdItem.id()) { + ItemDeleteJob *del = new ItemDeleteJob(merge->item(), this); + AKVERIFYEXEC(del); + } + ItemDeleteJob *del = new ItemDeleteJob(createdItem, this); + AKVERIFYEXEC(del); +} + diff --git a/autotests/libs/itemappendtest.h b/autotests/libs/itemappendtest.h new file mode 100644 index 0000000..77b9ff7 --- /dev/null +++ b/autotests/libs/itemappendtest.h @@ -0,0 +1,44 @@ +/* + 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. +*/ + +#ifndef ITEMAPPENDTEST_H +#define ITEMAPPENDTEST_H + +#include + +class ItemAppendTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testItemAppend_data(); + void testItemAppend(); + void testContent_data(); + void testContent(); + void testNewMimetype(); + void testIllegalAppend(); + void testMultipartAppend(); + void testInvalidMultipartAppend(); + void testItemSize_data(); + void testItemSize(); + void testItemMerge_data(); + void testItemMerge(); +}; + +#endif diff --git a/autotests/libs/itembenchmark.cpp b/autotests/libs/itembenchmark.cpp new file mode 100644 index 0000000..a18ffb0 --- /dev/null +++ b/autotests/libs/itembenchmark.cpp @@ -0,0 +1,200 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include + +#include "qtest_akonadi.h" +#include "test_utils.h" + +#include + +using namespace Akonadi; + +class ItemBenchmark : public QObject +{ + Q_OBJECT +public Q_SLOTS: + void createResult(KJob *job) + { + Q_ASSERT(job->error() == KJob::NoError); + Item createdItem = static_cast(job)->item(); + mCreatedItems[createdItem.size()].append(createdItem); + } + + void fetchResult(KJob *job) + { + Q_ASSERT(job->error() == KJob::NoError); + } + + void modifyResult(KJob *job) + { + Q_ASSERT(job->error() == KJob::NoError); + } + +private: + QMap mCreatedItems; + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + } + + void data() + { + QTest::addColumn("count"); + QTest::addColumn("size"); + + QList counts = QList() << 1 << 10 << 100 << 1000; // << 10000; + QList sizes = QList() << 0 << 256 << 1024 << 8192 << 32768 << 65536; + foreach (int count, counts) + foreach (int size, sizes) + QTest::newRow(QString::fromLatin1("%1-%2").arg(count).arg(size).toLatin1().constData()) + << count << size; + } + + void itemBenchmarkCreate_data() + { + data(); + } + void itemBenchmarkCreate() /// Tests performance of creating items in the cache + { + QFETCH(int, count); + QFETCH(int, size); + + const Collection parent(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(parent.isValid()); + + Item item(QStringLiteral("application/octet-stream")); + item.setPayload(QByteArray(size, 'X')); + item.setSize(size); + + Job *lastJob = 0; + QBENCHMARK { + for (int i = 0; i < count; ++i) + { + lastJob = new ItemCreateJob(item, parent, this); + connect(lastJob, SIGNAL(result(KJob*)), SLOT(createResult(KJob*))); + } + AkonadiTest::akWaitForSignal(lastJob, SIGNAL(result(KJob*))); + } + } + + void itemBenchmarkFetch_data() + { + data(); + } + void itemBenchmarkFetch() /// Tests performance of fetching cached items + { + QFETCH(int, count); + QFETCH(int, size); + + // With only one iteration itemBenchmarkCreate() should have created count + // items, otherwise iterations * count, however, at least count items should + // be there. + QVERIFY(mCreatedItems.value(size).count() >= count); + + QBENCHMARK { + Item::List items; + for (int i = 0; i < count; ++i) + { + items << mCreatedItems[size].at(i); + } + + ItemFetchJob *fetchJob = new ItemFetchJob(items, this); + fetchJob->fetchScope().fetchFullPayload(); + fetchJob->fetchScope().setCacheOnly(true); + connect(fetchJob, SIGNAL(result(KJob*)), SLOT(fetchResult(KJob*))); + AkonadiTest::akWaitForSignal(fetchJob, SIGNAL(result(KJob*))); + } + } + + void itemBenchmarkModifyPayload_data() + { + data(); + } + void itemBenchmarkModifyPayload() /// Tests performance of modifying payload of cached items + { + QFETCH(int, count); + QFETCH(int, size); + + // With only one iteration itemBenchmarkCreate() should have created count + // items, otherwise iterations * count, however, at least count items should + // be there. + QVERIFY(mCreatedItems.value(size).count() >= count); + + Job *lastJob = 0; + const int newSize = qMax(size, 1); + QBENCHMARK { + for (int i = 0; i < count; ++i) + { + Item item = mCreatedItems.value(size).at(i); + item.setPayload(QByteArray(newSize, 'Y')); + ItemModifyJob *job = new ItemModifyJob(item, this); + job->disableRevisionCheck(); + lastJob = job; + connect(lastJob, SIGNAL(result(KJob*)), SLOT(modifyResult(KJob*))); + } + AkonadiTest::akWaitForSignal(lastJob, SIGNAL(result(KJob*))); + } + } + + void itemBenchmarkDelete_data() + { + data(); + } + void itemBenchmarkDelete() /// Tests performance of removing items from the cache + { + QFETCH(int, count); + QFETCH(int, size); + + Job *lastJob = 0; + int emptyItemArrayIterations = 0; + QBENCHMARK { + if (mCreatedItems[size].isEmpty()) + { + ++emptyItemArrayIterations; + } + + Item::List items; + for (int i = 0; i < count && !mCreatedItems[size].isEmpty(); ++i) + { + items << mCreatedItems[size].takeFirst(); + } + lastJob = new ItemDeleteJob(items, this); + AkonadiTest::akWaitForSignal(lastJob, SIGNAL(result(KJob*))); + } + + if (emptyItemArrayIterations) { + qDebug() << "Delete Benchmark performed" << emptyItemArrayIterations << "times on an empty list."; + } + } +}; + +QTEST_AKONADIMAIN(ItemBenchmark) + +#include "itembenchmark.moc" diff --git a/autotests/libs/itemcopytest.cpp b/autotests/libs/itemcopytest.cpp new file mode 100644 index 0000000..7772674 --- /dev/null +++ b/autotests/libs/itemcopytest.cpp @@ -0,0 +1,102 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "test_utils.h" +#include + +using namespace Akonadi; + +class ItemCopyTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + // switch target resources offline to reduce interference from them + foreach (Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances()) { //krazy:exclude=foreach + if (agent.identifier() == QStringLiteral("akonadi_knut_resource_2")) { + agent.setIsOnline(false); + } + } + } + + void testCopy() + { + const Collection target(collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(target.isValid()); + + ItemCopyJob *copy = new ItemCopyJob(Item(1), target); + AKVERIFYEXEC(copy); + + Item source(1); + ItemFetchJob *sourceFetch = new ItemFetchJob(source); + AKVERIFYEXEC(sourceFetch); + source = sourceFetch->items().first(); + + ItemFetchJob *fetch = new ItemFetchJob(target); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().fetchAllAttributes(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + + Item item = fetch->items().first(); + QVERIFY(item.hasPayload()); + QVERIFY(source.size() > 0); + QVERIFY(item.size() > 0); + QCOMPARE(item.size(), source.size()); + QCOMPARE(item.attributes().count(), 1); + QVERIFY(item.remoteId().isEmpty()); + QEXPECT_FAIL("", "statistics are not properly updated after copy", Abort); + QCOMPARE(target.statistics().count(), 1ll); + } + + void testIlleagalCopy() + { + // empty item list + ItemCopyJob *copy = new ItemCopyJob(Item::List(), Collection::root()); + QVERIFY(!copy->exec()); + + // non-existing target + copy = new ItemCopyJob(Item(1), Collection(INT_MAX)); + QVERIFY(!copy->exec()); + + // non-existing source + copy = new ItemCopyJob(Item(INT_MAX), Collection::root()); + QVERIFY(!copy->exec()); + } + +}; + +QTEST_AKONADIMAIN(ItemCopyTest) + +#include "itemcopytest.moc" diff --git a/autotests/libs/itemdeletetest.cpp b/autotests/libs/itemdeletetest.cpp new file mode 100644 index 0000000..ecbbb16 --- /dev/null +++ b/autotests/libs/itemdeletetest.cpp @@ -0,0 +1,188 @@ +/* + Copyright (c) 20089 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include "test_utils.h" + +#include + +#include + +using namespace Akonadi; + +class ItemDeleteTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + } + + void testIllegalDelete() + { + ItemDeleteJob *djob = new ItemDeleteJob(Item(INT_MAX), this); + QVERIFY(!djob->exec()); + + // make sure a failed delete doesn't leave a transaction open (the kpilot bug) + TransactionRollbackJob *tjob = new TransactionRollbackJob(this); + QVERIFY(!tjob->exec()); + } + + void testDelete() + { + ItemFetchJob *fjob = new ItemFetchJob(Item(1), this); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + + ItemDeleteJob *djob = new ItemDeleteJob(Item(1), this); + AKVERIFYEXEC(djob); + + fjob = new ItemFetchJob(Item(1), this); + QVERIFY(!fjob->exec()); + } + + void testDeleteFromUnselectedCollection() + { + const QString path = QStringLiteral("res1") + + CollectionPathResolver::pathDelimiter() + + QStringLiteral("foo"); + CollectionPathResolver *rjob = new CollectionPathResolver(path, this); + AKVERIFYEXEC(rjob); + + ItemFetchJob *fjob = new ItemFetchJob(Collection(rjob->collection()), this); + AKVERIFYEXEC(fjob); + + const Item::List items = fjob->items(); + QVERIFY(items.count() > 0); + + fjob = new ItemFetchJob(items[ 0 ], this); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + + ItemDeleteJob *djob = new ItemDeleteJob(items[ 0 ], this); + AKVERIFYEXEC(djob); + + fjob = new ItemFetchJob(items[ 0 ], this); + QVERIFY(!fjob->exec()); + } + + void testRidDelete() + { + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + const Collection col(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + + Item i; + i.setRemoteId(QStringLiteral("C")); + + ItemFetchJob *fjob = new ItemFetchJob(i, this); + fjob->setCollection(col); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + + ItemDeleteJob *djob = new ItemDeleteJob(i, this); + AKVERIFYEXEC(djob); + + fjob = new ItemFetchJob(i, this); + fjob->setCollection(col); + QVERIFY(!fjob->exec()); + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } + } + + void testTagDelete() + { + // Create tag + Tag tag; + tag.setName(QStringLiteral("Tag1")); + tag.setGid("Tag1"); + TagCreateJob *tjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(tjob); + tag = tjob->tag(); + + const Collection col(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + + Item i; + i.setRemoteId(QStringLiteral("D")); + + ItemFetchJob *fjob = new ItemFetchJob(i, this); + fjob->setCollection(col); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + + i = fjob->items().first(); + i.setTag(tag); + ItemModifyJob *mjob = new ItemModifyJob(i, this); + AKVERIFYEXEC(mjob); + + // Delete the tagged item + ItemDeleteJob *djob = new ItemDeleteJob(tag, this); + AKVERIFYEXEC(djob); + + // Try to fetch the item again, there should be none + fjob = new ItemFetchJob(i, this); + QVERIFY(!fjob->exec()); + } + + void testCollectionDelete() + { + const Collection col(collectionIdFromPath(QStringLiteral("res1/foo"))); + ItemFetchJob *fjob = new ItemFetchJob(col, this); + AKVERIFYEXEC(fjob); + QVERIFY(fjob->items().count() > 0); + + // delete from non-empty collection + ItemDeleteJob *djob = new ItemDeleteJob(col, this); + AKVERIFYEXEC(djob); + + fjob = new ItemFetchJob(col, this); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 0); + + // delete from empty collection + djob = new ItemDeleteJob(col, this); + QVERIFY(!djob->exec()); // error: no items found + + fjob = new ItemFetchJob(col, this); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 0); + } + +}; + +QTEST_AKONADIMAIN(ItemDeleteTest) + +#include "itemdeletetest.moc" diff --git a/autotests/libs/itemfetchtest.cpp b/autotests/libs/itemfetchtest.cpp new file mode 100644 index 0000000..94333bc --- /dev/null +++ b/autotests/libs/itemfetchtest.cpp @@ -0,0 +1,280 @@ +/* + 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 "itemfetchtest.h" +#include "collectionpathresolver.h" +#include "testattribute.h" + +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +#include + +QTEST_AKONADIMAIN(ItemFetchTest) + +void ItemFetchTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + qRegisterMetaType(); + AttributeFactory::registerAttribute(); +} + +void ItemFetchTest::testFetch() +{ + CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("res1"), this); + AKVERIFYEXEC(resolver); + int colId = resolver->collection(); + + // listing of an empty folder + ItemFetchJob *job = new ItemFetchJob(Collection(colId), this); + AKVERIFYEXEC(job); + QVERIFY(job->items().isEmpty()); + + resolver = new CollectionPathResolver(QStringLiteral("res1/foo"), this); + AKVERIFYEXEC(resolver); + int colId2 = resolver->collection(); + + // listing of a non-empty folder + job = new ItemFetchJob(Collection(colId2), this); + QSignalSpy spy(job, SIGNAL(itemsReceived(Akonadi::Item::List))); + QVERIFY(spy.isValid()); + AKVERIFYEXEC(job); + Item::List items = job->items(); + QCOMPARE(items.count(), 15); + + int count = 0; + for (int i = 0; i < spy.count(); ++i) { + Item::List l = spy[i][0].value(); + for (int j = 0; j < l.count(); ++j) { + QVERIFY(items.count() > count + j); + QCOMPARE(items[count + j], l[j]); + } + count += l.count(); + } + QCOMPARE(count, items.count()); + + // check if the fetch response is parsed correctly (note: order is undefined) + Item item; + foreach (const Item &it, items) { + if (it.remoteId() == QLatin1String("A")) { + item = it; + } + } + QVERIFY(item.isValid()); + + QCOMPARE(item.flags().count(), 3); + QVERIFY(item.hasFlag("\\SEEN")); + QVERIFY(item.hasFlag("\\FLAGGED")); + QVERIFY(item.hasFlag("\\DRAFT")); + + item = Item(); + foreach (const Item &it, items) { + if (it.remoteId() == QLatin1String("B")) { + item = it; + } + } + QVERIFY(item.isValid()); + QCOMPARE(item.flags().count(), 1); + QVERIFY(item.hasFlag("\\FLAGGED")); + + item = Item(); + foreach (const Item &it, items) { + if (it.remoteId() == QLatin1String("C")) { + item = it; + } + } + QVERIFY(item.isValid()); + QVERIFY(item.flags().isEmpty()); +} + +void ItemFetchTest::testResourceRetrieval() +{ + Item item(1); + + ItemFetchJob *job = new ItemFetchJob(item, this); + job->fetchScope().fetchFullPayload(true); + job->fetchScope().fetchAllAttributes(true); + job->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), 1); + item = job->items().first(); + QCOMPARE(item.id(), 1ll); + QVERIFY(!item.remoteId().isEmpty()); + QVERIFY(!item.hasPayload()); // not yet in cache + QCOMPARE(item.attributes().count(), 1); + + job = new ItemFetchJob(item, this); + job->fetchScope().fetchFullPayload(true); + job->fetchScope().fetchAllAttributes(true); + job->fetchScope().setCacheOnly(false); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), 1); + item = job->items().first(); + QCOMPARE(item.id(), 1ll); + QVERIFY(!item.remoteId().isEmpty()); + QVERIFY(item.hasPayload()); + QCOMPARE(item.attributes().count(), 1); +} + +void ItemFetchTest::testIllegalFetch() +{ + // fetch non-existing folder + ItemFetchJob *job = new ItemFetchJob(Collection(INT_MAX), this); + QVERIFY(!job->exec()); + + // listing of root + job = new ItemFetchJob(Collection::root(), this); + QVERIFY(!job->exec()); + + // fetch a non-existing message + job = new ItemFetchJob(Item(INT_MAX), this); + QVERIFY(!job->exec()); + QVERIFY(job->items().isEmpty()); + + // fetch message with empty reference + job = new ItemFetchJob(Item(), this); + QVERIFY(!job->exec()); +} + +void ItemFetchTest::testMultipartFetch_data() +{ + QTest::addColumn("fetchFullPayload"); + QTest::addColumn("fetchAllAttrs"); + QTest::addColumn("fetchSinglePayload"); + QTest::addColumn("fetchSingleAttr"); + + QTest::newRow("empty") << false << false << false << false; + QTest::newRow("full") << true << true << false << false; + QTest::newRow("full payload") << true << false << false << false; + QTest::newRow("single payload") << false << false << true << false; + QTest::newRow("single") << false << false << true << true; + QTest::newRow("attr full") << false << true << false << false; + QTest::newRow("attr single") << false << false << false << true; + QTest::newRow("mixed cross 1") << true << false << false << true; + QTest::newRow("mixed cross 2") << false << true << true << false; + QTest::newRow("all") << true << true << true << true; + QTest::newRow("all payload") << true << false << true << false; + QTest::newRow("all attr") << false << true << true << false; +} + +void ItemFetchTest::testMultipartFetch() +{ + QFETCH(bool, fetchFullPayload); + QFETCH(bool, fetchAllAttrs); + QFETCH(bool, fetchSinglePayload); + QFETCH(bool, fetchSingleAttr); + + CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("res1/foo"), this); + AKVERIFYEXEC(resolver); + int colId = resolver->collection(); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("body data"); + item.attribute(Item::AddIfMissing)->data = "extra data"; + ItemCreateJob *job = new ItemCreateJob(item, Collection(colId), this); + AKVERIFYEXEC(job); + Item ref = job->item(); + + ItemFetchJob *fjob = new ItemFetchJob(ref, this); + fjob->setCollection(Collection(colId)); + if (fetchFullPayload) { + fjob->fetchScope().fetchFullPayload(); + } + if (fetchAllAttrs) { + fjob->fetchScope().fetchAttribute(); + } + if (fetchSinglePayload) { + fjob->fetchScope().fetchPayloadPart(Item::FullPayload); + } + if (fetchSingleAttr) { + fjob->fetchScope().fetchAttribute(); + } + + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items().first(); + + if (fetchFullPayload || fetchSinglePayload) { + QCOMPARE(item.loadedPayloadParts().count(), 1); + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), QByteArray("body data")); + } else { + QCOMPARE(item.loadedPayloadParts().count(), 0); + QVERIFY(!item.hasPayload()); + } + + if (fetchAllAttrs || fetchSingleAttr) { + QCOMPARE(item.attributes().count(), 1); + QVERIFY(item.hasAttribute()); + QCOMPARE(item.attribute()->data, QByteArray("extra data")); + } else { + QCOMPARE(item.attributes().count(), 0); + } + + // cleanup + ItemDeleteJob *djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); +} + +void ItemFetchTest::testRidFetch() +{ + Item item; + item.setRemoteId(QStringLiteral("A")); + Collection col; + col.setRemoteId(QStringLiteral("10")); + + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0"), this); + AKVERIFYEXEC(select); + + ItemFetchJob *job = new ItemFetchJob(item, this); + job->setCollection(col); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), 1); + item = job->items().first(); + QVERIFY(item.isValid()); + QCOMPARE(item.remoteId(), QString::fromLatin1("A")); + QCOMPARE(item.mimeType(), QString::fromLatin1("application/octet-stream")); +} + +void ItemFetchTest::testAncestorRetrieval() +{ + ItemFetchJob *job = new ItemFetchJob(Item(1), this); + job->fetchScope().setAncestorRetrieval(ItemFetchScope::All); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), 1); + const Item item = job->items().first(); + QVERIFY(item.isValid()); + QCOMPARE(item.remoteId(), QString::fromLatin1("A")); + QCOMPARE(item.mimeType(), QString::fromLatin1("application/octet-stream")); + const Collection c = item.parentCollection(); + QCOMPARE(c.remoteId(), QLatin1String("10")); + const Collection c2 = c.parentCollection(); + QCOMPARE(c2.remoteId(), QLatin1String("6")); + const Collection c3 = c2.parentCollection(); + QCOMPARE(c3, Collection::root()); + +} + diff --git a/autotests/libs/itemfetchtest.h b/autotests/libs/itemfetchtest.h new file mode 100644 index 0000000..afbf345 --- /dev/null +++ b/autotests/libs/itemfetchtest.h @@ -0,0 +1,39 @@ +/* + 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 ITEMFETCHTEST_H +#define ITEMFETCHTEST_H + +#include + +class ItemFetchTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testFetch(); + void testResourceRetrieval(); + void testIllegalFetch(); + void testMultipartFetch_data(); + void testMultipartFetch(); + void testRidFetch(); + void testAncestorRetrieval(); +}; + +#endif diff --git a/autotests/libs/itemhydratest.cpp b/autotests/libs/itemhydratest.cpp new file mode 100644 index 0000000..e53f3e3 --- /dev/null +++ b/autotests/libs/itemhydratest.cpp @@ -0,0 +1,338 @@ +/* + Copyright (c) 2006 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. +*/ + +#include "itemhydratest.h" + +#include +#include "item.h" +#include +#include +#include + +using namespace Akonadi; + +struct Volker { + bool operator==(const Volker &f) const + { + return f.who == who; + } + virtual ~Volker() { } + virtual Volker *clone() const = 0; + QString who; +}; +typedef std::shared_ptr VolkerPtr; +typedef QSharedPointer VolkerQPtr; + +struct Rudi: public Volker { + Rudi() + { + who = QStringLiteral("Rudi"); + } + virtual ~Rudi() { } + Rudi *clone() const Q_DECL_OVERRIDE + { + return new Rudi(*this); + } +}; + +typedef std::shared_ptr RudiPtr; +typedef QSharedPointer RudiQPtr; + +struct Gerd: public Volker { + Gerd() + { + who = QStringLiteral("Gerd"); + } + Gerd *clone() const Q_DECL_OVERRIDE + { + return new Gerd(*this); + } +}; + +typedef std::shared_ptr GerdPtr; +typedef QSharedPointer GerdQPtr; + +Q_DECLARE_METATYPE(Volker *) +Q_DECLARE_METATYPE(Rudi *) +Q_DECLARE_METATYPE(Gerd *) + +Q_DECLARE_METATYPE(Rudi) +Q_DECLARE_METATYPE(Gerd) + +namespace Akonadi +{ +template <> struct SuperClass : public SuperClassTrait {}; +template <> struct SuperClass : public SuperClassTrait {}; +} + +QTEST_MAIN(ItemHydra) + +ItemHydra::ItemHydra() +{ +} + +void ItemHydra::initTestCase() +{ +} + +void ItemHydra::testItemValuePayload() +{ + Item f; + Rudi rudi; + f.setPayload(rudi); + QVERIFY(f.hasPayload()); + + Item b; + Gerd gerd; + b.setPayload(gerd); + QVERIFY(b.hasPayload()); + + QCOMPARE(f.payload(), rudi); + QVERIFY(!(f.payload() == gerd)); + QCOMPARE(b.payload(), gerd); + QVERIFY(!(b.payload() == rudi)); +} + +void ItemHydra::testItemPointerPayload() +{ + Item f; + Rudi *rudi = new Rudi; + + // the below should not compile + //f.setPayload( rudi ); + + // std::auto_ptr is not copyconstructable and assignable, therefore this will fail as well + //f.setPayload( std::auto_ptr( rudi ) ); + //QVERIFY( f.hasPayload() ); + //QCOMPARE( f.payload< std::auto_ptr >()->who, rudi->who ); + + // below doesn't compile, hopefully + //QCOMPARE( f.payload< Rudi* >()->who, rudi->who ); + + delete rudi; +} + +void ItemHydra::testItemCopy() +{ + Item f; + Rudi rudi; + f.setPayload(rudi); + + Item r = f; + QCOMPARE(r.payload(), rudi); + + Item s; + s = f; + QVERIFY(s.hasPayload()); + QCOMPARE(s.payload(), rudi); + +} + +void ItemHydra::testEmptyPayload() +{ + Item i1; + Item i2; + i1 = i2; // should not crash + + QVERIFY(!i1.hasPayload()); + QVERIFY(!i2.hasPayload()); + QVERIFY(!i1.hasPayload()); + QVERIFY(!i1.hasPayload()); + + bool caughtException = false; + bool caughtRightException = true; + try { + Rudi r = i1.payload(); + } catch (const Akonadi::PayloadException &e) { + qDebug() << e.what(); + caughtException = true; + caughtRightException = true; + } catch (const Akonadi::Exception &e) { + qDebug() << "Caught Akonadi exception of type " << typeid(e).name() << ": " << e.what() + << ", expected type" << typeid(Akonadi::PayloadException).name(); + caughtException = true; + caughtRightException = false; + } catch (const std::exception &e) { + qDebug() << "Caught exception of type " << typeid(e).name() << ": " << e.what() + << ", expected type" << typeid(Akonadi::PayloadException).name(); + caughtException = true; + caughtRightException = false; + } catch (...) { + qDebug() << "Caught unknown exception"; + caughtException = true; + caughtRightException = false; + } + QVERIFY(caughtException); + QVERIFY(caughtRightException); +} + +void ItemHydra::testPointerPayload() +{ + Rudi *r = new Rudi; + RudiPtr p(r); + std::weak_ptr w(p); + QCOMPARE(p.use_count(), (long)1); + + { + Item i1; + i1.setPayload(p); + QVERIFY(i1.hasPayload()); + QCOMPARE(p.use_count(), (long)2); + { + QVERIFY(i1.hasPayload< RudiPtr >()); + RudiPtr p2 = i1.payload< RudiPtr >(); + QCOMPARE(p.use_count(), (long)3); + } + + { + QVERIFY(i1.hasPayload< VolkerPtr >()); + VolkerPtr p2 = i1.payload< VolkerPtr >(); + QCOMPARE(p.use_count(), (long)3); + } + + QCOMPARE(p.use_count(), (long)2); + } + QCOMPARE(p.use_count(), (long)1); + QCOMPARE(w.use_count(), (long)1); + p.reset(); + QCOMPARE(w.use_count(), (long)0); +} + +void ItemHydra::testPolymorphicPayload() +{ + VolkerPtr p(new Rudi); + + { + Item i1; + i1.setPayload(p); + QVERIFY(i1.hasPayload()); + QVERIFY(i1.hasPayload()); + QVERIFY(i1.hasPayload()); + QVERIFY(!i1.hasPayload()); + QCOMPARE(p.use_count(), (long)2); + { + RudiPtr p2 = std::dynamic_pointer_cast(i1.payload< VolkerPtr >()); + QCOMPARE(p.use_count(), (long)3); + QCOMPARE(p2->who, QStringLiteral("Rudi")); + } + + { + RudiPtr p2 = i1.payload< RudiPtr >(); + QCOMPARE(p.use_count(), (long)3); + QCOMPARE(p2->who, QStringLiteral("Rudi")); + } + + bool caughtException = false; + try { + GerdPtr p3 = i1.payload(); + } catch (const Akonadi::PayloadException &e) { + qDebug() << e.what(); + caughtException = true; + } + QVERIFY(caughtException); + + QCOMPARE(p.use_count(), (long)2); + } +} + +void ItemHydra::testNullPointerPayload() +{ + RudiPtr p((Rudi *)0); + Item i; + i.setPayload(p); + QVERIFY(i.hasPayload()); + QVERIFY(i.hasPayload()); + QVERIFY(i.hasPayload()); + // Fails, because GerdQPtr is QSharedPointer, while RudiPtr is std::shared_ptr + // and we cannot do sharedptr casting for null pointers + QVERIFY(!i.hasPayload()); + QCOMPARE(i.payload().get(), (Rudi *)0); + QCOMPARE(i.payload().get(), (Volker *)0); +} + +void ItemHydra::testQSharedPointerPayload() +{ + RudiQPtr p(new Rudi); + Item i; + i.setPayload(p); + QVERIFY(i.hasPayload()); + QVERIFY(i.hasPayload()); + QVERIFY(i.hasPayload()); + QVERIFY(!i.hasPayload()); + + { + VolkerQPtr p2 = i.payload< VolkerQPtr >(); + QCOMPARE(p2->who, QStringLiteral("Rudi")); + } + + { + RudiQPtr p2 = i.payload< RudiQPtr >(); + QCOMPARE(p2->who, QStringLiteral("Rudi")); + } + + bool caughtException = false; + try { + GerdQPtr p3 = i.payload(); + } catch (const Akonadi::PayloadException &e) { + qDebug() << e.what(); + caughtException = true; + } + QVERIFY(caughtException); +} + +void ItemHydra::testHasPayload() +{ + Item i1; + QVERIFY(!i1.hasPayload()); + QVERIFY(!i1.hasPayload()); + + Rudi r; + i1.setPayload(r); + QVERIFY(i1.hasPayload()); + QVERIFY(!i1.hasPayload()); +} + +void ItemHydra::testSharedPointerConversions() +{ + + Item a; + RudiQPtr rudi(new Rudi); + a.setPayload(rudi); + // only the root base classes should show up with their metatype ids: + QVERIFY(a.availablePayloadMetaTypeIds().contains(qMetaTypeId())); + QVERIFY(a.hasPayload()); + QVERIFY(a.hasPayload()); + QVERIFY(a.hasPayload()); + QVERIFY(!a.hasPayload()); + QVERIFY(a.payload().get()); + QVERIFY(a.payload().get()); + bool thrown = false, thrownCorrectly = true; + try { + QVERIFY(!a.payload()); + } catch (const Akonadi::PayloadException &e) { + thrown = thrownCorrectly = true; + } catch (...) { + thrown = true; + thrownCorrectly = false; + } + QVERIFY(thrown); + QVERIFY(thrownCorrectly); + +} + diff --git a/autotests/libs/itemhydratest.h b/autotests/libs/itemhydratest.h new file mode 100644 index 0000000..97f5b53 --- /dev/null +++ b/autotests/libs/itemhydratest.h @@ -0,0 +1,47 @@ +/* + Copyright (c) 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 ITEMHYDRA_H +#define ITEMHYDRA_H + +#include + +class ItemHydra : public QObject +{ + Q_OBJECT +public: + ItemHydra(); + virtual ~ItemHydra() + { + } +private Q_SLOTS: + void initTestCase(); + void testItemValuePayload(); + void testItemPointerPayload(); + void testItemCopy(); + void testEmptyPayload(); + void testPointerPayload(); + void testPolymorphicPayload(); + void testNullPointerPayload(); + void testQSharedPointerPayload(); + void testHasPayload(); + void testSharedPointerConversions(); +}; + +#endif diff --git a/autotests/libs/itemmovetest.cpp b/autotests/libs/itemmovetest.cpp new file mode 100644 index 0000000..3726266 --- /dev/null +++ b/autotests/libs/itemmovetest.cpp @@ -0,0 +1,117 @@ +/* + 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 "collection.h" +#include "control.h" +#include "itemfetchjob.h" +#include "itemmovejob.h" +#include "itemfetchscope.h" + +#include + +#include +#include + +using namespace Akonadi; + +class ItemMoveTest: public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + } + + // TODO: test inter and intra resource moves + void testMove_data() + { + QTest::addColumn("items"); + QTest::addColumn("destination"); + QTest::addColumn("source"); + + const Collection destination(collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(destination.isValid()); + + QTest::newRow("single uid") << (Item::List() << Item(1)) << destination << Collection(); + QTest::newRow("two uid") << (Item::List() << Item(2) << Item(3)) << destination << Collection(); + Item r1; r1.setRemoteId(QStringLiteral("D")); + Collection ridDest; + ridDest.setRemoteId(QStringLiteral("3")); + Collection ridSource; + ridSource.setRemoteId(QStringLiteral("10")); + QTest::newRow("single rid") << (Item::List() << r1) << ridDest << ridSource; + } + + void testMove() + { + QFETCH(Item::List, items); + QFETCH(Collection, destination); + QFETCH(Collection, source); + + //Collection source( collectionIdFromPath( "res1/foo" ) ); + //QVERIFY( source.isValid() ); + + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); // for rid based moves + + ItemFetchJob *prefetchjob = new ItemFetchJob(destination, this); + AKVERIFYEXEC(prefetchjob); + int baseline = prefetchjob->items().size(); + + ItemMoveJob *move = new ItemMoveJob(items, source, destination, this); + AKVERIFYEXEC(move); + + ItemFetchJob *fetch = new ItemFetchJob(destination, this); + fetch->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), items.count() + baseline); + foreach (const Item &movedItem, fetch->items()) { + QVERIFY(movedItem.hasPayload()); + QVERIFY(!movedItem.payload().isEmpty()); + } + } + + void testIllegalMove() + { + Collection col(collectionIdFromPath(QStringLiteral("res2"))); + QVERIFY(col.isValid()); + + ItemFetchJob *prefetchjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(prefetchjob); + QCOMPARE(prefetchjob->items().count(), 1); + Item item = prefetchjob->items()[0]; + + // move into invalid collection + ItemMoveJob *store = new ItemMoveJob(item, Collection(INT_MAX), this); + QVERIFY(!store->exec()); + + // move item into folder that doesn't support its mimetype + store = new ItemMoveJob(item, col, this); + QEXPECT_FAIL("", "Check not yet implemented by the server.", Continue); + QVERIFY(!store->exec()); + } +}; + +QTEST_AKONADIMAIN(ItemMoveTest) + +#include "itemmovetest.moc" diff --git a/autotests/libs/itemsearchjobtest.cpp b/autotests/libs/itemsearchjobtest.cpp new file mode 100644 index 0000000..eaba195 --- /dev/null +++ b/autotests/libs/itemsearchjobtest.cpp @@ -0,0 +1,108 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "collection.h" +#include "item.h" +#include "agentmanager.h" +#include "agentinstance.h" +#include "itemsearchjob.h" +#include "searchquery.h" + +Q_DECLARE_METATYPE(QSet) +Q_DECLARE_METATYPE(Akonadi::SearchQuery) + +using namespace Akonadi; +class ItemSearchJobTest : public QObject +{ + Q_OBJECT +private: + Akonadi::SearchQuery createQuery(const QString &key, const QSet< qint64 > &resultSet) + { + Akonadi::SearchQuery query; + foreach (qint64 id, resultSet) { + query.addTerm(Akonadi::SearchTerm(key, id)); + } + return query; + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + Akonadi::AgentInstance agent = Akonadi::AgentManager::self()->instance(QStringLiteral("akonadi_knut_resource_0")); + QVERIFY(agent.isValid()); + agent.setIsOnline(true); + QTRY_VERIFY(agent.isOnline()); + } + + void testItemSearch_data() + { + QTest::addColumn("remoteSearchEnabled"); + QTest::addColumn("query"); + QTest::addColumn >("resultSet"); + + { + QSet resultSet; + resultSet << 1 << 2 << 3; + QTest::newRow("plugin search") << false << createQuery(QStringLiteral("plugin"), resultSet) << resultSet; + } + { + QSet resultSet; + resultSet << 1 << 2 << 3; + QTest::newRow("resource search") << true << createQuery(QStringLiteral("resource"), resultSet) << resultSet; + } + { + QSet resultSet; + resultSet << 1 << 2 << 3 << 4; + Akonadi::SearchQuery query; + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 1)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 2)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 3)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 4)); + QTest::newRow("mixed search: results are merged") << true << query << resultSet; + } + } + + void testItemSearch() + { + QFETCH(bool, remoteSearchEnabled); + QFETCH(SearchQuery, query); + QFETCH(QSet, resultSet); + + ItemSearchJob *itemSearchJob = new ItemSearchJob(query, this); + itemSearchJob->setRemoteSearchEnabled(remoteSearchEnabled); + itemSearchJob->setSearchCollections(Collection::List() << Collection::root()); + itemSearchJob->setRecursive(true); + AKVERIFYEXEC(itemSearchJob); + QSet actualResultSet; + foreach (const Item &item, itemSearchJob->items()) { + actualResultSet << item.id(); + } + qDebug() << actualResultSet << resultSet; + QCOMPARE(actualResultSet, resultSet); + } + +}; + +QTEST_AKONADIMAIN(ItemSearchJobTest) + +#include "itemsearchjobtest.moc" diff --git a/autotests/libs/itemserializertest.cpp b/autotests/libs/itemserializertest.cpp new file mode 100644 index 0000000..1119710 --- /dev/null +++ b/autotests/libs/itemserializertest.cpp @@ -0,0 +1,68 @@ +/* + 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 "itemserializertest.h" + +#include "attributefactory.h" +#include "item.h" +#include "itemserializer_p.h" + +#include + +using namespace Akonadi; + +QTEST_MAIN(ItemSerializerTest) + +void ItemSerializerTest::testEmptyPayload() +{ + // should not crash + QByteArray data; + Item item; + ItemSerializer::deserialize(item, Item::FullPayload, data, 0, false); + QVERIFY(data.isEmpty()); +} + +void ItemSerializerTest::testDefaultSerializer_data() +{ + QTest::addColumn("serialized"); + + QTest::newRow("null") << QByteArray(); + QTest::newRow("empty") << QByteArray(""); + QTest::newRow("nullbytei") << QByteArray("\0", 1); + QTest::newRow("mixed") << QByteArray("\0\r\n\0bla", 7); +} + +void ItemSerializerTest::testDefaultSerializer() +{ + QFETCH(QByteArray, serialized); + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + ItemSerializer::deserialize(item, Item::FullPayload, serialized, 0, false); + + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), serialized); + + QByteArray data; + int version = 0; + ItemSerializer::serialize(item, Item::FullPayload, data, version); + QCOMPARE(data, serialized); + QEXPECT_FAIL("null", "Serializer cannot distinguish null vs. empty", Continue); + QCOMPARE(data.isNull(), serialized.isNull()); +} + diff --git a/autotests/libs/itemserializertest.h b/autotests/libs/itemserializertest.h new file mode 100644 index 0000000..bf43b75 --- /dev/null +++ b/autotests/libs/itemserializertest.h @@ -0,0 +1,34 @@ +/* + 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 ITEMSERIALIZERTEST_H +#define ITEMSERIALIZERTEST_H + +#include + +class ItemSerializerTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testEmptyPayload(); + void testDefaultSerializer_data(); + void testDefaultSerializer(); +}; + +#endif diff --git a/autotests/libs/itemstoretest.cpp b/autotests/libs/itemstoretest.cpp new file mode 100644 index 0000000..53dc0dc --- /dev/null +++ b/autotests/libs/itemstoretest.cpp @@ -0,0 +1,383 @@ +/* + Copyright (c) 2006 Volker Krause + Copyright (c) 2007 Robert Zwerus + + 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 "itemstoretest.h" + +#include "control.h" +#include "testattribute.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "test_utils.h" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(ItemStoreTest) + +static Collection res1_foo; +static Collection res2; +static Collection res3; + +void ItemStoreTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AttributeFactory::registerAttribute(); + + // get the collections we run the tests on + res1_foo = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(res1_foo.isValid()); + res2 = Collection(collectionIdFromPath(QStringLiteral("res2"))); + QVERIFY(res2.isValid()); + res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(res3.isValid()); + + AkonadiTest::setAllResourcesOffline(); +} + +void ItemStoreTest::testFlagChange() +{ + ItemFetchJob *fjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + Item item = fjob->items()[0]; + + // add a flag + Item::Flags origFlags = item.flags(); + Item::Flags expectedFlags = origFlags; + expectedFlags.insert("added_test_flag_1"); + item.setFlag("added_test_flag_1"); + ItemModifyJob *sjob = new ItemModifyJob(item, this); + AKVERIFYEXEC(sjob); + + fjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QCOMPARE(item.flags().count(), expectedFlags.count()); + Item::Flags diff = expectedFlags - item.flags(); + QVERIFY(diff.isEmpty()); + + // set flags + expectedFlags.insert("added_test_flag_2"); + item.setFlags(expectedFlags); + sjob = new ItemModifyJob(item, this); + AKVERIFYEXEC(sjob); + + fjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QCOMPARE(item.flags().count(), expectedFlags.count()); + diff = expectedFlags - item.flags(); + QVERIFY(diff.isEmpty()); + + // remove a flag + item.clearFlag("added_test_flag_1"); + item.clearFlag("added_test_flag_2"); + sjob = new ItemModifyJob(item, this); + AKVERIFYEXEC(sjob); + + fjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QCOMPARE(item.flags().count(), origFlags.count()); + diff = origFlags - item.flags(); + QVERIFY(diff.isEmpty()); +} + +void ItemStoreTest::testDataChange_data() +{ + QTest::addColumn("data"); + + QTest::newRow("simple") << QByteArray("testbody"); + QTest::newRow("null") << QByteArray(); + QTest::newRow("empty") << QByteArray(""); + QTest::newRow("nullbyte") << QByteArray("\0", 1); + QTest::newRow("nullbyte2") << QByteArray("\0X", 2); + QTest::newRow("linebreaks") << QByteArray("line1\nline2\n\rline3\rline4\r\n"); + QTest::newRow("linebreaks2") << QByteArray("line1\r\nline2\r\n\r\n"); + QTest::newRow("linebreaks3") << QByteArray("line1\nline2"); + QByteArray b; + QTest::newRow("big") << b.fill('a', 1 << 20); + QTest::newRow("bignull") << b.fill('\0', 1 << 20); + QTest::newRow("bigcr") << b.fill('\r', 1 << 20); + QTest::newRow("biglf") << b.fill('\n', 1 << 20); +} + +void ItemStoreTest::testDataChange() +{ + QFETCH(QByteArray, data); + + Item item; + ItemFetchJob *prefetchjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(prefetchjob); + item = prefetchjob->items()[0]; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload(data); + QCOMPARE(item.payload(), data); + + // modify data + ItemModifyJob *sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); + + ItemFetchJob *fjob = new ItemFetchJob(Item(1)); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), data); + QEXPECT_FAIL("null", "STORE will not update item size on 0 sizes", Continue); + QEXPECT_FAIL("empty", "STORE will not update item size on 0 sizes", Continue); + QCOMPARE(item.size(), static_cast(data.size())); +} + +void ItemStoreTest::testRemoteId_data() +{ + QTest::addColumn("rid"); + QTest::addColumn("exprid"); + + QTest::newRow("set") << QString::fromLatin1("A") << QString::fromLatin1("A"); + QTest::newRow("no-change") << QString() << QString::fromLatin1("A"); + QTest::newRow("clear") << QString::fromLatin1("") << QString::fromLatin1(""); + QTest::newRow("reset") << QString::fromLatin1("A") << QString::fromLatin1("A"); + QTest::newRow("utf8") << QString::fromUtf8("ä ö ü @") << QString::fromUtf8("ä ö ü @"); +} + +void ItemStoreTest::testRemoteId() +{ + QFETCH(QString, rid); + QFETCH(QString, exprid); + + // pretend to be a resource, we cannot change remote identifiers otherwise + ResourceSelectJob *rsel = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0"), this); + AKVERIFYEXEC(rsel); + + ItemFetchJob *prefetchjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(prefetchjob); + Item item = prefetchjob->items()[0]; + + item.setRemoteId(rid); + ItemModifyJob *store = new ItemModifyJob(item, this); + store->disableRevisionCheck(); + store->setIgnorePayload(true); // we only want to update the remote id + AKVERIFYEXEC(store); + + ItemFetchJob *fetch = new ItemFetchJob(item, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().at(0); + QCOMPARE(item.remoteId().toUtf8(), exprid.toUtf8()); + + // no longer pretend to be a resource + rsel = new ResourceSelectJob(QString(), this); + AKVERIFYEXEC(rsel); +} + +void ItemStoreTest::testMultiPart() +{ + ItemFetchJob *prefetchjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(prefetchjob); + QCOMPARE(prefetchjob->items().count(), 1); + Item item = prefetchjob->items()[0]; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("testmailbody"); + item.attribute(Item::AddIfMissing)->data = "extra"; + + // store item + ItemModifyJob *sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); + + ItemFetchJob *fjob = new ItemFetchJob(Item(1)); + fjob->fetchScope().fetchAttribute(); + fjob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), QByteArray("testmailbody")); + QVERIFY(item.hasAttribute()); + QCOMPARE(item.attribute()->data, QByteArray("extra")); + + // clean up + item.removeAttribute("EXTRA"); + sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); +} + +void ItemStoreTest::testPartRemove() +{ + ItemFetchJob *prefetchjob = new ItemFetchJob(Item(2)); + AKVERIFYEXEC(prefetchjob); + Item item = prefetchjob->items()[0]; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.attribute(Item::AddIfMissing)->data = "extra"; + + // store item + ItemModifyJob *sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); + + // fetch item and its parts (should be RFC822, HEAD and EXTRA) + ItemFetchJob *fjob = new ItemFetchJob(Item(2)); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().fetchAllAttributes(); + fjob->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QCOMPARE(item.attributes().count(), 2); + QVERIFY(item.hasAttribute()); + + // remove a part + item.removeAttribute(); + sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); + + // fetch item again (should only have RFC822 and HEAD left) + ItemFetchJob *fjob2 = new ItemFetchJob(Item(2)); + fjob2->fetchScope().fetchFullPayload(); + fjob2->fetchScope().fetchAllAttributes(); + fjob2->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fjob2); + QCOMPARE(fjob2->items().count(), 1); + item = fjob2->items()[0]; + QCOMPARE(item.attributes().count(), 1); + QVERIFY(!item.hasAttribute()); +} + +void ItemStoreTest::testRevisionCheck() +{ + // fetch same item twice + Item ref(2); + ItemFetchJob *prefetchjob = new ItemFetchJob(ref); + AKVERIFYEXEC(prefetchjob); + QCOMPARE(prefetchjob->items().count(), 1); + Item item1 = prefetchjob->items()[0]; + Item item2 = prefetchjob->items()[0]; + + // store first item unmodified + ItemModifyJob *sjob = new ItemModifyJob(item1); + AKVERIFYEXEC(sjob); + + // store the first item with modifications (should work) + item1.attribute(Item::AddIfMissing)->data = "random stuff 1"; + sjob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(sjob); + + // try to store second item with modifications (should be detected as a conflict) + item2.attribute(Item::AddIfMissing)->data = "random stuff 2"; + ItemModifyJob *sjob2 = new ItemModifyJob(item2); + sjob2->disableAutomaticConflictHandling(); + QVERIFY(!sjob2->exec()); + + // fetch same again + prefetchjob = new ItemFetchJob(ref); + AKVERIFYEXEC(prefetchjob); + item1 = prefetchjob->items()[0]; + + // delete item + ItemDeleteJob *djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); + + // try to store it + sjob = new ItemModifyJob(item1); + QVERIFY(!sjob->exec()); +} + +void ItemStoreTest::testModificationTime() +{ + Item item; + item.setMimeType(QStringLiteral("text/directory")); + QVERIFY(item.modificationTime().isNull()); + + ItemCreateJob *job = new ItemCreateJob(item, res1_foo); + AKVERIFYEXEC(job); + + // The item should have a datetime set now. + item = job->item(); + QVERIFY(!item.modificationTime().isNull()); + QDateTime initialDateTime = item.modificationTime(); + + // Fetch the same item again. + Item item2(item.id()); + ItemFetchJob *fjob = new ItemFetchJob(item2, this); + AKVERIFYEXEC(fjob); + item2 = fjob->items().first(); + QCOMPARE(initialDateTime, item2.modificationTime()); + + // Lets wait at least a second, which is the resolution of mtime + QTest::qWait(1000); + + // Modify the item + item.attribute(Item::AddIfMissing)->data = "extra"; + ItemModifyJob *mjob = new ItemModifyJob(item); + AKVERIFYEXEC(mjob); + + // The item should still have a datetime set and that date should be somewhere + // after the initialDateTime. + item = mjob->item(); + QVERIFY(!item.modificationTime().isNull()); + QVERIFY(initialDateTime < item.modificationTime()); + + // Fetch the item after modification. + Item item3(item.id()); + ItemFetchJob *fjob2 = new ItemFetchJob(item3, this); + AKVERIFYEXEC(fjob2); + + // item3 should have the same modification time as item. + item3 = fjob2->items().first(); + QCOMPARE(item3.modificationTime(), item.modificationTime()); + + // Clean up + ItemDeleteJob *idjob = new ItemDeleteJob(item, this); + AKVERIFYEXEC(idjob); +} + +void ItemStoreTest::testRemoteIdRace() +{ + // Create an item and store it + Item item; + item.setMimeType(QStringLiteral("text/directory")); + ItemCreateJob *job = new ItemCreateJob(item, res1_foo); + AKVERIFYEXEC(job); + + // Fetch the same item again. It should not have a remote Id yet, as the resource + // is offline. + // The remote id should be null, not only empty, so that item modify jobs with this + // item don't overwrite the remote id. + Item item2(job->item().id()); + ItemFetchJob *fetchJob = new ItemFetchJob(item2); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().size(), 1); + QVERIFY(fetchJob->items().first().remoteId().isNull()); +} + diff --git a/autotests/libs/itemstoretest.h b/autotests/libs/itemstoretest.h new file mode 100644 index 0000000..dc661be --- /dev/null +++ b/autotests/libs/itemstoretest.h @@ -0,0 +1,43 @@ +/* + Copyright (c) 2006 Volker Krause + Copyright (c) 2007 Robert Zwerus + + 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 ITEMSTORETEST_H +#define ITEMSTORETEST_H + +#include + +class ItemStoreTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testFlagChange(); + void testDataChange_data(); + void testDataChange(); + void testRemoteId_data(); + void testRemoteId(); + void testMultiPart(); + void testPartRemove(); + void testRevisionCheck(); + void testModificationTime(); + void testRemoteIdRace(); +}; + +#endif diff --git a/autotests/libs/itemsynctest.cpp b/autotests/libs/itemsynctest.cpp new file mode 100644 index 0000000..a55fbea --- /dev/null +++ b/autotests/libs/itemsynctest.cpp @@ -0,0 +1,582 @@ +/* + 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 "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +using namespace Akonadi; + +Q_DECLARE_METATYPE(KJob *) +Q_DECLARE_METATYPE(ItemSync::TransactionMode) + +class ItemsyncTest : public QObject +{ + Q_OBJECT +private: + Item::List fetchItems(const Collection &col) + { + qDebug() << col.remoteId(); + ItemFetchJob *fetch = new ItemFetchJob(col, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().fetchAllAttributes(); + fetch->fetchScope().setCacheOnly(true); // resources are switched off anyway + if (!fetch->exec()) { + []() { QFAIL("Failed to fetch items!"); }(); + } + return fetch->items(); + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + qRegisterMetaType(); + qRegisterMetaType(); + } + + static Item modifyItem(Item item) + { + static int counter = 0; + item.setFlag(QByteArray("\\READ") + QByteArray::number(counter)); + counter++; + return item; + } + + void testFullSync() + { + const Collection col = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + //Since the item sync affects the knut resource we ensure we actually managed to load all items + //This needs to be adjusted should the testdataset change + QCOMPARE(origItems.size(), 15); + + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(col); + QSignalSpy deletedSpy(&monitor, SIGNAL(itemRemoved(Akonadi::Item))); + QVERIFY(deletedSpy.isValid()); + QSignalSpy addedSpy(&monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QVERIFY(addedSpy.isValid()); + QSignalSpy changedSpy(&monitor, SIGNAL(itemChanged(Akonadi::Item,QSet))); + QVERIFY(changedSpy.isValid()); + + ItemSync *syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, SIGNAL(transactionCommitted())); + QVERIFY(transactionSpy.isValid()); + syncer->setFullSyncItems(origItems); + AKVERIFYEXEC(syncer); + QCOMPARE(transactionSpy.count(), 1); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + QTest::qWait(100); + QCOMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QCOMPARE(changedSpy.count(), 0); + } + + void testFullStreamingSync_data() + { + QTest::addColumn("transactionMode"); + QTest::addColumn("goToEventLoopAfterAddingItems"); + + QTest::newRow("single transaction, no eventloop") << ItemSync::SingleTransaction << false; + QTest::newRow("multi transaction, no eventloop") << ItemSync::MultipleTransactions << false; + QTest::newRow("single transaction, with eventloop") << ItemSync::SingleTransaction << true; + QTest::newRow("multi transaction, with eventloop") << ItemSync::MultipleTransactions << true; + } + + void testFullStreamingSync() + { + QFETCH(ItemSync::TransactionMode, transactionMode); + QFETCH(bool, goToEventLoopAfterAddingItems); + + const Collection col = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + QCOMPARE(origItems.size(), 15); + + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(col); + QSignalSpy deletedSpy(&monitor, SIGNAL(itemRemoved(Akonadi::Item))); + QVERIFY(deletedSpy.isValid()); + QSignalSpy addedSpy(&monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QVERIFY(addedSpy.isValid()); + QSignalSpy changedSpy(&monitor, SIGNAL(itemChanged(Akonadi::Item,QSet))); + QVERIFY(changedSpy.isValid()); + + ItemSync *syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, SIGNAL(transactionCommitted())); + QVERIFY(transactionSpy.isValid()); + syncer->setTransactionMode(transactionMode); + syncer->setBatchSize(1); + syncer->setAutoDelete(false); + syncer->setStreamingEnabled(true); + QSignalSpy spy(syncer, SIGNAL(result(KJob*))); + QVERIFY(spy.isValid()); + syncer->setTotalItems(origItems.count()); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < origItems.count(); ++i) { + Item::List l; + //Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setFullSyncItems(l); + if (goToEventLoopAfterAddingItems) { + QTest::qWait(0); + } + if (i < origItems.count() - 1) { + QCOMPARE(spy.count(), 0); + } + } + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + KJob *job = spy.at(0).at(0).value(); + QCOMPARE(job, syncer); + QCOMPARE(job->error(), 0); + if (transactionMode == ItemSync::SingleTransaction) { + QCOMPARE(transactionSpy.count(), 1); + } + if (transactionMode == ItemSync::MultipleTransactions) { + QCOMPARE(transactionSpy.count(), origItems.count()); + } + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + delete syncer; + QTest::qWait(100); + QTRY_COMPARE(deletedSpy.count(), 0); + QTRY_COMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), origItems.count()); + } + + void testIncrementalSync() + { + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + + const Collection col = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + QCOMPARE(origItems.size(), 15); + + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(col); + QSignalSpy deletedSpy(&monitor, SIGNAL(itemRemoved(Akonadi::Item))); + QVERIFY(deletedSpy.isValid()); + QSignalSpy addedSpy(&monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QVERIFY(addedSpy.isValid()); + QSignalSpy changedSpy(&monitor, SIGNAL(itemChanged(Akonadi::Item,QSet))); + QVERIFY(changedSpy.isValid()); + + { + ItemSync *syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, SIGNAL(transactionCommitted())); + QVERIFY(transactionSpy.isValid()); + syncer->setTransactionMode(ItemSync::SingleTransaction); + syncer->setIncrementalSyncItems(origItems, Item::List()); + AKVERIFYEXEC(syncer); + QCOMPARE(transactionSpy.count(), 1); + } + + QTest::qWait(100); + QTRY_COMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), 0); + deletedSpy.clear(); + addedSpy.clear(); + changedSpy.clear(); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + Item::List delItems; + delItems << resultItems.takeFirst(); + + Item itemWithOnlyRemoteId; + itemWithOnlyRemoteId.setRemoteId(resultItems.front().remoteId()); + delItems << itemWithOnlyRemoteId; + resultItems.takeFirst(); + + //This item will not be removed since it isn't existing locally + Item itemWithRandomRemoteId; + itemWithRandomRemoteId.setRemoteId(KRandom::randomString(100)); + delItems << itemWithRandomRemoteId; + + { + ItemSync *syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, SIGNAL(transactionCommitted())); + QVERIFY(transactionSpy.isValid()); + syncer->setIncrementalSyncItems(resultItems, delItems); + AKVERIFYEXEC(syncer); + QCOMPARE(transactionSpy.count(), 1); + } + + Item::List resultItems2 = fetchItems(col); + QCOMPARE(resultItems2.count(), resultItems.count()); + + QTest::qWait(100); + QTRY_COMPARE(deletedSpy.count(), 2); + QCOMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), 0); + + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } + } + + void testIncrementalStreamingSync() + { + const Collection col = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(col); + QSignalSpy deletedSpy(&monitor, SIGNAL(itemRemoved(Akonadi::Item))); + QVERIFY(deletedSpy.isValid()); + QSignalSpy addedSpy(&monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QVERIFY(addedSpy.isValid()); + QSignalSpy changedSpy(&monitor, SIGNAL(itemChanged(Akonadi::Item,QSet))); + QVERIFY(changedSpy.isValid()); + + ItemSync *syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, SIGNAL(transactionCommitted())); + QVERIFY(transactionSpy.isValid()); + syncer->setAutoDelete(false); + QSignalSpy spy(syncer, SIGNAL(result(KJob*))); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < origItems.count(); ++i) { + Item::List l; + //Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < origItems.count() - 1) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + KJob *job = spy.at(0).at(0).value(); + QCOMPARE(job, syncer); + QCOMPARE(job->error(), 0); + QCOMPARE(transactionSpy.count(), 1); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + delete syncer; + + QTest::qWait(100); + QCOMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), origItems.size()); + } + + void testEmptyIncrementalSync() + { + const Collection col = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(col); + QSignalSpy deletedSpy(&monitor, SIGNAL(itemRemoved(Akonadi::Item))); + QVERIFY(deletedSpy.isValid()); + QSignalSpy addedSpy(&monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QVERIFY(addedSpy.isValid()); + QSignalSpy changedSpy(&monitor, SIGNAL(itemChanged(Akonadi::Item,QSet))); + QVERIFY(changedSpy.isValid()); + + ItemSync *syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, SIGNAL(transactionCommitted())); + QVERIFY(transactionSpy.isValid()); + syncer->setIncrementalSyncItems(Item::List(), Item::List()); + AKVERIFYEXEC(syncer); + //It would be better if we didn't have a transaction at all, but so far the transaction is still created + QCOMPARE(transactionSpy.count(), 1); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + QTest::qWait(100); + QCOMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QCOMPARE(changedSpy.count(), 0); + } + + void testIncrementalStreamingSyncBatchProcessing() + { + const Collection col = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(col); + QSignalSpy deletedSpy(&monitor, SIGNAL(itemRemoved(Akonadi::Item))); + QVERIFY(deletedSpy.isValid()); + QSignalSpy addedSpy(&monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QVERIFY(addedSpy.isValid()); + QSignalSpy changedSpy(&monitor, SIGNAL(itemChanged(Akonadi::Item,QSet))); + QVERIFY(changedSpy.isValid()); + + ItemSync *syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, SIGNAL(transactionCommitted())); + QVERIFY(transactionSpy.isValid()); + QSignalSpy spy(syncer, SIGNAL(result(KJob*))); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + syncer->setTransactionMode(ItemSync::MultipleTransactions); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < syncer->batchSize(); ++i) { + Item::List l; + //Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < (syncer->batchSize() - 1)) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + QTest::qWait(100); + //this should process one batch of batchSize() items + QTRY_COMPARE(changedSpy.count(), syncer->batchSize()); + QCOMPARE(transactionSpy.count(), 1); //one per batch + + for (int i = syncer->batchSize(); i < origItems.count(); ++i) { + Item::List l; + //Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < origItems.count() - 1) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + QCOMPARE(transactionSpy.count(), 2); //one per batch + QTest::qWait(100); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + QTest::qWait(100); + QCOMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), resultItems.count()); + } + + void testGidMerge() + { + Collection col(collectionIdFromPath(QStringLiteral("res3"))); + { + Item item(QStringLiteral("application/octet-stream")); + item.setRemoteId(QStringLiteral("rid1")); + item.setGid(QStringLiteral("gid1")); + item.setPayload("payload1"); + ItemCreateJob *job = new ItemCreateJob(item, col); + AKVERIFYEXEC(job); + } + { + Item item(QStringLiteral("application/octet-stream")); + item.setRemoteId(QStringLiteral("rid2")); + item.setGid(QStringLiteral("gid2")); + item.setPayload("payload1"); + ItemCreateJob *job = new ItemCreateJob(item, col); + AKVERIFYEXEC(job); + } + Item modifiedItem(QStringLiteral("application/octet-stream")); + modifiedItem.setRemoteId(QStringLiteral("rid3")); + modifiedItem.setGid(QStringLiteral("gid2")); + modifiedItem.setPayload("payload2"); + + ItemSync *syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::MultipleTransactions); + syncer->setIncrementalSyncItems(Item::List() << modifiedItem, Item::List()); + AKVERIFYEXEC(syncer); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), 3); + + Item item; + item.setGid(QStringLiteral("gid2")); + ItemFetchJob *fetchJob = new ItemFetchJob(item); + fetchJob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().size(), 2); + QCOMPARE(fetchJob->items().first().payload(), QByteArray("payload2")); + QCOMPARE(fetchJob->items().first().remoteId(), QString::fromLatin1("rid3")); + QCOMPARE(fetchJob->items().at(1).payload(), QByteArray("payload1")); + QCOMPARE(fetchJob->items().at(1).remoteId(), QStringLiteral("rid2")); + } + + /* + * This test verifies that ItemSync doesn't prematurly emit it's result if a job inside a transaction fails. + * ItemSync is supposed to continue the sync but simply ignoring all delivered data. + */ + void testFailingJob() + { + const Collection col = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + ItemSync *syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, SIGNAL(transactionCommitted())); + QVERIFY(transactionSpy.isValid()); + QSignalSpy spy(syncer, SIGNAL(result(KJob*))); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + syncer->setTransactionMode(ItemSync::MultipleTransactions); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < syncer->batchSize(); ++i) { + Item::List l; + //Modify to trigger a changed signal + Item item = modifyItem(origItems[i]); + // item.setRemoteId(QByteArray("foo")); + item.setRemoteId(QString()); + item.setId(-1); + l << item; + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < (syncer->batchSize() - 1)) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + QTest::qWait(100); + QTRY_COMPARE(spy.count(), 0); + + for (int i = syncer->batchSize(); i < origItems.count(); ++i) { + Item::List l; + //Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < origItems.count() - 1) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + } + + /* + * This test verifies that ItemSync doesn't prematurly emit it's result if a job inside a transaction fails, due to a duplicate. + * This case used to break the TransactionSequence. + * ItemSync is supposed to continue the sync but simply ignoring all delivered data. + */ + void testFailingDueToDuplicateJob() + { + const Collection col = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + //Create a duplicate that will trigger an error during the first batch + Item duplicate = origItems.first(); + duplicate.setId(-1); + { + ItemCreateJob *job = new ItemCreateJob(duplicate, col); + AKVERIFYEXEC(job); + } + origItems = fetchItems(col); + + ItemSync *syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, SIGNAL(transactionCommitted())); + QVERIFY(transactionSpy.isValid()); + QSignalSpy spy(syncer, SIGNAL(result(KJob*))); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + syncer->setTransactionMode(ItemSync::MultipleTransactions); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < syncer->batchSize(); ++i) { + Item::List l; + //Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < (syncer->batchSize() - 1)) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + QTest::qWait(100); + //Ensure the job hasn't finished yet due to the errors + QTRY_COMPARE(spy.count(), 0); + + for (int i = syncer->batchSize(); i < origItems.count(); ++i) { + Item::List l; + //Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < origItems.count() - 1) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + } +}; + +QTEST_AKONADIMAIN(ItemsyncTest) + +#include "itemsynctest.moc" diff --git a/autotests/libs/itemtest.cpp b/autotests/libs/itemtest.cpp new file mode 100644 index 0000000..957e5ca --- /dev/null +++ b/autotests/libs/itemtest.cpp @@ -0,0 +1,125 @@ +/* + 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 "itemtest.h" +#include "testattribute.h" + +#include "item.h" + +#include +#include + +#include + +QTEST_MAIN(ItemTest) + +using namespace Akonadi; + +void ItemTest::testMultipart() +{ + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + + QSet parts; + QCOMPARE(item.loadedPayloadParts(), parts); + + QByteArray bodyData = "bodydata"; + item.setPayload(bodyData); + parts << Item::FullPayload; + QCOMPARE(item.loadedPayloadParts(), parts); + QCOMPARE(item.payload(), bodyData); + + QByteArray myData = "mypartdata"; + item.attribute(Item::AddIfMissing)->data = myData; + + QCOMPARE(item.loadedPayloadParts(), parts); + QCOMPARE(item.attributes().count(), 1); + QVERIFY(item.hasAttribute()); + QCOMPARE(item.attribute()->data, myData); +} + +void ItemTest::testInheritance() +{ + Item a; + + a.setRemoteId(QStringLiteral("Hello World")); + a.setSize(10); + + Item b(a); + b.setFlag("\\send"); + QCOMPARE(b.remoteId(), QStringLiteral("Hello World")); + QCOMPARE(b.size(), (qint64)10); +} + +void ItemTest::testParentCollection() +{ + Item a; + QVERIFY(!a.parentCollection().isValid()); + + a.setParentCollection(Collection::root()); + QCOMPARE(a.parentCollection(), Collection::root()); + Item b = a; + QCOMPARE(b.parentCollection(), Collection::root()); + + Item c; + c.parentCollection().setRemoteId(QStringLiteral("foo")); + QCOMPARE(c.parentCollection().remoteId(), QStringLiteral("foo")); + const Item d = c; + QCOMPARE(d.parentCollection().remoteId(), QStringLiteral("foo")); + + const Item e; + QVERIFY(!e.parentCollection().isValid()); + + Collection col(5); + Item f; + f.setParentCollection(col); + QCOMPARE(f.parentCollection(), col); + Item g = f; + QCOMPARE(g.parentCollection(), col); + b = g; + QCOMPARE(b.parentCollection(), col); +} + +void ItemTest::testComparision_data() +{ + QTest::addColumn("itemA"); + QTest::addColumn("itemB"); + QTest::addColumn("match"); + + QTest::newRow("both invalid, same invalid IDs") << Item(-10) << Item(-10) << true; + QTest::newRow("both invalid, different invalid IDs") << Item(-11) << Item(-12) << true; + QTest::newRow("one valid") << Item(1) << Item() << false; + QTest::newRow("both valid, same IDs") << Item(2) << Item(2) << true; + QTest::newRow("both valid, different IDs") << Item(3) << Item(4) << false; +} + +void ItemTest::testComparision() +{ + QFETCH(Akonadi::Item, itemA); + QFETCH(Akonadi::Item, itemB); + QFETCH(bool, match); + + if (match) { + QVERIFY(itemA == itemB); + QVERIFY(!(itemA != itemB)); + } else { + QVERIFY(itemA != itemB); + QVERIFY(!(itemA == itemB)); + } +} diff --git a/autotests/libs/itemtest.h b/autotests/libs/itemtest.h new file mode 100644 index 0000000..7af8d9e --- /dev/null +++ b/autotests/libs/itemtest.h @@ -0,0 +1,37 @@ +/* + 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_ITEMTEST_H +#define AKONADI_ITEMTEST_H + +#include + +class ItemTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testMultipart(); + void testInheritance(); + void testParentCollection(); + + void testComparision_data(); + void testComparision(); +}; + +#endif diff --git a/autotests/libs/jobtest.cpp b/autotests/libs/jobtest.cpp new file mode 100644 index 0000000..bf267ac --- /dev/null +++ b/autotests/libs/jobtest.cpp @@ -0,0 +1,185 @@ +/* + Copyright (c) 2012 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 "fakesession.h" +#include "job.h" + +Q_DECLARE_METATYPE(KJob *) +Q_DECLARE_METATYPE(Akonadi::Job *) + +using namespace Akonadi; + +class FakeJob : public Job +{ + Q_OBJECT +public: + explicit FakeJob(QObject *parent = 0) : Job(parent) {} + void done() + { + emitResult(); + } +protected: + void doStart() Q_DECL_OVERRIDE { emitWriteFinished(); } +}; + +class JobTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + qRegisterMetaType(); + qRegisterMetaType(); + } + + void testTopLevelJobExecution() + { + FakeSession session("fakeSession", FakeSession::EndJobsManually); + + QSignalSpy sessionQueueSpy(&session, SIGNAL(jobAdded(Akonadi::Job*))); + QVERIFY(sessionQueueSpy.isValid()); + + FakeJob *job1 = new FakeJob(&session); + QSignalSpy job1DoneSpy(job1, SIGNAL(result(KJob*))); + QVERIFY(job1DoneSpy.isValid()); + + FakeJob *job2 = new FakeJob(&session); + QSignalSpy job2DoneSpy(job2, SIGNAL(result(KJob*))); + QVERIFY(job2DoneSpy.isValid()); + + QCOMPARE(sessionQueueSpy.size(), 2); + QCOMPARE(job1DoneSpy.size(), 0); + + QVERIFY(AkonadiTest::akWaitForSignal(job1, SIGNAL(aboutToStart(Akonadi::Job*)), 1)); + QCOMPARE(job1DoneSpy.size(), 0); + + job1->done(); + QCOMPARE(job1DoneSpy.size(), 1); + + QVERIFY(AkonadiTest::akWaitForSignal(job2, SIGNAL(aboutToStart(Akonadi::Job*)), 1)); + QCOMPARE(job2DoneSpy.size(), 0); + job2->done(); + + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 1); + } + + void testKillSession() + { + FakeSession session("fakeSession", FakeSession::EndJobsManually); + + QSignalSpy sessionQueueSpy(&session, SIGNAL(jobAdded(Akonadi::Job*))); + QVERIFY(sessionQueueSpy.isValid()); + QSignalSpy sessionReconnectSpy(&session, SIGNAL(reconnected())); + QVERIFY(sessionReconnectSpy.isValid()); + + FakeJob *job1 = new FakeJob(&session); + QSignalSpy job1DoneSpy(job1, SIGNAL(result(KJob*))); + QVERIFY(job1DoneSpy.isValid()); + + FakeJob *job2 = new FakeJob(&session); + QSignalSpy job2DoneSpy(job2, SIGNAL(result(KJob*))); + QVERIFY(job2DoneSpy.isValid()); + + QCOMPARE(sessionQueueSpy.size(), 2); + QVERIFY(AkonadiTest::akWaitForSignal(job1, SIGNAL(aboutToStart(Akonadi::Job*)), 1)); + + // one job running, one queued, now kill the session + session.clear(); + QVERIFY(AkonadiTest::akWaitForSignal(&session, SIGNAL(reconnected()), 1)); + + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 1); + QCOMPARE(sessionReconnectSpy.size(), 1); // the first one is missed as it happens directly from the ctor + } + + void testKillQueuedJob() + { + FakeSession session("fakeSession", FakeSession::EndJobsManually); + + QSignalSpy sessionQueueSpy(&session, SIGNAL(jobAdded(Akonadi::Job*))); + QVERIFY(sessionQueueSpy.isValid()); + QSignalSpy sessionReconnectSpy(&session, SIGNAL(reconnected())); + QVERIFY(sessionReconnectSpy.isValid()); + + FakeJob *job1 = new FakeJob(&session); + QSignalSpy job1DoneSpy(job1, SIGNAL(result(KJob*))); + QVERIFY(job1DoneSpy.isValid()); + + FakeJob *job2 = new FakeJob(&session); + QSignalSpy job2DoneSpy(job2, SIGNAL(result(KJob*))); + QVERIFY(job2DoneSpy.isValid()); + + QCOMPARE(sessionQueueSpy.size(), 2); + QVERIFY(AkonadiTest::akWaitForSignal(job1, SIGNAL(aboutToStart(Akonadi::Job*)), 1)); + + // one job running, one queued, now kill the waiting job + QVERIFY(job2->kill(KJob::EmitResult)); + + QCOMPARE(job1DoneSpy.size(), 0); + QCOMPARE(job2DoneSpy.size(), 1); + + job1->done(); + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 1); + QCOMPARE(sessionReconnectSpy.size(), 0); // the first one is missed as it happens directly from the ctor + } + + void testKillRunningJob() + { + FakeSession session("fakeSession", FakeSession::EndJobsManually); + + QSignalSpy sessionQueueSpy(&session, SIGNAL(jobAdded(Akonadi::Job*))); + QVERIFY(sessionQueueSpy.isValid()); + QSignalSpy sessionReconnectSpy(&session, SIGNAL(reconnected())); + QVERIFY(sessionReconnectSpy.isValid()); + + FakeJob *job1 = new FakeJob(&session); + QSignalSpy job1DoneSpy(job1, SIGNAL(result(KJob*))); + QVERIFY(job1DoneSpy.isValid()); + + FakeJob *job2 = new FakeJob(&session); + QSignalSpy job2DoneSpy(job2, SIGNAL(result(KJob*))); + QVERIFY(job2DoneSpy.isValid()); + + QCOMPARE(sessionQueueSpy.size(), 2); + QVERIFY(AkonadiTest::akWaitForSignal(job1, SIGNAL(aboutToStart(Akonadi::Job*)), 1)); + + // one job running, one queued, now kill the running one + QVERIFY(job1->kill(KJob::EmitResult)); + + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 0); + + // session needs to reconnect, then execute the next job + QVERIFY(AkonadiTest::akWaitForSignal(job2, SIGNAL(aboutToStart(Akonadi::Job*)), 1)); + QCOMPARE(sessionReconnectSpy.size(), 1); + job2->done(); + + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 1); + QCOMPARE(sessionReconnectSpy.size(), 1); // the first one is missed as it happens directly from the ctor + } +}; + +QTEST_AKONADIMAIN(JobTest) + +#include "jobtest.moc" diff --git a/autotests/libs/lazypopulationtest.cpp b/autotests/libs/lazypopulationtest.cpp new file mode 100644 index 0000000..f00dee9 --- /dev/null +++ b/autotests/libs/lazypopulationtest.cpp @@ -0,0 +1,367 @@ +/* + Copyright (c) 2013 Christian Mollekopf + + 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 "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +class InspectableETM: public EntityTreeModel +{ +public: + explicit InspectableETM(ChangeRecorder *monitor, QObject *parent = Q_NULLPTR) + : EntityTreeModel(monitor, parent) {} + EntityTreeModelPrivate *etmPrivate() + { + return d_ptr; + } +}; + +/** + * This is a test for the LazyPopulation of the ETM and the associated refcounting in the Monitor. + */ +class LazyPopulationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + /** + * Test a complete scenario that checks: + * * loading on referencing + * * buffering after referencing + * * purging after the collection leaves the buffer + * * not-fetching when a collection is not buffered and not referenced + * * reloading after a collection becomes referenced again + */ + void testItemAdded(); + /* + * Test what happens if we + * * Create an item + * * Reference before item added signal arrives + * * Try fetching rest of items + */ + void testItemAddedBeforeFetch(); + + /* + * We purge an empty collection and make sure it can be fetched later on. + * * reference collection to remember empty status + * * purge collection + * * add item (it should not be added since not monitored) + * * reference collection and make sure items are added + */ + void testPurgeEmptyCollection(); + +private: + Collection res3; + static const int numberOfRootCollections = 4; + static const int bufferSize; +}; + +const int LazyPopulationTest::bufferSize = MonitorPrivate::PurgeBuffer::buffersize(); + +void LazyPopulationTest::initTestCase() +{ + qRegisterMetaType("Akonadi::Collection::Id"); + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + + res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + + //Set up a bunch of collections that we can select to purge a collection from the buffer + + //Number of buffered collections in the monitor + const int bufferSize = MonitorPrivate::PurgeBuffer::buffersize(); + for (int i = 0; i < bufferSize; i++) { + Collection col; + col.setParentCollection(res3); + col.setName(QStringLiteral("col%1").arg(i)); + CollectionCreateJob *create = new CollectionCreateJob(col, this); + AKVERIFYEXEC(create); + } +} + +QModelIndex getIndex(const QString &string, EntityTreeModel *model) +{ + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, string, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return QModelIndex(); + } + return list.first(); +} + +/** + * Since we have no sensible way to figure out if the model is fully populated, + * we use the brute force approach. + */ +bool waitForPopulation(const QModelIndex &idx, EntityTreeModel *model, int count) +{ + for (int i = 0; i < 500; i++) { + if (model->rowCount(idx) >= count) { + return true; + } + QTest::qWait(10); + } + return false; +} + +void referenceCollection(EntityTreeModel *model, int index) +{ + QModelIndex idx = getIndex(QStringLiteral("col%1").arg(index), model); + QVERIFY(idx.isValid()); + model->setData(idx, QVariant(), EntityTreeModel::CollectionRefRole); + model->setData(idx, QVariant(), EntityTreeModel::CollectionDerefRole); +} + +void referenceCollections(EntityTreeModel *model, int count) +{ + for (int i = 0; i < count; i++) { + referenceCollection(model, i); + } +} + +void LazyPopulationTest::testItemAdded() +{ + const int bufferSize = MonitorPrivate::PurgeBuffer::buffersize(); + const QString mainCollectionName(QStringLiteral("main")); + Collection monitorCol; + { + monitorCol.setParentCollection(res3); + monitorCol.setName(mainCollectionName); + CollectionCreateJob *create = new CollectionCreateJob(monitorCol, this); + AKVERIFYEXEC(create); + monitorCol = create->collection(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, monitorCol, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(Collection::root()); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation); + + //Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(waitForPopulation(res3Index, model, bufferSize + 1)); + + QModelIndex monitorIndex = getIndex(mainCollectionName, model); + QVERIFY(monitorIndex.isValid()); + + //Start + + //---Check that the item is present after the collection was referenced + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionRefRole); + model->fetchMore(monitorIndex); + //Wait for collection to be fetched + QVERIFY(waitForPopulation(monitorIndex, model, 1)); + + //---ensure we cannot fetchMore again + QVERIFY(!model->etmPrivate()->canFetchMore(monitorIndex)); + + //The item should now be present + QCOMPARE(model->index(0, 0, monitorIndex).data(Akonadi::EntityTreeModel::ItemIdRole).value(), item1.id()); + + //---ensure item1 is still available after no longer being referenced due to buffering + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionDerefRole); + QCOMPARE(model->index(0, 0, monitorIndex).data(Akonadi::EntityTreeModel::ItemIdRole).value(), item1.id()); + + //---ensure item1 gets purged after the collection is no longer buffered + //Get the monitorCol out of the buffer + referenceCollections(model, bufferSize - 1); + //The collection is still in the buffer... + QCOMPARE(model->rowCount(monitorIndex), 1); + referenceCollection(model, bufferSize - 1); + //...and now purged + QCOMPARE(model->rowCount(monitorIndex), 0); + QVERIFY(model->etmPrivate()->canFetchMore(monitorIndex)); + + //---ensure item2 added to unbuffered and unreferenced collection is not added to the model + Item item2; + { + item2.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item2, monitorCol, this); + AKVERIFYEXEC(append); + item2 = append->item(); + } + QCOMPARE(model->rowCount(monitorIndex), 0); + + //---ensure all items are loaded after re-referencing the collection + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionRefRole); + model->fetchMore(monitorIndex); + //Wait for collection to be fetched + QVERIFY(waitForPopulation(monitorIndex, model, 2)); + QCOMPARE(model->rowCount(monitorIndex), 2); + + QVERIFY(!model->etmPrivate()->canFetchMore(monitorIndex)); + + //purge collection again + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionDerefRole); + referenceCollections(model, bufferSize); + QCOMPARE(model->rowCount(monitorIndex), 0); + //fetch when not monitored + QVERIFY(model->etmPrivate()->canFetchMore(monitorIndex)); + model->fetchMore(monitorIndex); + QVERIFY(waitForPopulation(monitorIndex, model, 2)); + //ensure we cannot refetch + QVERIFY(!model->etmPrivate()->canFetchMore(monitorIndex)); +} + +void LazyPopulationTest::testItemAddedBeforeFetch() +{ + const QString mainCollectionName(QStringLiteral("main2")); + Collection monitorCol; + { + monitorCol.setParentCollection(res3); + monitorCol.setName(mainCollectionName); + CollectionCreateJob *create = new CollectionCreateJob(monitorCol, this); + AKVERIFYEXEC(create); + monitorCol = create->collection(); + } + + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(Collection::root()); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation); + + //Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(waitForPopulation(res3Index, model, bufferSize + 1)); + + QModelIndex monitorIndex = getIndex(mainCollectionName, model); + QVERIFY(monitorIndex.isValid()); + + //Create a first item before referencing, it should not show up in the ETM + { + Item item1; + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, monitorCol, this); + AKVERIFYEXEC(append); + } + + //Before referenced or fetchMore is called, the collection should be empty + QTest::qWait(500); + QCOMPARE(model->rowCount(monitorIndex), 0); + + //Reference the collection + QVERIFY(!model->etmPrivate()->isMonitored(monitorCol.id())); + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionRefRole); + QVERIFY(model->etmPrivate()->isMonitored(monitorCol.id())); + + //Create another item, it should not be added to the ETM although the signal is emitted from the monitor, but we should be able to fetchMore + { + QSignalSpy addedSpy(changeRecorder, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QVERIFY(addedSpy.isValid()); + Item item2; + item2.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item2, monitorCol, this); + AKVERIFYEXEC(append); + QTRY_VERIFY(addedSpy.count() >= 1); + } + + QVERIFY(model->etmPrivate()->canFetchMore(monitorIndex)); + + model->fetchMore(monitorIndex); + //Wait for collection to be fetched + QVERIFY(waitForPopulation(monitorIndex, model, 2)); +} + +void LazyPopulationTest::testPurgeEmptyCollection() +{ + const QString mainCollectionName(QStringLiteral("main3")); + Collection monitorCol; + { + monitorCol.setParentCollection(res3); + monitorCol.setName(mainCollectionName); + CollectionCreateJob *create = new CollectionCreateJob(monitorCol, this); + AKVERIFYEXEC(create); + monitorCol = create->collection(); + } + //Monitor without referencing so we get all signals + Monitor *monitor = new Monitor(this); + monitor->setCollectionMonitored(Collection::root()); + + ChangeRecorder *changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(Collection::root()); + InspectableETM *model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation); + + //Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(waitForPopulation(res3Index, model, bufferSize + 1)); + + QModelIndex monitorIndex = getIndex(mainCollectionName, model); + QVERIFY(monitorIndex.isValid()); + + //fetch the collection so we remember the empty status + QSignalSpy populatedSpy(model, SIGNAL(collectionPopulated(Akonadi::Collection::Id))); + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionRefRole); + model->fetchMore(monitorIndex); + //Wait for collection to be fetched + QTRY_VERIFY(populatedSpy.count() >= 1); + + //get the collection purged + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionDerefRole); + referenceCollections(model, bufferSize); + + //create an item + { + QSignalSpy addedSpy(monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QVERIFY(addedSpy.isValid()); + Item item1; + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, monitorCol, this); + AKVERIFYEXEC(append); + QTRY_VERIFY(addedSpy.count() >= 1); + } + + //ensure it's not in the model + //fetch the collection + QVERIFY(model->etmPrivate()->canFetchMore(monitorIndex)); + + model->fetchMore(monitorIndex); + //Wait for collection to be fetched + QVERIFY(waitForPopulation(monitorIndex, model, 1)); +} + +#include "lazypopulationtest.moc" + +QTEST_AKONADIMAIN(LazyPopulationTest) diff --git a/autotests/libs/linktest.cpp b/autotests/libs/linktest.cpp new file mode 100644 index 0000000..138ea7a --- /dev/null +++ b/autotests/libs/linktest.cpp @@ -0,0 +1,113 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace Akonadi; + +class LinkTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + } + + void testLink() + { + SearchCreateJob *create = new SearchCreateJob(QStringLiteral("linkTestFolder"), SearchQuery(), this); + AKVERIFYEXEC(create); + + CollectionFetchJob *list = new CollectionFetchJob(Collection(1), CollectionFetchJob::Recursive, this); + AKVERIFYEXEC(list); + Collection col; + foreach (const Collection &c, list->collections()) { + if (c.name() == QStringLiteral("linkTestFolder")) { + col = c; + } + } + QVERIFY(col.isValid()); + + Item::List items; + items << Item(3) << Item(4) << Item(6); + + Monitor *monitor = new Monitor(this); + monitor->setCollectionMonitored(col); + monitor->itemFetchScope().fetchFullPayload(); + + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy lspy(monitor, SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection))); + QSignalSpy uspy(monitor, SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection))); + QVERIFY(lspy.isValid()); + QVERIFY(uspy.isValid()); + + LinkJob *link = new LinkJob(col, items, this); + AKVERIFYEXEC(link); + + QTRY_COMPARE(lspy.count(), 3); + QTest::qWait(100); + QVERIFY(uspy.isEmpty()); + + QList arg = lspy.takeFirst(); + Item item = arg.at(0).value(); + QCOMPARE(item.mimeType(), QString::fromLatin1("application/octet-stream")); + QVERIFY(item.hasPayload()); + + lspy.clear(); + + ItemFetchJob *fetch = new ItemFetchJob(col); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 3); + foreach (const Item &item, fetch->items()) { + QVERIFY(items.contains(item)); + } + + UnlinkJob *unlink = new UnlinkJob(col, items, this); + AKVERIFYEXEC(unlink); + + QTRY_COMPARE(uspy.count(), 3); + QTest::qWait(100); + QVERIFY(lspy.isEmpty()); + + fetch = new ItemFetchJob(col); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 0); + } + +}; + +QTEST_AKONADIMAIN(LinkTest) + +#include "linktest.moc" diff --git a/autotests/libs/mimetypecheckertest.cpp b/autotests/libs/mimetypecheckertest.cpp new file mode 100644 index 0000000..55c65d2 --- /dev/null +++ b/autotests/libs/mimetypecheckertest.cpp @@ -0,0 +1,306 @@ +/* + Copyright (c) 2009 Kevin Krammer + + 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 "mimetypecheckertest.h" +#include "testattribute.h" + +#include "collection.h" +#include "item.h" + +#include "krandom.h" + +#include +#include + + +#include + +QTEST_MAIN(MimeTypeCheckerTest) + +using namespace Akonadi; + +MimeTypeCheckerTest::MimeTypeCheckerTest(QObject *parent) + : QObject(parent) +{ + mCalendarSubTypes << QStringLiteral("application/x-vnd.akonadi.calendar.event") + << QStringLiteral("application/x-vnd.akonadi.calendar.todo"); +} + +void MimeTypeCheckerTest::initTestCase() +{ + QVERIFY(QMimeDatabase().mimeTypeForName(QLatin1String("application/x-vnd.akonadi.calendar.event")).isValid()); + + MimeTypeChecker emptyChecker; + MimeTypeChecker calendarChecker; + MimeTypeChecker subTypeChecker; + MimeTypeChecker aliasChecker; + + // for testing reset through assignments + const QLatin1String textPlain = QLatin1String("text/plain"); + mEmptyChecker.addWantedMimeType(textPlain); + QVERIFY(!mEmptyChecker.wantedMimeTypes().isEmpty()); + + const QLatin1String textCalendar = QLatin1String("text/calendar"); + calendarChecker.addWantedMimeType(textCalendar); + QCOMPARE(calendarChecker.wantedMimeTypes().count(), 1); + + subTypeChecker.setWantedMimeTypes(mCalendarSubTypes); + QCOMPARE(subTypeChecker.wantedMimeTypes().count(), 2); + + const QLatin1String textVCard = QLatin1String("text/directory"); + aliasChecker.addWantedMimeType(textVCard); + QCOMPARE(aliasChecker.wantedMimeTypes().count(), 1); + + // test assignment works correctly + mEmptyChecker = emptyChecker; + mCalendarChecker = calendarChecker; + mSubTypeChecker = subTypeChecker; + mAliasChecker = aliasChecker; + + QVERIFY(mEmptyChecker.wantedMimeTypes().isEmpty()); + + QCOMPARE(mCalendarChecker.wantedMimeTypes().count(), 1); + QCOMPARE(mCalendarChecker.wantedMimeTypes(), QStringList() << textCalendar); + + QCOMPARE(mSubTypeChecker.wantedMimeTypes().count(), 2); + const QSet calendarSubTypes = QSet::fromList(mCalendarSubTypes); + const QSet wantedSubTypes = QSet::fromList(mSubTypeChecker.wantedMimeTypes()); + QCOMPARE(wantedSubTypes, calendarSubTypes); + + QCOMPARE(mAliasChecker.wantedMimeTypes().count(), 1); + QCOMPARE(mAliasChecker.wantedMimeTypes(), QStringList() << textVCard); +} + +void MimeTypeCheckerTest::testCollectionCheck() +{ + Collection invalidCollection; + Collection emptyCollection(1); + Collection calendarCollection(2); + Collection eventCollection(3); + Collection journalCollection(4); + Collection vcardCollection(5); + Collection aliasCollection(6); + + const QLatin1String textCalendar = QLatin1String("text/calendar"); + calendarCollection.setContentMimeTypes(QStringList() << textCalendar); + const QLatin1String akonadiEvent = QLatin1String("application/x-vnd.akonadi.calendar.event"); + eventCollection.setContentMimeTypes(QStringList() << akonadiEvent); + journalCollection.setContentMimeTypes(QStringList() << QStringLiteral("application/x-vnd.akonadi.calendar.journal")); + const QLatin1String textDirectory = QLatin1String("text/directory"); + vcardCollection.setContentMimeTypes(QStringList() << textDirectory); + aliasCollection.setContentMimeTypes(QStringList() << QStringLiteral("text/x-vcard")); + + Collection::List voidCollections; + voidCollections << invalidCollection << emptyCollection; + + Collection::List subTypeCollections; + subTypeCollections << eventCollection << journalCollection; + + Collection::List calendarCollections = subTypeCollections; + calendarCollections << calendarCollection; + + Collection::List contactCollections; + contactCollections << vcardCollection << aliasCollection; + + //// empty checker fails for all + Collection::List collections = voidCollections + calendarCollections + contactCollections; + foreach (const Collection &collection, collections) { + QVERIFY(!mEmptyChecker.isWantedCollection(collection)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection, QString())); + } + + //// calendar checker fails for void and contact collections + collections = voidCollections + contactCollections; + foreach (const Collection &collection, collections) { + QVERIFY(!mCalendarChecker.isWantedCollection(collection)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection, textCalendar)); + } + + // but accepts all calendar collections + collections = calendarCollections; + foreach (const Collection &collection, collections) { + QVERIFY(mCalendarChecker.isWantedCollection(collection)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection, textCalendar)); + } + + //// sub type checker fails for all but the event collection + collections = voidCollections + calendarCollections + contactCollections; + collections.removeAll(eventCollection); + foreach (const Collection &collection, collections) { + QVERIFY(!mSubTypeChecker.isWantedCollection(collection)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection, akonadiEvent)); + } + + // but accepts the event collection + collections = Collection::List() << eventCollection; + foreach (const Collection &collection, collections) { + QVERIFY(mSubTypeChecker.isWantedCollection(collection)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection, akonadiEvent)); + } + + //// alias checker fails for void and calendar collections + collections = voidCollections + calendarCollections; + foreach (const Collection &collection, collections) { + QVERIFY(!mAliasChecker.isWantedCollection(collection)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection, textDirectory)); + } + + // but accepts all contact collections + collections = contactCollections; + foreach (const Collection &collection, collections) { + QVERIFY(mAliasChecker.isWantedCollection(collection)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection, textDirectory)); + } +} + +void MimeTypeCheckerTest::testItemCheck() +{ + Item invalidItem; + Item emptyItem(1); + Item calendarItem(2); + Item eventItem(3); + Item journalItem(4); + Item vcardItem(5); + Item aliasItem(6); + + const QLatin1String textCalendar = QLatin1String("text/calendar"); + calendarItem.setMimeType(textCalendar); + const QLatin1String akonadiEvent = QLatin1String("application/x-vnd.akonadi.calendar.event"); + eventItem.setMimeType(akonadiEvent); + journalItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.journal")); + const QLatin1String textDirectory = QLatin1String("text/directory"); + vcardItem.setMimeType(textDirectory); + aliasItem.setMimeType(QStringLiteral("text/x-vcard")); + + Item::List voidItems; + voidItems << invalidItem << emptyItem; + + Item::List subTypeItems; + subTypeItems << eventItem << journalItem; + + Item::List calendarItems = subTypeItems; + calendarItems << calendarItem; + + Item::List contactItems; + contactItems << vcardItem << aliasItem; + + //// empty checker fails for all + Item::List items = voidItems + calendarItems + contactItems; + foreach (const Item &item, items) { + QVERIFY(!mEmptyChecker.isWantedItem(item)); + QVERIFY(!MimeTypeChecker::isWantedItem(item, QString())); + } + + //// calendar checker fails for void and contact items + items = voidItems + contactItems; + foreach (const Item &item, items) { + QVERIFY(!mCalendarChecker.isWantedItem(item)); + QVERIFY(!MimeTypeChecker::isWantedItem(item, textCalendar)); + } + + // but accepts all calendar items + items = calendarItems; + foreach (const Item &item, items) { + QVERIFY(mCalendarChecker.isWantedItem(item)); + QVERIFY(MimeTypeChecker::isWantedItem(item, textCalendar)); + } + + //// sub type checker fails for all but the event item + items = voidItems + calendarItems + contactItems; + items.removeAll(eventItem); + foreach (const Item &item, items) { + QVERIFY(!mSubTypeChecker.isWantedItem(item)); + QVERIFY(!MimeTypeChecker::isWantedItem(item, akonadiEvent)); + } + + // but accepts the event item + items = Item::List() << eventItem; + foreach (const Item &item, items) { + QVERIFY(mSubTypeChecker.isWantedItem(item)); + QVERIFY(MimeTypeChecker::isWantedItem(item, akonadiEvent)); + } + + //// alias checker fails for void and calendar items + items = voidItems + calendarItems; + foreach (const Item &item, items) { + QVERIFY(!mAliasChecker.isWantedItem(item)); + QVERIFY(!MimeTypeChecker::isWantedItem(item, textDirectory)); + } + + // but accepts all contact items + items = contactItems; + foreach (const Item &item, items) { + QVERIFY(mAliasChecker.isWantedItem(item)); + QVERIFY(MimeTypeChecker::isWantedItem(item, textDirectory)); + } +} + +void MimeTypeCheckerTest::testStringMatchEquivalent() +{ + // check that a random and thus not installed MIME type + // can still be checked just like with direct string comparison + + const QLatin1String installedMimeType("text/plain"); + const QString randomMimeType = QLatin1String("application/x-vnd.test.") + + KRandom::randomString(10); + + MimeTypeChecker installedTypeChecker; + installedTypeChecker.addWantedMimeType(installedMimeType); + + MimeTypeChecker randomTypeChecker; + randomTypeChecker.addWantedMimeType(randomMimeType); + + Item item1(1); + item1.setMimeType(installedMimeType); + Item item2(2); + item2.setMimeType(randomMimeType); + + Collection collection1(1); + collection1.setContentMimeTypes(QStringList() << installedMimeType); + Collection collection2(2); + collection2.setContentMimeTypes(QStringList() << randomMimeType); + Collection collection3(3); + collection3.setContentMimeTypes(QStringList() << installedMimeType << randomMimeType); + + QVERIFY(installedTypeChecker.isWantedItem(item1)); + QVERIFY(!randomTypeChecker.isWantedItem(item1)); + QVERIFY(MimeTypeChecker::isWantedItem(item1, installedMimeType)); + QVERIFY(!MimeTypeChecker::isWantedItem(item1, randomMimeType)); + + QVERIFY(!installedTypeChecker.isWantedItem(item2)); + QVERIFY(randomTypeChecker.isWantedItem(item2)); + QVERIFY(!MimeTypeChecker::isWantedItem(item2, installedMimeType)); + QVERIFY(MimeTypeChecker::isWantedItem(item2, randomMimeType)); + + QVERIFY(installedTypeChecker.isWantedCollection(collection1)); + QVERIFY(!randomTypeChecker.isWantedCollection(collection1)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection1, installedMimeType)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection1, randomMimeType)); + + QVERIFY(!installedTypeChecker.isWantedCollection(collection2)); + QVERIFY(randomTypeChecker.isWantedCollection(collection2)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection2, installedMimeType)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection2, randomMimeType)); + + QVERIFY(installedTypeChecker.isWantedCollection(collection3)); + QVERIFY(randomTypeChecker.isWantedCollection(collection3)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection3, installedMimeType)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection3, randomMimeType)); +} + diff --git a/autotests/libs/mimetypecheckertest.h b/autotests/libs/mimetypecheckertest.h new file mode 100644 index 0000000..063813f --- /dev/null +++ b/autotests/libs/mimetypecheckertest.h @@ -0,0 +1,49 @@ +/* + Copyright (c) 2009 Kevin Krammer + + 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_MIMETYPECHECKERTEST_H +#define AKONADI_MIMETYPECHECKERTEST_H + +#include "mimetypechecker.h" + +#include +#include + +class MimeTypeCheckerTest : public QObject +{ + Q_OBJECT +public: + explicit MimeTypeCheckerTest(QObject *parent = Q_NULLPTR); + +private: + QStringList mCalendarSubTypes; + + Akonadi::MimeTypeChecker mEmptyChecker; + Akonadi::MimeTypeChecker mCalendarChecker; + Akonadi::MimeTypeChecker mSubTypeChecker; + Akonadi::MimeTypeChecker mAliasChecker; + +private Q_SLOTS: + void initTestCase(); + void testCollectionCheck(); + void testItemCheck(); + void testStringMatchEquivalent(); +}; + +#endif diff --git a/autotests/libs/modelspy.cpp b/autotests/libs/modelspy.cpp new file mode 100644 index 0000000..71ad003 --- /dev/null +++ b/autotests/libs/modelspy.cpp @@ -0,0 +1,226 @@ +/* + 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 "modelspy.h" + +#include +#include + +ModelSpy::ModelSpy(QObject *parent) + : QObject(parent), QList(), m_isSpying(false) +{ + qRegisterMetaType("QModelIndex"); +} + +bool ModelSpy::isEmpty() const +{ + return QList::isEmpty() && m_expectedSignals.isEmpty(); +} + +void ModelSpy::setModel(QAbstractItemModel *model) +{ + Q_ASSERT(model); + m_model = model; +} + +void ModelSpy::setExpectedSignals(const QList< ExpectedSignal > &expectedSignals) +{ + m_expectedSignals = expectedSignals; +} + +QList ModelSpy::expectedSignals() const +{ + return m_expectedSignals; +} + +void ModelSpy::verifySignal(SignalType type, const QModelIndex &parent, int start, int end) +{ + ExpectedSignal expectedSignal = m_expectedSignals.takeFirst(); + QCOMPARE(int(type), int(expectedSignal.signalType)); + QCOMPARE(parent.data(), expectedSignal.parentData); + QCOMPARE(start, expectedSignal.startRow); + QCOMPARE(end, expectedSignal.endRow); + if (!expectedSignal.newData.isEmpty()) { + // TODO + } +} + +void ModelSpy::verifySignal(SignalType type, const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destStart) +{ + ExpectedSignal expectedSignal = m_expectedSignals.takeFirst(); + QCOMPARE(int(type), int(expectedSignal.signalType)); + QCOMPARE(start, expectedSignal.startRow); + QCOMPARE(end, expectedSignal.endRow); + QCOMPARE(parent.data(), expectedSignal.sourceParentData); + QCOMPARE(destParent.data(), expectedSignal.parentData); + QCOMPARE(destStart, expectedSignal.destRow); + QVariantList moveList; + QModelIndex _parent = type == RowsAboutToBeMoved ? parent : destParent; + int _start = type == RowsAboutToBeMoved ? start : destStart; + int _end = type == RowsAboutToBeMoved ? end : destStart + (end - start); + for (int row = _start; row <= _end; ++row) { + moveList << m_model->index(row, 0, _parent).data(); + } + QCOMPARE(moveList, expectedSignal.newData); +} + +void ModelSpy::verifySignal(SignalType type, const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + QCOMPARE(type, DataChanged); + ExpectedSignal expectedSignal = m_expectedSignals.takeFirst(); + QCOMPARE(int(type), int(expectedSignal.signalType)); + QModelIndex parent = topLeft.parent(); + //This check won't work for toplevel indexes + if (parent.isValid()) { + if (expectedSignal.parentData.isValid()) { + QCOMPARE(parent.data(), expectedSignal.parentData); + } + } + QCOMPARE(topLeft.row(), expectedSignal.startRow); + QCOMPARE(bottomRight.row(), expectedSignal.endRow); + for (int i = 0, row = topLeft.row(); row <= bottomRight.row(); ++row, ++i) { + QCOMPARE(expectedSignal.newData.at(i), m_model->index(row, 0, parent).data()); + } +} + +void ModelSpy::startSpying() +{ + m_isSpying = true; + + // If a signal is connected to a slot multiple times, the slot gets called multiple times. + // As we're doing start and stop spying all the time, we disconnect here first to make sure. + + disconnect(m_model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int))); + disconnect(m_model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(rowsInserted(QModelIndex,int,int))); + disconnect(m_model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int))); + disconnect(m_model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(rowsRemoved(QModelIndex,int,int))); + disconnect(m_model, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)), + this, SLOT(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int))); + disconnect(m_model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), + this, SLOT(rowsMoved(QModelIndex,int,int,QModelIndex,int))); + + disconnect(m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(dataChanged(QModelIndex,QModelIndex))); + + connect(m_model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + SLOT(rowsAboutToBeInserted(QModelIndex,int,int))); + connect(m_model, SIGNAL(rowsInserted(QModelIndex,int,int)), + SLOT(rowsInserted(QModelIndex,int,int))); + connect(m_model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + SLOT(rowsAboutToBeRemoved(QModelIndex,int,int))); + connect(m_model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + SLOT(rowsRemoved(QModelIndex,int,int))); + connect(m_model, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)), + SLOT(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int))); + connect(m_model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), + SLOT(rowsMoved(QModelIndex,int,int,QModelIndex,int))); + + connect(m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + SLOT(dataChanged(QModelIndex,QModelIndex))); + +} + +void ModelSpy::stopSpying() +{ + m_isSpying = false; + disconnect(m_model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int))); + disconnect(m_model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(rowsInserted(QModelIndex,int,int))); + disconnect(m_model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int))); + disconnect(m_model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(rowsRemoved(QModelIndex,int,int))); + disconnect(m_model, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)), + this, SLOT(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int))); + disconnect(m_model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), + this, SLOT(rowsMoved(QModelIndex,int,int,QModelIndex,int))); + + disconnect(m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(dataChanged(QModelIndex,QModelIndex))); + +} + +void ModelSpy::rowsAboutToBeInserted(const QModelIndex &parent, int start, int end) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsAboutToBeInserted, parent, start, end); + } else { + append(QVariantList() << RowsAboutToBeInserted << QVariant::fromValue(parent) << start << end); + } +} + +void ModelSpy::rowsInserted(const QModelIndex &parent, int start, int end) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsInserted, parent, start, end); + } else { + append(QVariantList() << RowsInserted << QVariant::fromValue(parent) << start << end); + } +} + +void ModelSpy::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsAboutToBeRemoved, parent, start, end); + } else { + append(QVariantList() << RowsAboutToBeRemoved << QVariant::fromValue(parent) << start << end); + } +} + +void ModelSpy::rowsRemoved(const QModelIndex &parent, int start, int end) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsRemoved, parent, start, end); + } else { + append(QVariantList() << RowsRemoved << QVariant::fromValue(parent) << start << end); + } +} + +void ModelSpy::rowsAboutToBeMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsAboutToBeMoved, srcParent, start, end, destParent, destStart); + } else { + append(QVariantList() << RowsAboutToBeMoved << QVariant::fromValue(srcParent) << start << end << QVariant::fromValue(destParent) << destStart); + } +} + +void ModelSpy::rowsMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsMoved, srcParent, start, end, destParent, destStart); + } else { + append(QVariantList() << RowsMoved << QVariant::fromValue(srcParent) << start << end << QVariant::fromValue(destParent) << destStart); + } +} + +void ModelSpy::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(DataChanged, topLeft, bottomRight); + } else { + append(QVariantList() << DataChanged << QVariant::fromValue(topLeft) << QVariant::fromValue(bottomRight)); + } +} + diff --git a/autotests/libs/modelspy.h b/autotests/libs/modelspy.h new file mode 100644 index 0000000..d0a76bb --- /dev/null +++ b/autotests/libs/modelspy.h @@ -0,0 +1,91 @@ +/* + 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 MODELSPY_H +#define MODELSPY_H + +#include +#include +#include +#include "akonaditestfake_export.h" + +enum SignalType { + NoSignal, + RowsAboutToBeInserted, + RowsInserted, + RowsAboutToBeRemoved, + RowsRemoved, + RowsAboutToBeMoved, + RowsMoved, + DataChanged +}; + +struct ExpectedSignal { + SignalType signalType; + int startRow; + int endRow; + QVariant parentData; + QVariant sourceParentData; + int destRow; + QVariantList newData; +}; + +Q_DECLARE_METATYPE(QModelIndex) + +class AKONADITESTFAKE_EXPORT ModelSpy : public QObject, public QList +{ + Q_OBJECT +public: + ModelSpy(QObject *parent); + + void setModel(QAbstractItemModel *model); + + bool isEmpty() const; + + void setExpectedSignals(const QList &expectedSignals); + QList expectedSignals() const; + + void verifySignal(SignalType type, const QModelIndex &parent, int start, int end); + void verifySignal(SignalType type, const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destStart); + void verifySignal(SignalType type, const QModelIndex &topLeft, const QModelIndex &bottomRight); + + void startSpying(); + void stopSpying(); + bool isSpying() + { + return m_isSpying; + } + +protected Q_SLOTS: + void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end); + void rowsInserted(const QModelIndex &parent, int start, int end); + void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + void rowsRemoved(const QModelIndex &parent, int start, int end); + void rowsAboutToBeMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart); + void rowsMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart); + + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + +private: + QAbstractItemModel *m_model; + bool m_isSpying; + QList m_expectedSignals; +}; + +#endif diff --git a/autotests/libs/monitorfiltertest.cpp b/autotests/libs/monitorfiltertest.cpp new file mode 100644 index 0000000..6dbab20 --- /dev/null +++ b/autotests/libs/monitorfiltertest.cpp @@ -0,0 +1,340 @@ +/* + Copyright (c) 2010 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 "monitor_p.h" +#include +#include + +using namespace Akonadi; + +Q_DECLARE_METATYPE(Akonadi::Protocol::ChangeNotification::Operation) +Q_DECLARE_METATYPE(QSet) + +class MonitorFilterTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType >(); + qRegisterMetaType(); + } + + void filterConnected_data() + { + QTest::addColumn("op"); + QTest::addColumn("signalName"); + + QTest::newRow("itemAdded") << Protocol::ChangeNotification::Add << QByteArray(SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QTest::newRow("itemChanged") << Protocol::ChangeNotification::Modify << QByteArray(SIGNAL(itemChanged(Akonadi::Item,QSet))); + QTest::newRow("itemsFlagsChanged") << Protocol::ChangeNotification::ModifyFlags << QByteArray(SIGNAL(itemsFlagsChanged(Akonadi::Item::List,QSet,QSet))); + QTest::newRow("itemRemoved") << Protocol::ChangeNotification::Remove << QByteArray(SIGNAL(itemRemoved(Akonadi::Item))); + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move << QByteArray(SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("itemLinked") << Protocol::ChangeNotification::Link << QByteArray(SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection))); + QTest::newRow("itemUnlinked") << Protocol::ChangeNotification::Unlink << QByteArray(SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection))); + } + + void filterConnected() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + + Protocol::ChangeNotification msg; + msg.addEntity(1); + msg.setOperation(op); + msg.setType(Akonadi::Protocol::ChangeNotification::Items); + + QVERIFY(!m.acceptNotification(msg)); + m.monitorAll = true; + QVERIFY(m.acceptNotification(msg)); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + QVERIFY(m.acceptNotification(msg)); + m.monitorAll = false; + QVERIFY(!m.acceptNotification(msg)); + } + + void filterSession() + { + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + m.monitorAll = true; + QSignalSpy spy(&dummyMonitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QVERIFY(spy.isValid()); + + Protocol::ChangeNotification msg; + msg.addEntity(1); + msg.setOperation(Protocol::ChangeNotification::Add); + msg.setType(Akonadi::Protocol::ChangeNotification::Items); + msg.setSessionId("foo"); + + QVERIFY(m.acceptNotification(msg)); + m.sessions.append("bar"); + QVERIFY(m.acceptNotification(msg)); + m.sessions.append("foo"); + QVERIFY(!m.acceptNotification(msg)); + } + + void filterResource_data() + { + QTest::addColumn("op"); + QTest::addColumn("type"); + QTest::addColumn("signalName"); + + QTest::newRow("itemAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QTest::newRow("itemChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemChanged(Akonadi::Item,QSet))); + QTest::newRow("itemsFlagsChanged") << Protocol::ChangeNotification::ModifyFlags << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemsFlagsChanged(Akonadi::Item::List,QSet,QSet))); + QTest::newRow("itemRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemRemoved(Akonadi::Item))); + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("itemLinked") << Protocol::ChangeNotification::Link << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection))); + QTest::newRow("itemUnlinked") << Protocol::ChangeNotification::Unlink << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection))); + + QTest::newRow("colAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + QTest::newRow("colRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionRemoved(Akonadi::Collection))); + QTest::newRow("colMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Subscribe << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionSubscribed(Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Unsubscribe << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionUnsubscribed(Akonadi::Collection))); + } + + void filterResource() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(Protocol::ChangeNotification::Type, type); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + + Protocol::ChangeNotification msg; + msg.addEntity(1); + msg.setOperation(op); + msg.setParentCollection(2); + msg.setType(type); + msg.setResource("foo"); + msg.setSessionId("mysession"); + + // using the right resource makes it pass + QVERIFY(!m.acceptNotification(msg)); + m.resources.insert("bar"); + QVERIFY(!m.acceptNotification(msg)); + m.resources.insert("foo"); + QVERIFY(m.acceptNotification(msg)); + + // filtering out the session overwrites the resource + m.sessions.append("mysession"); + QVERIFY(!m.acceptNotification(msg)); + } + + void filterDestinationResource_data() + { + QTest::addColumn("op"); + QTest::addColumn("type"); + QTest::addColumn("signalName"); + + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))); + } + + void filterDestinationResource() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(Protocol::ChangeNotification::Type, type); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + + Protocol::ChangeNotification msg; + msg.setOperation(op); + msg.setType(type); + msg.setResource("foo"); + msg.setDestinationResource("bar"); + msg.setSessionId("mysession"); + msg.addEntity(1); + + // using the right resource makes it pass + QVERIFY(!m.acceptNotification(msg)); + m.resources.insert("bla"); + QVERIFY(!m.acceptNotification(msg)); + m.resources.insert("bar"); + QVERIFY(m.acceptNotification(msg)); + + // filtering out the mimetype does not overwrite resources + msg.addEntity(1, QString(), QString(), QStringLiteral("my/type")); + QVERIFY(m.acceptNotification(msg)); + + // filtering out the session overwrites the resource + m.sessions.append("mysession"); + QVERIFY(!m.acceptNotification(msg)); + } + + void filterMimeType_data() + { + QTest::addColumn("op"); + QTest::addColumn("type"); + QTest::addColumn("signalName"); + + QTest::newRow("itemAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QTest::newRow("itemChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemChanged(Akonadi::Item,QSet))); + QTest::newRow("itemsFlagsChanged") << Protocol::ChangeNotification::ModifyFlags << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemsFlagsChanged(Akonadi::Item::List,QSet,QSet))); + QTest::newRow("itemRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemRemoved(Akonadi::Item))); + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("itemLinked") << Protocol::ChangeNotification::Link << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection))); + QTest::newRow("itemUnlinked") << Protocol::ChangeNotification::Unlink << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection))); + + QTest::newRow("colAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + QTest::newRow("colRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionRemoved(Akonadi::Collection))); + QTest::newRow("colMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Subscribe << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionSubscribed(Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Unsubscribe << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionUnsubscribed(Akonadi::Collection))); + } + + void filterMimeType() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(Protocol::ChangeNotification::Type, type); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + + Protocol::ChangeNotification msg; + msg.addEntity(1, QString(), QString(), QStringLiteral("my/type")); + msg.setOperation(op); + msg.setParentCollection(2); + msg.setType(type); + msg.setResource("foo"); + msg.setSessionId("mysession"); + + // using the right resource makes it pass + QVERIFY(!m.acceptNotification(msg)); + m.mimetypes.insert(QStringLiteral("your/type")); + QVERIFY(!m.acceptNotification(msg)); + m.mimetypes.insert(QStringLiteral("my/type")); + QCOMPARE(m.acceptNotification(msg), type == Protocol::ChangeNotification::Items); + + // filter out the resource does not overwrite mimetype + m.resources.insert("bar"); + QCOMPARE(m.acceptNotification(msg), type == Protocol::ChangeNotification::Items); + + // filtering out the session overwrites the mimetype + m.sessions.append("mysession"); + QVERIFY(!m.acceptNotification(msg)); + } + + void filterCollection_data() + { + QTest::addColumn("op"); + QTest::addColumn("type"); + QTest::addColumn("signalName"); + + QTest::newRow("itemAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QTest::newRow("itemChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemChanged(Akonadi::Item,QSet))); + QTest::newRow("itemsFlagsChanged") << Protocol::ChangeNotification::ModifyFlags << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemsFlagsChanged(Akonadi::Item::List,QSet,QSet))); + QTest::newRow("itemRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemRemoved(Akonadi::Item))); + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("itemLinked") << Protocol::ChangeNotification::Link << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection))); + QTest::newRow("itemUnlinked") << Protocol::ChangeNotification::Unlink << Protocol::ChangeNotification::Items << QByteArray(SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection))); + + QTest::newRow("colAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + QTest::newRow("colRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionRemoved(Akonadi::Collection))); + QTest::newRow("colMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Subscribe << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionSubscribed(Akonadi::Collection,Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Unsubscribe << Protocol::ChangeNotification::Collections << QByteArray(SIGNAL(collectionUnsubscribed(Akonadi::Collection))); + } + + void filterCollection() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(Protocol::ChangeNotification::Type, type); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + + Protocol::ChangeNotification msg; + msg.addEntity(1, QString(), QString(), QStringLiteral("my/type")); + msg.setOperation(op); + msg.setParentCollection(2); + msg.setType(type); + msg.setResource("foo"); + msg.setSessionId("mysession"); + + // using the right resource makes it pass + QVERIFY(!m.acceptNotification(msg)); + m.collections.append(Collection(3)); + QVERIFY(!m.acceptNotification(msg)); + + for (int colId = 0; colId < 3; ++colId) { // 0 == root, 1 == this, 2 == parent + if (colId == 1 && type == Protocol::ChangeNotification::Items) { + continue; + } + + m.collections.clear(); + m.collections.append(Collection(colId)); + + QVERIFY(m.acceptNotification(msg)); + + // filter out the resource does overwrite collection + m.resources.insert("bar"); + QVERIFY(!m.acceptNotification(msg)); + m.resources.clear(); + + // filter out the mimetype does overwrite collection, for item operations (mimetype filter has no effect on collections) + m.mimetypes.insert(QStringLiteral("your/type")); + QCOMPARE(!m.acceptNotification(msg), type == Protocol::ChangeNotification::Items); + m.mimetypes.clear(); + + // filtering out the session overwrites the mimetype + m.sessions.append("mysession"); + QVERIFY(!m.acceptNotification(msg)); + m.sessions.clear(); + + // filter non-matching resource and matching mimetype make it pass + m.resources.insert("bar"); + m.mimetypes.insert(QStringLiteral("my/type")); + QVERIFY(m.acceptNotification(msg)); + m.resources.clear(); + m.mimetypes.clear(); + } + } +}; + +QTEST_MAIN(MonitorFilterTest) + +#include "monitorfiltertest.moc" diff --git a/autotests/libs/monitornotificationtest.cpp b/autotests/libs/monitornotificationtest.cpp new file mode 100644 index 0000000..7fb3f3d --- /dev/null +++ b/autotests/libs/monitornotificationtest.cpp @@ -0,0 +1,308 @@ +/* + Copyright (c) 2011 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 "monitor.h" + +#include "fakeserverdata.h" +#include "fakesession.h" +#include "inspectablemonitor.h" +#include "inspectablechangerecorder.h" +#include "akonaditestfake_export.h" +#include +#include + +using namespace Akonadi; + +class AKONADITESTFAKE_EXPORT MonitorNotificationTest : public QObject +{ + Q_OBJECT +public: + MonitorNotificationTest(QObject *parent = Q_NULLPTR) + : QObject(parent) + { + m_sessionName = "MonitorNotificationTest fake session"; + m_fakeSession = new FakeSession(m_sessionName, FakeSession::EndJobsImmediately); + m_fakeSession->setAsDefaultSession(); + } + +private Q_SLOTS: + void testSingleMessage(); + void testFillPipeline(); + void testMonitor(); + + void testSingleMessage_data(); + void testFillPipeline_data(); + void testMonitor_data(); + +private: + template + void testSingleMessage_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache); + template + void testFillPipeline_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache); + template + void testMonitor_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache); + +private: + FakeSession *m_fakeSession; + QByteArray m_sessionName; +}; + +void MonitorNotificationTest::testSingleMessage_data() +{ + QTest::addColumn("useChangeRecorder"); + + QTest::newRow("useChangeRecorder") << true; + QTest::newRow("useMonitor") << false; +} + +void MonitorNotificationTest::testSingleMessage() +{ + QFETCH(bool, useChangeRecorder); + + FakeCollectionCache *collectionCache = new FakeCollectionCache(m_fakeSession); + FakeItemCache *itemCache = new FakeItemCache(m_fakeSession); + FakeMonitorDependeciesFactory *depsFactory = new FakeMonitorDependeciesFactory(itemCache, collectionCache); + + if (!useChangeRecorder) { + testSingleMessage_impl(new InspectableMonitor(depsFactory, this), collectionCache, itemCache); + } else { + InspectableChangeRecorder *changeRecorder = new InspectableChangeRecorder(depsFactory, this); + changeRecorder->setChangeRecordingEnabled(false); + testSingleMessage_impl(changeRecorder, collectionCache, itemCache); + } +} + +template +void MonitorNotificationTest::testSingleMessage_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache) +{ + Q_UNUSED(itemCache) + + monitor->setSession(m_fakeSession); + monitor->fetchCollection(true); + + Protocol::ChangeNotification::List list; + + Collection parent(1); + Collection added(2); + + Protocol::ChangeNotification msg; + msg.setParentCollection(parent.id()); + msg.setOperation(Protocol::ChangeNotification::Add); + msg.setType(Protocol::ChangeNotification::Collections); + msg.addEntity(added.id()); + + QHash data; + data.insert(parent.id(), parent); + data.insert(added.id(), added); + + // Pending notifications remains empty because we don't fill the pipeline with one message. + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + monitor->notificationBus()->emitNotify(msg); + + QCOMPARE(monitor->pipeline().size(), 1); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + collectionCache->setData(data); + collectionCache->emitDataAvailable(); + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); +} + +void MonitorNotificationTest::testFillPipeline_data() +{ + QTest::addColumn("useChangeRecorder"); + + QTest::newRow("useChangeRecorder") << true; + QTest::newRow("useMonitor") << false; +} + +void MonitorNotificationTest::testFillPipeline() +{ + QFETCH(bool, useChangeRecorder); + + FakeCollectionCache *collectionCache = new FakeCollectionCache(m_fakeSession); + FakeItemCache *itemCache = new FakeItemCache(m_fakeSession); + FakeMonitorDependeciesFactory *depsFactory = new FakeMonitorDependeciesFactory(itemCache, collectionCache); + + if (!useChangeRecorder) { + testFillPipeline_impl(new InspectableMonitor(depsFactory, this), collectionCache, itemCache); + } else { + InspectableChangeRecorder *changeRecorder = new InspectableChangeRecorder(depsFactory, this); + changeRecorder->setChangeRecordingEnabled(false); + testFillPipeline_impl(changeRecorder, collectionCache, itemCache); + } +} + +template +void MonitorNotificationTest::testFillPipeline_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache) +{ + Q_UNUSED(itemCache) + + monitor->setSession(m_fakeSession); + monitor->fetchCollection(true); + + Protocol::ChangeNotification::List list; + QHash data; + + int i = 1; + while (i < 40) { + Collection parent(i++); + Collection added(i++); + + Protocol::ChangeNotification msg; + msg.setParentCollection(parent.id()); + msg.setOperation(Protocol::ChangeNotification::Add); + msg.setType(Protocol::ChangeNotification::Collections); + msg.addEntity(added.id()); + + data.insert(parent.id(), parent); + data.insert(added.id(), added); + + list << msg; + } + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + Q_FOREACH (const Protocol::ChangeNotification &ntf, list) { + monitor->notificationBus()->emitNotify(ntf); + } + + QCOMPARE(monitor->pipeline().size(), 5); + QCOMPARE(monitor->pendingNotifications().size(), 15); + + collectionCache->setData(data); + collectionCache->emitDataAvailable(); + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); +} + +void MonitorNotificationTest::testMonitor_data() +{ + QTest::addColumn("useChangeRecorder"); + + QTest::newRow("useChangeRecorder") << true; + QTest::newRow("useMonitor") << false; +} + +void MonitorNotificationTest::testMonitor() +{ + QFETCH(bool, useChangeRecorder); + + FakeCollectionCache *collectionCache = new FakeCollectionCache(m_fakeSession); + FakeItemCache *itemCache = new FakeItemCache(m_fakeSession); + FakeMonitorDependeciesFactory *depsFactory = new FakeMonitorDependeciesFactory(itemCache, collectionCache); + + if (!useChangeRecorder) { + testMonitor_impl(new InspectableMonitor(depsFactory, this), collectionCache, itemCache); + } else { + InspectableChangeRecorder *changeRecorder = new InspectableChangeRecorder(depsFactory, this); + changeRecorder->setChangeRecordingEnabled(false); + testMonitor_impl(changeRecorder, collectionCache, itemCache); + } +} + +template +void MonitorNotificationTest::testMonitor_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache) +{ + Q_UNUSED(itemCache) + + monitor->setSession(m_fakeSession); + monitor->fetchCollection(true); + + Protocol::ChangeNotification::List list; + + Collection col2(2); + col2.setParentCollection(Collection::root()); + + collectionCache->insert(col2); + + int i = 4; + + while (i < 8) { + Collection added(i++); + + Protocol::ChangeNotification msg; + msg.setParentCollection(i % 2 ? 2 : added.id() - 1); + msg.setOperation(Protocol::ChangeNotification::Add); + msg.setType(Protocol::ChangeNotification::Collections); + msg.addEntity(added.id()); + + list << msg; + } + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + Collection col4(4); + col4.setParentCollection(col2); + Collection col6(6); + col6.setParentCollection(col2); + + collectionCache->insert(col4); + collectionCache->insert(col6); + + qRegisterMetaType(); + QSignalSpy collectionAddedSpy(monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection))); + + collectionCache->emitDataAvailable(); + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + Q_FOREACH (const Protocol::ChangeNotification &ntf, list) { + monitor->notificationBus()->emitNotify(ntf); + } + + // Collection 6 is not notified, because Collection 5 has held up the pipeline + QCOMPARE(collectionAddedSpy.size(), 1); + QCOMPARE((int)collectionAddedSpy.takeFirst().first().value().id(), 4); + QCOMPARE(monitor->pipeline().size(), 3); + QCOMPARE(monitor->pendingNotifications().size(), 0); + + Collection col7(7); + col7.setParentCollection(col6); + + collectionCache->insert(col7); + collectionCache->emitDataAvailable(); + + // Collection 5 is still holding the pipeline + QCOMPARE(collectionAddedSpy.size(), 0); + QCOMPARE(monitor->pipeline().size(), 3); + QCOMPARE(monitor->pendingNotifications().size(), 0); + + Collection col5(5); + col5.setParentCollection(col4); + + collectionCache->insert(col5); + collectionCache->emitDataAvailable(); + + // Collection 5 is in cache, pipeline is flushed + QCOMPARE(collectionAddedSpy.size(), 3); + QCOMPARE(monitor->pipeline().size(), 0); + QCOMPARE(monitor->pendingNotifications().size(), 0); +} + +QTEST_MAIN(MonitorNotificationTest) +#include "monitornotificationtest.moc" diff --git a/autotests/libs/monitortest.cpp b/autotests/libs/monitortest.cpp new file mode 100644 index 0000000..2c27dd1 --- /dev/null +++ b/autotests/libs/monitortest.cpp @@ -0,0 +1,426 @@ +/* + 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 "monitortest.h" +#include "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(MonitorTest) + +static Collection res3; + +Q_DECLARE_METATYPE(Akonadi::Collection::Id) +Q_DECLARE_METATYPE(QSet) + +void MonitorTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + + res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + + AkonadiTest::setAllResourcesOffline(); +} + +void MonitorTest::testMonitor_data() +{ + QTest::addColumn("fetchCol"); + QTest::newRow("with collection fetching") << true; + QTest::newRow("without collection fetching") << false; +} + +void MonitorTest::testMonitor() +{ + QFETCH(bool, fetchCol); + + Monitor *monitor = new Monitor(this); + monitor->setCollectionMonitored(Collection::root()); + monitor->fetchCollection(fetchCol); + monitor->itemFetchScope().fetchFullPayload(); + monitor->itemFetchScope().setCacheOnly(true); + + // monitor signals + qRegisterMetaType(); + /* + qRegisterMetaType() registers the type with a + name of "qlonglong". Doing + qRegisterMetaType( "Akonadi::Collection::Id" ) + doesn't help. (works now , see QTBUG-937 and QTBUG-6833, -- dvratil) + + The problem here is that Akonadi::Collection::Id is a typedef to qlonglong, + and qlonglong is already a registered meta type. So the signal spy will + give us a QVariant of type Akonadi::Collection::Id, but calling + .value() on that variant will in fact end up + calling qvariant_cast. From the point of view of QMetaType, + Akonadi::Collection::Id and qlonglong are different types, so QVariant + can't convert, and returns a default-constructed qlonglong, zero. + + When connecting to a real slot (without QSignalSpy), this problem is + avoided, because the casting is done differently (via a lot of void + pointers). + + The docs say nothing about qRegisterMetaType -ing a typedef, so I'm not + sure if this is a bug or not. (cberzan) + */ + qRegisterMetaType("Akonadi::Collection::Id"); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType >(); + QSignalSpy caddspy(monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection))); + QSignalSpy cmodspy(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + QSignalSpy cmvspy(monitor, SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))); + QSignalSpy crmspy(monitor, SIGNAL(collectionRemoved(Akonadi::Collection))); + QSignalSpy cstatspy(monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics))); + QSignalSpy cSubscribedSpy(monitor, SIGNAL(collectionSubscribed(Akonadi::Collection,Akonadi::Collection))); + QSignalSpy cUnsubscribedSpy(monitor, SIGNAL(collectionUnsubscribed(Akonadi::Collection))); + QSignalSpy iaddspy(monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))); + QSignalSpy imodspy(monitor, SIGNAL(itemChanged(Akonadi::Item,QSet))); + QSignalSpy imvspy(monitor, SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); + QSignalSpy irmspy(monitor, SIGNAL(itemRemoved(Akonadi::Item))); + + QVERIFY(caddspy.isValid()); + QVERIFY(cmodspy.isValid()); + QVERIFY(cmvspy.isValid()); + QVERIFY(crmspy.isValid()); + QVERIFY(cstatspy.isValid()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isValid()); + QVERIFY(imodspy.isValid()); + QVERIFY(imvspy.isValid()); + QVERIFY(irmspy.isValid()); + + // create a collection + Collection monitorCol; + monitorCol.setParentCollection(res3); + monitorCol.setName(QStringLiteral("monitor")); + CollectionCreateJob *create = new CollectionCreateJob(monitorCol, this); + AKVERIFYEXEC(create); + monitorCol = create->collection(); + QVERIFY(monitorCol.isValid()); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)), 1000)); + + QCOMPARE(caddspy.count(), 1); + QList arg = caddspy.takeFirst(); + Collection col = arg.at(0).value(); + QCOMPARE(col, monitorCol); + if (fetchCol) { + QCOMPARE(col.name(), QStringLiteral("monitor")); + } + Collection parent = arg.at(1).value(); + QCOMPARE(parent, res3); + + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // add an item + Item newItem; + newItem.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(newItem, monitorCol, this); + AKVERIFYEXEC(append); + Item monitorRef = append->item(); + QVERIFY(monitorRef.isValid()); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), 1000)); + + QCOMPARE(cstatspy.count(), 1); + arg = cstatspy.takeFirst(); + QCOMPARE(arg.at(0).value(), monitorCol.id()); + + QCOMPARE(iaddspy.count(), 1); + arg = iaddspy.takeFirst(); + Item item = arg.at(0).value(); + QCOMPARE(item, monitorRef); + QCOMPARE(item.mimeType(), QString::fromLatin1("application/octet-stream")); + Collection collection = arg.at(1).value(); + QCOMPARE(collection.id(), monitorCol.id()); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // modify an item + item.setPayload("some new content"); + ItemModifyJob *store = new ItemModifyJob(item, this); + AKVERIFYEXEC(store); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), 1000)); + + QCOMPARE(cstatspy.count(), 1); + arg = cstatspy.takeFirst(); + QCOMPARE(arg.at(0).value(), monitorCol.id()); + + QCOMPARE(imodspy.count(), 1); + arg = imodspy.takeFirst(); + item = arg.at(0).value(); + QCOMPARE(monitorRef, item); + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), QByteArray("some new content")); + QSet parts = arg.at(1).value >(); + QCOMPARE(parts, QSet() << "PLD:RFC822"); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // move an item + ItemMoveJob *move = new ItemMoveJob(item, res3); + AKVERIFYEXEC(move); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), 1000)); + QCOMPARE(cstatspy.count(), 2); + // NOTE: We don't make any assumptions about the order of the collectionStatisticsChanged + // signals, they seem to arrive in random order + QList notifiedCols; + notifiedCols << cstatspy.takeFirst().at(0).value() + << cstatspy.takeFirst().at(0).value(); + QVERIFY(notifiedCols.contains(res3.id())); // destination + QVERIFY(notifiedCols.contains(monitorCol.id())); // source + + QCOMPARE(imvspy.count(), 1); + arg = imvspy.takeFirst(); + item = arg.at(0).value(); // the item + QCOMPARE(monitorRef, item); + col = arg.at(1).value(); // the source collection + QCOMPARE(col.id(), monitorCol.id()); + col = arg.at(2).value(); // the destination collection + QCOMPARE(col.id(), res3.id()); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // delete an item + ItemDeleteJob *del = new ItemDeleteJob(monitorRef, this); + AKVERIFYEXEC(del); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), 1000)); + + QCOMPARE(cstatspy.count(), 1); + arg = cstatspy.takeFirst(); + QCOMPARE(arg.at(0).value(), res3.id()); + cmodspy.clear(); + + QCOMPARE(irmspy.count(), 1); + arg = irmspy.takeFirst(); + Item ref = qvariant_cast(arg.at(0)); + QCOMPARE(monitorRef, ref); + QCOMPARE(ref.parentCollection(), res3); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + imvspy.clear(); + + // Unsubscribe and re-subscribed a collection that existed before the monitor was created. + Collection subCollection = Collection(collectionIdFromPath(QStringLiteral("res2/foo2"))); + subCollection.setName(QStringLiteral("foo2")); + QVERIFY(subCollection.isValid()); + + SubscriptionJob *subscribeJob = new SubscriptionJob(this); + subscribeJob->unsubscribe(Collection::List() << subCollection); + AKVERIFYEXEC(subscribeJob); + // Wait for unsubscribed signal, it goes after changed, so we can check for both + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionUnsubscribed(Akonadi::Collection)), 1000)); + QCOMPARE(cmodspy.size(), 1); + arg = cmodspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), subCollection.id()); + + QVERIFY(cSubscribedSpy.isEmpty()); + QCOMPARE(cUnsubscribedSpy.size(), 1); + arg = cUnsubscribedSpy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), subCollection.id()); + + subscribeJob = new SubscriptionJob(this); + subscribeJob->subscribe(Collection::List() << subCollection); + AKVERIFYEXEC(subscribeJob); + // Wait for subscribed signal, it goes after changed, so we can check for both + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionSubscribed(Akonadi::Collection,Akonadi::Collection)), 1000)); + QCOMPARE(cmodspy.size(), 1); + arg = cmodspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), subCollection.id()); + + QVERIFY(cUnsubscribedSpy.isEmpty()); + QCOMPARE(cSubscribedSpy.size(), 1); + arg = cSubscribedSpy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), subCollection.id()); + if (fetchCol) { + QVERIFY(!col.name().isEmpty()); + QCOMPARE(col.name(), subCollection.name()); + } + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // modify a collection + monitorCol.setName(QStringLiteral("changed name")); + CollectionModifyJob *mod = new CollectionModifyJob(monitorCol, this); + AKVERIFYEXEC(mod); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionChanged(Akonadi::Collection)), 1000)); + + QCOMPARE(cmodspy.count(), 1); + arg = cmodspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col, monitorCol); + if (fetchCol) { + QCOMPARE(col.name(), QStringLiteral("changed name")); + } + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // move a collection + Collection dest = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); + CollectionMoveJob *cmove = new CollectionMoveJob(monitorCol, dest, this); + AKVERIFYEXEC(cmove); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)), 1000)); + + QCOMPARE(cmvspy.count(), 1); + arg = cmvspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col, monitorCol); + QCOMPARE(col.parentCollection(), dest); + if (fetchCol) { + QCOMPARE(col.name(), monitorCol.name()); + } + col = arg.at(1).value(); + QCOMPARE(col, res3); + col = arg.at(2).value(); + QCOMPARE(col, dest); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // delete a collection + CollectionDeleteJob *cdel = new CollectionDeleteJob(monitorCol, this); + AKVERIFYEXEC(cdel); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), 1000)); + + QCOMPARE(crmspy.count(), 1); + arg = crmspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), monitorCol.id()); + QCOMPARE(col.parentCollection(), dest); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); +} + +void MonitorTest::testVirtualCollectionsMonitoring() +{ + Monitor *monitor = new Monitor(this); + monitor->setCollectionMonitored(Collection(1)); // top-level 'Search' collection + + QSignalSpy caddspy(monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection))); + QVERIFY(caddspy.isValid()); + + SearchCreateJob *job = new SearchCreateJob(QStringLiteral("Test search collection"), Akonadi::SearchQuery(), this); + AKVERIFYEXEC(job); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)), 1000)); + QCOMPARE(caddspy.count(), 1); +} + diff --git a/autotests/libs/monitortest.h b/autotests/libs/monitortest.h new file mode 100644 index 0000000..e7cd1d5 --- /dev/null +++ b/autotests/libs/monitortest.h @@ -0,0 +1,35 @@ +/* + 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. +*/ + +#ifndef MONITORTEST_H +#define MONITORTEST_H + +#include + +class MonitorTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testMonitor_data(); + void testMonitor(); + void testVirtualCollectionsMonitoring(); +}; + +#endif diff --git a/autotests/libs/newmailnotifierattributetest.cpp b/autotests/libs/newmailnotifierattributetest.cpp new file mode 100644 index 0000000..6c733fe --- /dev/null +++ b/autotests/libs/newmailnotifierattributetest.cpp @@ -0,0 +1,75 @@ +/* + Copyright (c) 2014 Montel Laurent + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "newmailnotifierattributetest.h" +#include "newmailnotifierattribute.h" +#include +using namespace Akonadi; +NewMailNotifierAttributeTest::NewMailNotifierAttributeTest(QObject *parent) + : QObject(parent) +{ + +} + +NewMailNotifierAttributeTest::~NewMailNotifierAttributeTest() +{ + +} + +void NewMailNotifierAttributeTest::shouldHaveDefaultValue() +{ + NewMailNotifierAttribute attr; + QVERIFY(!attr.ignoreNewMail()); +} + +void NewMailNotifierAttributeTest::shouldSetIgnoreNotification() +{ + NewMailNotifierAttribute attr; + bool ignore = false; + attr.setIgnoreNewMail(ignore); + QCOMPARE(attr.ignoreNewMail(), ignore); + ignore = true; + attr.setIgnoreNewMail(ignore); + QCOMPARE(attr.ignoreNewMail(), ignore); +} + +void NewMailNotifierAttributeTest::shouldSerializedData() +{ + NewMailNotifierAttribute attr; + attr.setIgnoreNewMail(true); + QByteArray ba = attr.serialized(); + NewMailNotifierAttribute result; + result.deserialize(ba); + QVERIFY(attr == result); +} + +void NewMailNotifierAttributeTest::shouldCloneAttribute() +{ + NewMailNotifierAttribute attr; + attr.setIgnoreNewMail(true); + NewMailNotifierAttribute *result = attr.clone(); + QVERIFY(attr == *result); + delete result; +} + +void NewMailNotifierAttributeTest::shouldHaveType() +{ + NewMailNotifierAttribute attr; + QCOMPARE(attr.type(), QByteArray("newmailnotifierattribute")); +} + +QTEST_MAIN(NewMailNotifierAttributeTest) diff --git a/autotests/libs/newmailnotifierattributetest.h b/autotests/libs/newmailnotifierattributetest.h new file mode 100644 index 0000000..713653a --- /dev/null +++ b/autotests/libs/newmailnotifierattributetest.h @@ -0,0 +1,37 @@ +/* + Copyright (c) 2014 Montel Laurent + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef NEWMAILNOTIFIERATTRIBUTETEST_H +#define NEWMAILNOTIFIERATTRIBUTETEST_H + +#include + +class NewMailNotifierAttributeTest : public QObject +{ + Q_OBJECT +public: + explicit NewMailNotifierAttributeTest(QObject *parent = Q_NULLPTR); + ~NewMailNotifierAttributeTest(); +private Q_SLOTS: + void shouldHaveDefaultValue(); + void shouldSetIgnoreNotification(); + void shouldSerializedData(); + void shouldCloneAttribute(); + void shouldHaveType(); +}; + +#endif // NEWMAILNOTIFIERATTRIBUTETEST_H diff --git a/autotests/libs/pop3resourceattributetest.cpp b/autotests/libs/pop3resourceattributetest.cpp new file mode 100644 index 0000000..c3931b6 --- /dev/null +++ b/autotests/libs/pop3resourceattributetest.cpp @@ -0,0 +1,73 @@ +/* + Copyright (c) 2014 Montel Laurent + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "pop3resourceattributetest.h" +#include "pop3resourceattribute.h" +#include +Pop3ResourceAttributeTest::Pop3ResourceAttributeTest(QObject *parent) + : QObject(parent) +{ + +} + +Pop3ResourceAttributeTest::~Pop3ResourceAttributeTest() +{ + +} + +void Pop3ResourceAttributeTest::shouldHaveDefaultValue() +{ + Akonadi::Pop3ResourceAttribute attr; + QVERIFY(attr.pop3AccountName().isEmpty()); +} + +void Pop3ResourceAttributeTest::shouldAssignValue() +{ + Akonadi::Pop3ResourceAttribute attr; + QString accountName; + attr.setPop3AccountName(accountName); + QCOMPARE(attr.pop3AccountName(), accountName); + accountName = QStringLiteral("foo"); + attr.setPop3AccountName(accountName); + QCOMPARE(attr.pop3AccountName(), accountName); + accountName.clear(); + attr.setPop3AccountName(accountName); + QCOMPARE(attr.pop3AccountName(), accountName); +} + +void Pop3ResourceAttributeTest::shouldDeserializeValue() +{ + Akonadi::Pop3ResourceAttribute attr; + QString accountName = QStringLiteral("foo"); + attr.setPop3AccountName(accountName); + const QByteArray ba = attr.serialized(); + Akonadi::Pop3ResourceAttribute result; + result.deserialize(ba); + QVERIFY(attr == result); +} + +void Pop3ResourceAttributeTest::shouldCloneAttribute() +{ + Akonadi::Pop3ResourceAttribute attr; + QString accountName = QStringLiteral("foo"); + attr.setPop3AccountName(accountName); + Akonadi::Pop3ResourceAttribute *result = attr.clone(); + QVERIFY(attr == *result); + delete result; +} + +QTEST_MAIN(Pop3ResourceAttributeTest) diff --git a/autotests/libs/pop3resourceattributetest.h b/autotests/libs/pop3resourceattributetest.h new file mode 100644 index 0000000..0b5137b --- /dev/null +++ b/autotests/libs/pop3resourceattributetest.h @@ -0,0 +1,36 @@ +/* + Copyright (c) 2014 Montel Laurent + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef POP3RESOURCEATTRIBUTETEST_H +#define POP3RESOURCEATTRIBUTETEST_H + +#include + +class Pop3ResourceAttributeTest : public QObject +{ + Q_OBJECT +public: + explicit Pop3ResourceAttributeTest(QObject *parent = Q_NULLPTR); + ~Pop3ResourceAttributeTest(); +private Q_SLOTS: + void shouldHaveDefaultValue(); + void shouldAssignValue(); + void shouldDeserializeValue(); + void shouldCloneAttribute(); +}; + +#endif // POP3RESOURCEATTRIBUTETEST_H diff --git a/autotests/libs/protocolhelpertest.cpp b/autotests/libs/protocolhelpertest.cpp new file mode 100644 index 0000000..5f07ec2 --- /dev/null +++ b/autotests/libs/protocolhelpertest.cpp @@ -0,0 +1,334 @@ +/* + 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; + +Q_DECLARE_METATYPE(Scope) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(Protocol::FetchCollectionsResponse) +Q_DECLARE_METATYPE(Protocol::FetchScope) +Q_DECLARE_METATYPE(Protocol::FetchTagsResponse) + +class ProtocolHelperTest : public QObject +{ + Q_OBJECT +private Q_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(QStringLiteral("A")); + Item r2; r2.setRemoteId(QStringLiteral("B")); + Item h1; h1.setRemoteId(QStringLiteral("H1")); h1.setParentCollection(Collection::root()); + Item h2; h2.setRemoteId(QStringLiteral("H2a")); h2.parentCollection().setRemoteId(QStringLiteral("H2b")); h2.parentCollection().setParentCollection(Collection::root()); + Item h3; h3.setRemoteId(QStringLiteral("H3a")); h3.parentCollection().setRemoteId(QStringLiteral("H3b")); + + QTest::newRow("empty") << Item::List() << Scope() << true; + QTest::newRow("single uid") << (Item::List() << u1) << Scope(1) << false; + QTest::newRow("multi uid") << (Item::List() << u1 << u3) << Scope(QVector { 1, 3 }) << false; + QTest::newRow("block uid") << (Item::List() << u1 << u2 << u3) << Scope(ImapInterval(1, 3)) << false; + QTest::newRow("single rid") << (Item::List() << r1) << Scope(Scope::Rid, { QStringLiteral("A") }) << false; + QTest::newRow("multi rid") << (Item::List() << r1 << r2) << Scope(Scope::Rid, { QStringLiteral("A"), QStringLiteral("B") }) << false; + QTest::newRow("invalid") << (Item::List() << Item()) << Scope() << true; + QTest::newRow("mixed") << (Item::List() << u1 << r1) << Scope() << true; + QTest::newRow("single hrid") << (Item::List() << h1) << Scope({ Scope::HRID(-1, QStringLiteral("H1")), Scope::HRID(0) }) << false; + QTest::newRow("single hrid 2") << (Item::List() << h2) << Scope({ Scope::HRID(-1, QStringLiteral("H2a")), Scope::HRID(-2, QStringLiteral("H2b")), Scope::HRID(0) }) << false; + QTest::newRow("mixed hrid/rid") << (Item::List() << h1 << r1) << Scope(Scope::Rid, { QStringLiteral("H1"), QStringLiteral("A") }) << false; + QTest::newRow("unterminated hrid") << (Item::List() << h3) << Scope(Scope::Rid, { QStringLiteral("H3a") }) << false; + } + + void testItemSetToByteArray() + { + QFETCH(Item::List, items); + QFETCH(Scope, result); + QFETCH(bool, shouldThrow); + + bool didThrow = false; + try { + const Scope scope = ProtocolHelper::entitySetToScope(items); + QCOMPARE(scope, result); + } catch (const std::exception &e) { + qDebug() << e.what(); + didThrow = true; + } + QCOMPARE(didThrow, shouldThrow); + } + + void testAncestorParsing_data() + { + QTest::addColumn>("input"); + QTest::addColumn("parent"); + + QTest::newRow("top-level") << QVector { Protocol::Ancestor(0) } << Collection::root(); + + Protocol::Ancestor a1(42); + a1.setRemoteId(QStringLiteral("net")); + + Collection c1; + c1.setRemoteId(QStringLiteral("net")); + c1.setId(42); + c1.setParentCollection(Collection::root()); + QTest::newRow("till's obscure folder") << QVector { a1, Protocol::Ancestor(0) } << c1; + } + + void testAncestorParsing() + { + QFETCH(QVector, input); + QFETCH(Collection, parent); + + Item i; + ProtocolHelper::parseAncestors(input, &i); + QCOMPARE(i.parentCollection().id(), parent.id()); + QCOMPARE(i.parentCollection().remoteId(), parent.remoteId()); + } + + void testCollectionParsing_data() + { + QTest::addColumn("input"); + QTest::addColumn("collection"); + + Collection c1; + c1.setId(2); + c1.setRemoteId(QStringLiteral("r2")); + c1.parentCollection().setId(1); + c1.setName(QStringLiteral("n2")); + + { + Protocol::FetchCollectionsResponse resp(2); + resp.setParentId(1); + resp.setRemoteId(QStringLiteral("r2")); + resp.setName(QStringLiteral("n2")); + QTest::newRow("no ancestors") << resp << c1; + } + + { + Protocol::FetchCollectionsResponse resp(3); + resp.setParentId(2); + resp.setRemoteId(QStringLiteral("r3")); + resp.setAncestors({ Protocol::Ancestor(2, QStringLiteral("r2")), Protocol::Ancestor(1, QStringLiteral("r1")), Protocol::Ancestor(0) }); + + Collection c2; + c2.setId(3); + c2.setRemoteId(QStringLiteral("r3")); + c2.parentCollection().setId(2); + c2.parentCollection().setRemoteId(QStringLiteral("r2")); + c2.parentCollection().parentCollection().setId(1); + c2.parentCollection().parentCollection().setRemoteId(QStringLiteral("r1")); + c2.parentCollection().parentCollection().setParentCollection(Collection::root()); + QTest::newRow("ancestors") << resp << c2; + } + } + + void testCollectionParsing() + { + QFETCH(Protocol::FetchCollectionsResponse, input); + QFETCH(Collection, collection); + + Collection parsedCollection = ProtocolHelper::parseCollection(input); + + 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; + qDebug() << p1.isValid() << p2.isValid(); + } + } + + void testParentCollectionAfterCollectionParsing() + { + Protocol::FetchCollectionsResponse resp(111); + resp.setParentId(222); + resp.setRemoteId(QStringLiteral("A")); + resp.setAncestors({ Protocol::Ancestor(222), Protocol::Ancestor(333), Protocol::Ancestor(0) }); + + Collection parsedCollection = ProtocolHelper::parseCollection(resp); + + QList ids; + ids << 111 << 222 << 333 << 0; + int i = 0; + + Collection col = parsedCollection; + while (col.isValid()) { + QCOMPARE(col.id(), ids[i++]); + col = col.parentCollection(); + } + QCOMPARE(i, 4); + } + + void testHRidToScope_data() + { + QTest::addColumn("collection"); + QTest::addColumn("result"); + + QTest::newRow("empty") << Collection() << Scope(); + + { + Scope scope; + scope.setHRidChain({ Scope::HRID(0) }); + QTest::newRow("root") << Collection::root() << scope; + } + + Collection c; + c.setId(1); + c.setParentCollection(Collection::root()); + c.setRemoteId(QStringLiteral("r1")); + { + Scope scope; + scope.setHRidChain({ Scope::HRID(1, QStringLiteral("r1")), Scope::HRID(0) }); + QTest::newRow("one level") << c << scope; + } + + { + Collection c2; + c2.setId(2); + c2.setParentCollection(c); + c2.setRemoteId(QStringLiteral("r2")); + + Scope scope; + scope.setHRidChain({ Scope::HRID(2, QStringLiteral("r2")), Scope::HRID(1, QStringLiteral("r1")), Scope::HRID(0) }); + QTest::newRow("two level ok") << c2 << scope; + } + } + + void testHRidToScope() + { + QFETCH(Collection, collection); + QFETCH(Scope, result); + QCOMPARE(ProtocolHelper::hierarchicalRidToScope(collection), result); + } + + void testItemFetchScopeToProtocol_data() + { + QTest::addColumn("scope"); + QTest::addColumn("result"); + + { + Protocol::FetchScope fs; + fs.setFetch(Protocol::FetchScope::Flags | + Protocol::FetchScope::Size | + Protocol::FetchScope::RemoteID | + Protocol::FetchScope::RemoteRevision | + Protocol::FetchScope::MTime); + QTest::newRow("empty") << ItemFetchScope() << fs; + } + + { + ItemFetchScope scope; + scope.fetchAllAttributes(); + scope.fetchFullPayload(); + scope.setAncestorRetrieval(Akonadi::ItemFetchScope::All); + scope.setIgnoreRetrievalErrors(true); + + Protocol::FetchScope fs; + fs.setFetch(Protocol::FetchScope::FullPayload | + Protocol::FetchScope::AllAttributes | + Protocol::FetchScope::Flags | + Protocol::FetchScope::Size | + Protocol::FetchScope::RemoteID | + Protocol::FetchScope::RemoteRevision | + Protocol::FetchScope::MTime | + Protocol::FetchScope::IgnoreErrors); + fs.setAncestorDepth(Akonadi::Protocol::Ancestor::AllAncestors); + QTest::newRow("full") << scope << fs; + } + + { + ItemFetchScope scope; + scope.setFetchModificationTime(false); + scope.setFetchRemoteIdentification(false); + + Protocol::FetchScope fs; + fs.setFetch(Protocol::FetchScope::Flags | + Protocol::FetchScope::Size); + QTest::newRow("minimal") << scope << fs; + } + } + + void testItemFetchScopeToProtocol() + { + QFETCH(ItemFetchScope, scope); + QFETCH(Protocol::FetchScope, result); + QCOMPARE(ProtocolHelper::itemFetchScopeToProtocol(scope), result); + } + + void testTagParsing_data() + { + QTest::addColumn("input"); + QTest::addColumn("expected"); + + QTest::newRow("invalid") << Protocol::FetchTagsResponse(-1) << Tag(); + + Protocol::FetchTagsResponse response(15); + response.setGid("TAG13GID"); + response.setRemoteId("TAG13RID"); + response.setParentId(-1); + response.setType("PLAIN"); + response.setAttributes({ { "TAGAttribute", "MyAttribute" } }); + + Tag tag(15); + tag.setGid("TAG13GID"); + tag.setRemoteId("TAG13RID"); + tag.setType("PLAIN"); + auto attr = AttributeFactory::createAttribute("TAGAttribute"); + attr->deserialize("MyAttribute"); + tag.addAttribute(attr); + QTest::newRow("valid with invalid parent") << response << tag; + + response.setParentId(15); + tag.setParent(Tag(15)); + QTest::newRow("valid with valid parent") << response << tag; + } + + void testTagParsing() + { + QFETCH(Protocol::FetchTagsResponse, input); + QFETCH(Tag, expected); + + const Tag tag = ProtocolHelper::parseTagFetchResult(input); + QCOMPARE(tag.id(), expected.id()); + QCOMPARE(tag.gid(), expected.gid()); + QCOMPARE(tag.remoteId(), expected.remoteId()); + QCOMPARE(tag.type(), expected.type()); + QCOMPARE(tag.parent(), expected.parent()); + QCOMPARE(tag.attributes().size(), expected.attributes().size()); + for (int i = 0; i < tag.attributes().size(); ++i) { + Attribute *attr = tag.attributes().at(i); + Attribute *expectedAttr = expected.attributes().at(i); + QCOMPARE(attr->type(), expectedAttr->type()); + QCOMPARE(attr->serialized(), expectedAttr->serialized()); + } + } +}; + +QTEST_MAIN(ProtocolHelperTest) + +#include "protocolhelpertest.moc" diff --git a/autotests/libs/proxymodelstest.cpp b/autotests/libs/proxymodelstest.cpp new file mode 100644 index 0000000..bc49092 --- /dev/null +++ b/autotests/libs/proxymodelstest.cpp @@ -0,0 +1,126 @@ +/* + Copyright (c) 2010 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. +*/ + +#include + +#include +#include + +class KRFPTestModel : public KRecursiveFilterProxyModel +{ +public: + KRFPTestModel(QObject *parent) : KRecursiveFilterProxyModel(parent) { } + + bool acceptRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE + { + const QModelIndex modelIndex = sourceModel()->index(sourceRow, 0, sourceParent); + return !modelIndex.data().toString().contains(QStringLiteral("three")); + } + +}; + +class ProxyModelsTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void init(); + + void testMatch(); + +private: + QStandardItemModel m_model; + KRecursiveFilterProxyModel *m_krfp; + KRFPTestModel *m_krfptest; +}; + +void ProxyModelsTest::initTestCase() +{ +} + +void ProxyModelsTest::init() +{ + m_model.setRowCount(5); + m_model.setColumnCount(1); + m_model.setData(m_model.index(0, 0, QModelIndex()), QStringLiteral("one")); + QModelIndex idx = m_model.index(1, 0, QModelIndex()); + m_model.setData(idx, QStringLiteral("two")); + m_model.insertRows(0, 1, idx); + m_model.insertColumns(0, 1, idx); + m_model.setData(m_model.index(0, 0, idx), QStringLiteral("three")); + m_model.setData(m_model.index(2, 0, QModelIndex()), QStringLiteral("three")); + m_model.setData(m_model.index(3, 0, QModelIndex()), QStringLiteral("four")); + m_model.setData(m_model.index(4, 0, QModelIndex()), QStringLiteral("five")); + + m_model.setData(m_model.index(4, 0, QModelIndex()), QStringLiteral("mystuff"), Qt::UserRole + 42); + + m_krfp = new KRecursiveFilterProxyModel(this); + m_krfp->setSourceModel(&m_model); + m_krfptest = new KRFPTestModel(this); + m_krfptest->setSourceModel(m_krfp); + + // some sanity checks that setup worked + QCOMPARE(m_model.rowCount(QModelIndex()), 5); + QCOMPARE(m_model.data(m_model.index(0, 0)).toString(), QStringLiteral("one")); + QCOMPARE(m_krfp->rowCount(QModelIndex()), 5); + QCOMPARE(m_krfp->data(m_krfp->index(0, 0)).toString(), QStringLiteral("one")); + QCOMPARE(m_krfptest->rowCount(QModelIndex()), 4); + QCOMPARE(m_krfptest->data(m_krfptest->index(0, 0)).toString(), QStringLiteral("one")); + + QCOMPARE(m_krfp->rowCount(m_krfp->index(1, 0)), 1); + QCOMPARE(m_krfptest->rowCount(m_krfptest->index(1, 0)), 0); +} + +void ProxyModelsTest::testMatch() +{ + QModelIndexList results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 1); + results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, QStringLiteral("fourtytwo")); + QCOMPARE(results.size(), 0); + results = m_model.match(m_model.index(0, 0), Qt::UserRole + 42, QStringLiteral("mystuff")); + QCOMPARE(results.size(), 1); + + results = m_krfp->match(m_krfp->index(0, 0), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 1); + results = m_krfp->match(m_krfp->index(0, 0), Qt::UserRole + 42, QStringLiteral("mystuff")); + QCOMPARE(results.size(), 1); + + results = m_krfptest->match(m_krfptest->index(0, 0), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 0); + results = m_krfptest->match(m_krfptest->index(0, 0), Qt::UserRole + 42, QStringLiteral("mystuff")); + QCOMPARE(results.size(), 1); + + results = m_model.match(QModelIndex(), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 0); + results = m_krfp->match(QModelIndex(), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 0); + results = m_krfptest->match(QModelIndex(), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 0); + + const QModelIndex index = m_model.index(0, 0, QModelIndex()); + results = m_model.match(index, Qt::DisplayRole, QStringLiteral("three"), -1, Qt::MatchRecursive | Qt::MatchStartsWith | Qt::MatchWrap); + QCOMPARE(results.size(), 2); +} + +#include "proxymodelstest.moc" + +QTEST_MAIN(ProxyModelsTest) + diff --git a/autotests/libs/referencetest.cpp b/autotests/libs/referencetest.cpp new file mode 100644 index 0000000..5ee8fd4 --- /dev/null +++ b/autotests/libs/referencetest.cpp @@ -0,0 +1,234 @@ +/* + 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 "referencetest.h" + +#include + +#include +#include "test_utils.h" + +#include "agentmanager.h" +#include "agentinstance.h" +#include "cachepolicy.h" +#include "collection.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionmodifyjob.h" +#include "control.h" +#include "item.h" +#include "collectionfetchscope.h" +#include "session.h" +#include "monitor.h" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(ReferenceTest, NoGUI) + +void ReferenceTest::initTestCase() +{ + qRegisterMetaType(); + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); +} + +static Collection::Id res1ColId = 6; // -1; + +void ReferenceTest::testReference() +{ + Akonadi::Collection baseCol; + { + baseCol.setParentCollection(Akonadi::Collection(res1ColId)); + baseCol.setName("base"); + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(baseCol); + AKVERIFYEXEC(create); + baseCol = create->collection(); + } + + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setName("referenced"); + col.setEnabled(false); + { + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + CollectionFetchJob *job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + col = job->collections().first(); + } + { + col.setReferenced(true); + Akonadi::CollectionModifyJob *modify = new Akonadi::CollectionModifyJob(col); + AKVERIFYEXEC(modify); + CollectionFetchJob *job = new CollectionFetchJob(col, CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), false); + QCOMPARE(result.referenced(), true); + } + { + col.setReferenced(false); + Akonadi::CollectionModifyJob *modify = new Akonadi::CollectionModifyJob(col); + AKVERIFYEXEC(modify); + CollectionFetchJob *job = new CollectionFetchJob(col, CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), false); + QCOMPARE(result.referenced(), false); + } + } + + //Cleanup + CollectionDeleteJob *deleteJob = new CollectionDeleteJob(baseCol); + AKVERIFYEXEC(deleteJob); +} + +void ReferenceTest::testReferenceFromMultiSession() +{ + Akonadi::Collection baseCol; + { + baseCol.setParentCollection(Akonadi::Collection(res1ColId)); + baseCol.setName("base"); + baseCol.setEnabled(false); + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(baseCol); + AKVERIFYEXEC(create); + baseCol = create->collection(); + } + + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setName("referenced"); + col.setEnabled(false); + { + Akonadi::CollectionCreateJob *create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + CollectionFetchJob *job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + col = job->collections().first(); + } + + Akonadi::Session *session1 = new Akonadi::Session("session1"); + Akonadi::Monitor *monitor1 = new Akonadi::Monitor(); + monitor1->setSession(session1); + monitor1->setCollectionMonitored(Collection::root()); + + Akonadi::Session *session2 = new Akonadi::Session("session2"); + Akonadi::Monitor *monitor2 = new Akonadi::Monitor(); + monitor2->setSession(session2); + monitor2->setCollectionMonitored(Collection::root()); + + //Reference in first session and ensure second session is not affected + { + QSignalSpy cmodspy1(monitor1, SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + QSignalSpy cmodspy2(monitor2, SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + + col.setReferenced(true); + Akonadi::CollectionModifyJob *modify = new Akonadi::CollectionModifyJob(col, session1); + AKVERIFYEXEC(modify); + + //We want a signal only in the session that referenced the collection + QVERIFY(QTest::kWaitForSignal(monitor1, SIGNAL(collectionChanged(Akonadi::Collection)), 1000)); + QTest::qWait(100); + QCOMPARE(cmodspy1.count(), 1); + QCOMPARE(cmodspy2.count(), 0); + + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::Recursive, session1); + job->fetchScope().setListFilter(CollectionFetchScope::Display); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 1); + } + + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::Recursive, session2); + job->fetchScope().setListFilter(CollectionFetchScope::Display); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 0); + } + } + //Reference in second session and ensure first session is not affected + { + QSignalSpy cmodspy1(monitor1, SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + QSignalSpy cmodspy2(monitor2, SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + + col.setReferenced(true); + Akonadi::CollectionModifyJob *modify = new Akonadi::CollectionModifyJob(col, session2); + AKVERIFYEXEC(modify); + + //We want a signal only in the session that referenced the collection + QVERIFY(QTest::kWaitForSignal(monitor2, SIGNAL(collectionChanged(Akonadi::Collection)), 1000)); + QTest::qWait(100); + //FIXME The first session still gets the notification since it has the session referenced + QCOMPARE(cmodspy1.count(), 1); + QCOMPARE(cmodspy2.count(), 1); + + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::Recursive, session1); + job->fetchScope().setListFilter(CollectionFetchScope::Display); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 1); + } + + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::Recursive, session2); + job->fetchScope().setListFilter(CollectionFetchScope::Display); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 1); + } + } + { + QSignalSpy cmodspy1(monitor1, SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + QSignalSpy cmodspy2(monitor2, SIGNAL(collectionChanged(Akonadi::Collection,QSet))); + col.setReferenced(false); + Akonadi::CollectionModifyJob *modify = new Akonadi::CollectionModifyJob(col, session1); + AKVERIFYEXEC(modify); + + //We want a signal only in the session that referenced the collection + QVERIFY(QTest::kWaitForSignal(monitor1, SIGNAL(collectionChanged(Akonadi::Collection)), 1000)); + QTest::qWait(100); + QCOMPARE(cmodspy1.count(), 1); + //FIXME here we still get a notification for dereferenced because we don't filter correctly + QCOMPARE(cmodspy2.count(), 1); + + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::Recursive, session1); + job->fetchScope().setListFilter(CollectionFetchScope::Display); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 0); + } + + { + CollectionFetchJob *job = new CollectionFetchJob(baseCol, CollectionFetchJob::Recursive, session2); + job->fetchScope().setListFilter(CollectionFetchScope::Display); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 1); + } + } + } + + //Cleanup + CollectionDeleteJob *deleteJob = new CollectionDeleteJob(baseCol); + AKVERIFYEXEC(deleteJob); +} + +#include "referencetest.moc" diff --git a/autotests/libs/referencetest.h b/autotests/libs/referencetest.h new file mode 100644 index 0000000..afd4ea9 --- /dev/null +++ b/autotests/libs/referencetest.h @@ -0,0 +1,34 @@ +/* + 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. +*/ + +#ifndef REFERENCETEST_H +#define REFERENCETEST_H + +#include + +class ReferenceTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testReference(); + void testReferenceFromMultiSession(); +}; + +#endif diff --git a/autotests/libs/relationtest.cpp b/autotests/libs/relationtest.cpp new file mode 100644 index 0000000..c00af49 --- /dev/null +++ b/autotests/libs/relationtest.cpp @@ -0,0 +1,164 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +class RelationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void testCreateFetch(); + void testMonitor(); +}; + +void RelationTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + qRegisterMetaType(); + qRegisterMetaType >(); + qRegisterMetaType(); +} + +void RelationTest::testCreateFetch() +{ + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + Item item2; + { + item2.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item2, res3, this); + AKVERIFYEXEC(append); + item2 = append->item(); + } + + Relation rel(Relation::GENERIC, item1, item2); + RelationCreateJob *createjob = new RelationCreateJob(rel, this); + AKVERIFYEXEC(createjob); + + //Test fetch & create + { + RelationFetchJob *fetchJob = new RelationFetchJob(QVector(), this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->relations().size(), 1); + QCOMPARE(fetchJob->relations().first().type(), QByteArray(Relation::GENERIC)); + } + + //Test item fetch + { + ItemFetchJob *fetchJob = new ItemFetchJob(item1); + fetchJob->fetchScope().setFetchRelations(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().relations().size(), 1); + } + + { + ItemFetchJob *fetchJob = new ItemFetchJob(item2); + fetchJob->fetchScope().setFetchRelations(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().relations().size(), 1); + } + + //Test delete + { + RelationDeleteJob *deleteJob = new RelationDeleteJob(rel, this); + AKVERIFYEXEC(deleteJob); + + RelationFetchJob *fetchJob = new RelationFetchJob(QVector(), this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->relations().size(), 0); + } +} + +void RelationTest::testMonitor() +{ + Akonadi::Monitor monitor; + monitor.setTypeMonitored(Akonadi::Monitor::Relations); + + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + Item item2; + { + item2.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item2, res3, this); + AKVERIFYEXEC(append); + item2 = append->item(); + } + + Relation rel(Relation::GENERIC, item1, item2); + + { + QSignalSpy addedSpy(&monitor, SIGNAL(relationAdded(Akonadi::Relation))); + QVERIFY(addedSpy.isValid()); + + RelationCreateJob *createjob = new RelationCreateJob(rel, this); + AKVERIFYEXEC(createjob); + + //We usually pick up signals from the previous tests as well (due to server-side notification caching) + QTRY_VERIFY(addedSpy.count() >= 1); + QTRY_COMPARE(addedSpy.last().first().value(), rel); + } + + { + QSignalSpy removedSpy(&monitor, SIGNAL(relationRemoved(Akonadi::Relation))); + QVERIFY(removedSpy.isValid()); + RelationDeleteJob *deleteJob = new RelationDeleteJob(rel, this); + AKVERIFYEXEC(deleteJob); + QTRY_VERIFY(removedSpy.count() >= 1); + QTRY_COMPARE(removedSpy.last().first().value(), rel); + } +} + +QTEST_AKONADIMAIN(RelationTest) + +#include "relationtest.moc" diff --git a/autotests/libs/resourceschedulertest.cpp b/autotests/libs/resourceschedulertest.cpp new file mode 100644 index 0000000..23a6e6f --- /dev/null +++ b/autotests/libs/resourceschedulertest.cpp @@ -0,0 +1,373 @@ +/* + Copyright (c) 2009 Thomas McGuire + + 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 "resourceschedulertest.h" + +#include "../src/agentbase/resourcescheduler_p.h" + +#include +#include + +using namespace Akonadi; + +QTEST_MAIN(ResourceSchedulerTest) + +Q_DECLARE_METATYPE(QSet) + +ResourceSchedulerTest::ResourceSchedulerTest(QObject *parent): + QObject(parent) +{ + qRegisterMetaType >(); +} + +void ResourceSchedulerTest::testTaskComparision() +{ + ResourceScheduler::Task t1; + t1.type = ResourceScheduler::ChangeReplay; + ResourceScheduler::Task t2; + t2.type = ResourceScheduler::ChangeReplay; + QCOMPARE(t1, t2); + QList taskList; + taskList.append(t1); + QVERIFY(taskList.contains(t2)); + + ResourceScheduler::Task t3; + t3.type = ResourceScheduler::DeleteResourceCollection; + QVERIFY(!(t2 == t3)); + QVERIFY(!taskList.contains(t3)); + + ResourceScheduler::Task t4; + t4.type = ResourceScheduler::Custom; + t4.receiver = this; + t4.methodName = "customTask"; + t4.argument = QStringLiteral("call1"); + + ResourceScheduler::Task t5(t4); + QVERIFY(t4 == t5); + + t5.argument = QStringLiteral("call2"); + QVERIFY(!(t4 == t5)); +} + +void ResourceSchedulerTest::testChangeReplaySchedule() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + qRegisterMetaType("Akonadi::Collection"); + QSignalSpy changeReplaySpy(&scheduler, SIGNAL(executeChangeReplay())); + QSignalSpy collectionTreeSyncSpy(&scheduler, SIGNAL(executeCollectionTreeSync())); + QSignalSpy syncSpy(&scheduler, SIGNAL(executeCollectionSync(Akonadi::Collection))); + QVERIFY(changeReplaySpy.isValid()); + QVERIFY(collectionTreeSyncSpy.isValid()); + QVERIFY(syncSpy.isValid()); + + // Schedule a change replay, it should be executed first thing when we enter the + // event loop, but not before + QVERIFY(scheduler.isEmpty()); + scheduler.scheduleChangeReplay(); + QVERIFY(!scheduler.isEmpty()); + QVERIFY(changeReplaySpy.isEmpty()); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + + // Schedule two change replays. The duplicate one should not be executed. + changeReplaySpy.clear(); + scheduler.scheduleChangeReplay(); + scheduler.scheduleChangeReplay(); + QVERIFY(changeReplaySpy.isEmpty()); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + + // Schedule a second change replay while one is in progress, should give as two signal emissions + changeReplaySpy.clear(); + scheduler.scheduleChangeReplay(); + QVERIFY(changeReplaySpy.isEmpty()); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + scheduler.scheduleChangeReplay(); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 2); + scheduler.taskDone(); + + // + // Schedule various stuff. + // + Collection collection(42); + changeReplaySpy.clear(); + scheduler.scheduleCollectionTreeSync(); + scheduler.scheduleChangeReplay(); + scheduler.scheduleSync(collection); + scheduler.scheduleChangeReplay(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 0); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + + // Omit a taskDone() here, there shouldn't be a new signal + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 1); + + // At this point, we're done, check that nothing else is emitted + scheduler.taskDone(); + QVERIFY(scheduler.isEmpty()); + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 1); +} + +void ResourceSchedulerTest::customTaskNoArg() +{ + ++mCustomCallCount; +} + +void ResourceSchedulerTest::customTask(const QVariant &argument) +{ + ++mCustomCallCount; + mLastArgument = argument; +} + +void ResourceSchedulerTest::testCustomTask() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + mCustomCallCount = 0; + + scheduler.scheduleCustomTask(this, "customTask", QStringLiteral("call1")); + scheduler.scheduleCustomTask(this, "customTask", QStringLiteral("call1")); + scheduler.scheduleCustomTask(this, "customTask", QStringLiteral("call2")); + scheduler.scheduleCustomTask(this, "customTaskNoArg", QVariant()); + + QCOMPARE(mCustomCallCount, 0); + + QTest::qWait(1); + QCOMPARE(mCustomCallCount, 1); + QCOMPARE(mLastArgument.toString(), QStringLiteral("call1")); + + scheduler.taskDone(); + QVERIFY(!scheduler.isEmpty()); + QTest::qWait(1); + QCOMPARE(mCustomCallCount, 2); + QCOMPARE(mLastArgument.toString(), QStringLiteral("call2")); + + scheduler.taskDone(); + QVERIFY(!scheduler.isEmpty()); + QTest::qWait(1); + QCOMPARE(mCustomCallCount, 3); + + scheduler.taskDone(); + QVERIFY(scheduler.isEmpty()); +} + +void ResourceSchedulerTest::testCompression() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + qRegisterMetaType("Akonadi::Collection"); + qRegisterMetaType("Akonadi::Item"); + QSignalSpy fullSyncSpy(&scheduler, SIGNAL(executeFullSync())); + QSignalSpy collectionTreeSyncSpy(&scheduler, SIGNAL(executeCollectionTreeSync())); + QSignalSpy syncSpy(&scheduler, SIGNAL(executeCollectionSync(Akonadi::Collection))); + QSignalSpy fetchSpy(&scheduler, SIGNAL(executeItemFetch(Akonadi::Item,QSet))); + QVERIFY(fullSyncSpy.isValid()); + QVERIFY(collectionTreeSyncSpy.isValid()); + QVERIFY(syncSpy.isValid()); + QVERIFY(fetchSpy.isValid()); + + // full sync + QVERIFY(scheduler.isEmpty()); + scheduler.scheduleFullSync(); + scheduler.scheduleFullSync(); + QTest::qWait(1); // start execution + QCOMPARE(fullSyncSpy.count(), 1); + scheduler.scheduleCollectionTreeSync(); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(fullSyncSpy.count(), 1); + QVERIFY(scheduler.isEmpty()); + + // collection tree sync + QVERIFY(scheduler.isEmpty()); + scheduler.scheduleCollectionTreeSync(); + scheduler.scheduleCollectionTreeSync(); + QTest::qWait(1); // start execution + QCOMPARE(collectionTreeSyncSpy.count(), 1); + scheduler.scheduleCollectionTreeSync(); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QVERIFY(scheduler.isEmpty()); + + // sync collection + scheduler.scheduleSync(Akonadi::Collection(42)); + scheduler.scheduleSync(Akonadi::Collection(42)); + QTest::qWait(1); // start execution + QCOMPARE(syncSpy.count(), 1); + scheduler.scheduleSync(Akonadi::Collection(43)); + scheduler.scheduleSync(Akonadi::Collection(42)); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(syncSpy.count(), 2); + scheduler.taskDone(); + QTest::qWait(2); + QVERIFY(scheduler.isEmpty()); + + // sync collection + scheduler.scheduleItemFetch(Akonadi::Item(42), QSet(), QDBusMessage()); + scheduler.scheduleItemFetch(Akonadi::Item(42), QSet(), QDBusMessage()); + QTest::qWait(1); // start execution + QCOMPARE(fetchSpy.count(), 1); + scheduler.scheduleItemFetch(Akonadi::Item(43), QSet(), QDBusMessage()); + scheduler.scheduleItemFetch(Akonadi::Item(42), QSet(), QDBusMessage()); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(fetchSpy.count(), 2); + scheduler.taskDone(); + QTest::qWait(2); + QVERIFY(scheduler.isEmpty()); +} + +void ResourceSchedulerTest::testSyncCompletion() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + QSignalSpy completionSpy(&scheduler, SIGNAL(fullSyncComplete())); + QVERIFY(completionSpy.isValid()); + + // sync completion does not do compression + QVERIFY(scheduler.isEmpty()); + scheduler.scheduleFullSyncCompletion(); + scheduler.scheduleFullSyncCompletion(); + QTest::qWait(1); // start execution + QCOMPARE(completionSpy.count(), 1); + scheduler.scheduleFullSyncCompletion(); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(completionSpy.count(), 2); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(completionSpy.count(), 3); + scheduler.taskDone(); + QVERIFY(scheduler.isEmpty()); +} + +void ResourceSchedulerTest::testPriorities() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + qRegisterMetaType("Akonadi::Collection"); + qRegisterMetaType("Akonadi::Item"); + QSignalSpy changeReplaySpy(&scheduler, SIGNAL(executeChangeReplay())); + QSignalSpy fullSyncSpy(&scheduler, SIGNAL(executeFullSync())); + QSignalSpy collectionTreeSyncSpy(&scheduler, SIGNAL(executeCollectionTreeSync())); + QSignalSpy syncSpy(&scheduler, SIGNAL(executeCollectionSync(Akonadi::Collection))); + QSignalSpy fetchSpy(&scheduler, SIGNAL(executeItemFetch(Akonadi::Item,QSet))); + QSignalSpy attributesSyncSpy(&scheduler, SIGNAL(executeCollectionAttributesSync(Akonadi::Collection))); + QVERIFY(changeReplaySpy.isValid()); + QVERIFY(fullSyncSpy.isValid()); + QVERIFY(collectionTreeSyncSpy.isValid()); + QVERIFY(syncSpy.isValid()); + QVERIFY(fetchSpy.isValid()); + QVERIFY(attributesSyncSpy.isValid()); + + scheduler.scheduleCollectionTreeSync(); + scheduler.scheduleChangeReplay(); + scheduler.scheduleSync(Akonadi::Collection(42)); + scheduler.scheduleItemFetch(Akonadi::Item(42), QSet(), QDBusMessage()); + scheduler.scheduleAttributesSync(Akonadi::Collection(42)); + scheduler.scheduleFullSync(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 0); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 0); + QCOMPARE(attributesSyncSpy.count(), 0); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 0); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 0); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 0); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 1); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 1); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 1); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 1); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 1); + QCOMPARE(fullSyncSpy.count(), 1); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 1); + scheduler.taskDone(); + + QVERIFY(scheduler.isEmpty()); +} + diff --git a/autotests/libs/resourceschedulertest.h b/autotests/libs/resourceschedulertest.h new file mode 100644 index 0000000..10d6082 --- /dev/null +++ b/autotests/libs/resourceschedulertest.h @@ -0,0 +1,49 @@ +/* + Copyright (c) 2009 Thomas McGuire + + 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 RESOURCESCHEDULERTEST_H +#define RESOURCESCHEDULERTEST_H + +#include +#include + +class ResourceSchedulerTest : public QObject +{ + Q_OBJECT +public: + explicit ResourceSchedulerTest(QObject *parent = 0); + +public Q_SLOTS: + void customTask(const QVariant &argument); + void customTaskNoArg(); + +private Q_SLOTS: + + void testTaskComparision(); + void testChangeReplaySchedule(); + void testCustomTask(); + void testCompression(); + void testSyncCompletion(); + void testPriorities(); + +private: + int mCustomCallCount; + QVariant mLastArgument; +}; + +#endif diff --git a/autotests/libs/resourcetest.cpp b/autotests/libs/resourcetest.cpp new file mode 100644 index 0000000..e673680 --- /dev/null +++ b/autotests/libs/resourcetest.cpp @@ -0,0 +1,99 @@ +/* + 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 +#include + +#include + +#include +#include +#include + +using namespace Akonadi; + +class ResourceTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + void testResourceManagement() + { + qRegisterMetaType(); + QSignalSpy spyAddInstance(AgentManager::self(), SIGNAL(instanceAdded(Akonadi::AgentInstance))); + QVERIFY(spyAddInstance.isValid()); + QSignalSpy spyRemoveInstance(AgentManager::self(), SIGNAL(instanceRemoved(Akonadi::AgentInstance))); + QVERIFY(spyRemoveInstance.isValid()); + + AgentType type = AgentManager::self()->type(QStringLiteral("akonadi_knut_resource")); + QVERIFY(type.isValid()); + + QStringList lst; + lst << QStringLiteral("Resource"); + QCOMPARE(type.capabilities(), lst); + + AgentInstanceCreateJob *job = new AgentInstanceCreateJob(type); + AKVERIFYEXEC(job); + + AgentInstance instance = job->instance(); + QVERIFY(instance.isValid()); + + QCOMPARE(spyAddInstance.count(), 1); + QCOMPARE(spyAddInstance.first().at(0).value(), instance); + QVERIFY(AgentManager::self()->instance(instance.identifier()).isValid()); + + job = new AgentInstanceCreateJob(type); + AKVERIFYEXEC(job); + AgentInstance instance2 = job->instance(); + QVERIFY(!(instance == instance2)); + QCOMPARE(spyAddInstance.count(), 2); + + AgentManager::self()->removeInstance(instance); + AgentManager::self()->removeInstance(instance2); + QTRY_COMPARE(spyRemoveInstance.count(), 2); + QVERIFY(!AgentManager::self()->instances().contains(instance)); + QVERIFY(!AgentManager::self()->instances().contains(instance2)); + } + + void testIllegalResourceManagement() + { + AgentInstanceCreateJob *job = new AgentInstanceCreateJob(AgentManager::self()->type(QStringLiteral("non_existing_resource"))); + QVERIFY(!job->exec()); + + // unique agent + // According to vkrause the mailthreader agent is no longer started by + // default so this won't work. + /* + const AgentType type = AgentManager::self()->type( "akonadi_mailthreader_agent" ); + QVERIFY( type.isValid() ); + job = new AgentInstanceCreateJob( type ); + AKVERIFYEXEC( job ); + + job = new AgentInstanceCreateJob( type ); + QVERIFY( !job->exec() ); + */ + } +}; + +QTEST_AKONADIMAIN(ResourceTest) + +#include "resourcetest.moc" diff --git a/autotests/libs/searchjobtest.cpp b/autotests/libs/searchjobtest.cpp new file mode 100644 index 0000000..86e13bc --- /dev/null +++ b/autotests/libs/searchjobtest.cpp @@ -0,0 +1,130 @@ +/* + Copyright (c) 2007 Volker Krause + Copyright (c) 2014 Daniel Vrátil + + 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 "searchjobtest.h" +#include "qtest_akonadi.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "collectionutils.h" + +QTEST_AKONADIMAIN(SearchJobTest) + +using namespace Akonadi; + +void SearchJobTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); +} + +void SearchJobTest::testCreateDeleteSearch() +{ + Akonadi::SearchQuery query; + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 1)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 2)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 3)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 4)); + + // Create collection + SearchCreateJob *create = new SearchCreateJob(QStringLiteral("search123456"), query, this); + create->setRemoteSearchEnabled(false); + AKVERIFYEXEC(create); + const Collection created = create->createdCollection(); + QVERIFY(created.isValid()); + + // Fetch "Search" collection, check the search collection has been created + CollectionFetchJob *list = new CollectionFetchJob(Collection(1), CollectionFetchJob::Recursive, this); + AKVERIFYEXEC(list); + const Collection::List cols = list->collections(); + Collection col; + for (const auto &c : cols) { + if (c.name() == QLatin1String("search123456")) { + col = c; + } + } + QVERIFY(col == created); + QCOMPARE(col.parentCollection().id(), 1LL); + QVERIFY(col.isVirtual()); + + // Fetch items in the search collection, check whether they are there + ItemFetchJob *fetch = new ItemFetchJob(created, this); + AKVERIFYEXEC(fetch); + const Item::List items = fetch->items(); + QCOMPARE(items.count(), 2); + + CollectionDeleteJob *delJob = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(delJob); +} + +void SearchJobTest::testModifySearch() +{ + Akonadi::SearchQuery query; + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 1)); + query.addTerm(Akonadi::SearchTerm(QLatin1String("resource"), 2)); + + // make sure there is a virtual collection + SearchCreateJob *create = new SearchCreateJob(QStringLiteral("search123456"), query, this); + AKVERIFYEXEC(create); + Collection created = create->createdCollection(); + QVERIFY(created.isValid()); + QVERIFY(created.hasAttribute()); + + auto attr = created.attribute(); + QVERIFY(!attr->isRecursive()); + QVERIFY(!attr->isRemoteSearchEnabled()); + QCOMPARE(attr->queryCollections(), QList{ 0 }); + const QString oldQueryString = attr->queryString(); + + // Change the attributes + attr->setRecursive(true); + attr->setRemoteSearchEnabled(true); + attr->setQueryCollections(QList{ 1 }); + Akonadi::SearchQuery newQuery; + newQuery.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 3)); + newQuery.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 4)); + attr->setQueryString(QString::fromUtf8(newQuery.toJSON())); + + auto modify = new CollectionModifyJob(created, this); + AKVERIFYEXEC(modify); + + auto fetch = new CollectionFetchJob(created, CollectionFetchJob::Base, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->collections().size(), 1); + + const auto col = fetch->collections().first(); + QVERIFY(col.hasAttribute()); + attr = col.attribute(); + + QVERIFY(attr->isRecursive()); + QVERIFY(attr->isRemoteSearchEnabled()); + QCOMPARE(attr->queryCollections(), QList{ 1 }); + QVERIFY(attr->queryString() != oldQueryString); + + auto delJob = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(delJob); +} diff --git a/autotests/libs/searchjobtest.h b/autotests/libs/searchjobtest.h new file mode 100644 index 0000000..7ae9707 --- /dev/null +++ b/autotests/libs/searchjobtest.h @@ -0,0 +1,34 @@ +/* + 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_SEARCHJOBTEST_H +#define AKONADI_SEARCHJOBTEST_H + +#include + +class SearchJobTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testCreateDeleteSearch(); + void testModifySearch(); +}; + +#endif diff --git a/autotests/libs/searchquerytest.cpp b/autotests/libs/searchquerytest.cpp new file mode 100644 index 0000000..7738f8e --- /dev/null +++ b/autotests/libs/searchquerytest.cpp @@ -0,0 +1,162 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "searchquery.h" +#include +#include + +using namespace Akonadi; + +class SearchQueryTest : public QObject +{ + Q_OBJECT +private: + void verifySimpleTerm(const QVariantMap &json, const SearchTerm &term, bool *ok) + { + *ok = false; + QCOMPARE(term.subTerms().count(), 0); + QVERIFY(json.contains(QStringLiteral("key"))); + QCOMPARE(json[QStringLiteral("key")].toString(), term.key()); + QVERIFY(json.contains(QStringLiteral("value"))); + QCOMPARE(json[QStringLiteral("value")], term.value()); + QVERIFY(json.contains(QStringLiteral("cond"))); + QCOMPARE(static_cast(json[QStringLiteral("cond")].toInt()), term.condition()); + QVERIFY(json.contains(QStringLiteral("negated"))); + QCOMPARE(json[QStringLiteral("negated")].toBool(), term.isNegated()); + *ok = true; + } + +private Q_SLOTS: + void testSerializer() + { + QJson::Parser parser; + bool ok = false; + + { + SearchQuery query; + query.addTerm(QStringLiteral("body"), QStringLiteral("test string"), SearchTerm::CondContains); + + ok = false; + QVariantMap map = parser.parse(query.toJSON(), &ok).toMap(); + QVERIFY(ok); + + QCOMPARE(static_cast(map[QStringLiteral("rel")].toInt()), SearchTerm::RelAnd); + const QVariantList subTerms = map[QStringLiteral("subTerms")].toList(); + QCOMPARE(subTerms.size(), 1); + + ok = false; + verifySimpleTerm(subTerms.first().toMap(), query.term().subTerms()[0], &ok); + QVERIFY(ok); + } + + { + SearchQuery query(SearchTerm::RelOr); + query.addTerm(SearchTerm(QStringLiteral("to"), QStringLiteral("test@test.user"), SearchTerm::CondEqual)); + SearchTerm term2(QStringLiteral("subject"), QStringLiteral("Hello"), SearchTerm::CondContains); + term2.setIsNegated(true); + query.addTerm(term2); + + ok = false; + QVariantMap map = parser.parse(query.toJSON(), &ok).toMap(); + QVERIFY(ok); + + QCOMPARE(static_cast(map[QStringLiteral("rel")].toInt()), query.term().relation()); + const QVariantList subTerms = map[QStringLiteral("subTerms")].toList(); + QCOMPARE(subTerms.size(), query.term().subTerms().count()); + + for (int i = 0; i < subTerms.size(); ++i) { + ok = false; + verifySimpleTerm(subTerms[i].toMap(), query.term().subTerms()[i], &ok); + QVERIFY(ok); + } + } + } + + void testParser() + { + QJson::Serializer serializer; + bool ok = false; + + { + QVariantList subTerms; + QVariantMap termJSON; + termJSON[QStringLiteral("key")] = QStringLiteral("created"); + termJSON[QStringLiteral("value")] = QDateTime(QDate(2014, 01, 24), QTime(17, 49, 00)); + termJSON[QStringLiteral("cond")] = static_cast(SearchTerm::CondGreaterOrEqual); + termJSON[QStringLiteral("negated")] = true; + subTerms << termJSON; + + termJSON[QStringLiteral("key")] = QStringLiteral("subject"); + termJSON[QStringLiteral("value")] = QStringLiteral("Hello"); + termJSON[QStringLiteral("cond")] = static_cast(SearchTerm::CondEqual); + termJSON[QStringLiteral("negated")] = false; + subTerms << termJSON; + + QVariantMap map; + map[QStringLiteral("rel")] = static_cast(SearchTerm::RelAnd); + map[QStringLiteral("subTerms")] = subTerms; + +#if !defined( USE_QJSON_0_8 ) + const QByteArray json = serializer.serialize(map); + QVERIFY(!json.isNull()); +#else + ok = false; + const QByteArray json = serializer.serialize(map, &ok); + QVERIFY(ok); +#endif + + const SearchQuery query = SearchQuery::fromJSON(json); + QVERIFY(!query.isNull()); + const SearchTerm term = query.term(); + + QCOMPARE(static_cast(map[QStringLiteral("rel")].toInt()), term.relation()); + QCOMPARE(subTerms.count(), term.subTerms().count()); + + for (int i = 0; i < subTerms.count(); ++i) { + ok = false; + verifySimpleTerm(subTerms.at(i).toMap(), term.subTerms()[i], &ok); + QVERIFY(ok); + } + } + } + + void testFullQuery() + { + { + SearchQuery query; + query.addTerm("key", "value"); + const QByteArray serialized = query.toJSON(); + QCOMPARE(SearchQuery::fromJSON(serialized), query); + } + { + SearchQuery query; + query.setLimit(10); + query.addTerm("key", "value"); + const QByteArray serialized = query.toJSON(); + QCOMPARE(SearchQuery::fromJSON(serialized), query); + } + } + +}; + +QTEST_AKONADIMAIN(SearchQueryTest, NoGUI) + +#include "searchquerytest.moc" diff --git a/autotests/libs/servermanagertest.cpp b/autotests/libs/servermanagertest.cpp new file mode 100644 index 0000000..ed983c1 --- /dev/null +++ b/autotests/libs/servermanagertest.cpp @@ -0,0 +1,96 @@ +/* + 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 +#include + +#include + +#include + +#include "test_utils.h" + +using namespace Akonadi; + +class ServerManagerTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + QVERIFY(Control::start()); + trackAkonadiProcess(false); + } + + void cleanupTestCase() + { + trackAkonadiProcess(true); + } + + void testStartStop() + { + QSignalSpy startSpy(ServerManager::self(), SIGNAL(started())); + QVERIFY(startSpy.isValid()); + QSignalSpy stopSpy(ServerManager::self(), SIGNAL(stopped())); + QVERIFY(stopSpy.isValid()); + + QVERIFY(ServerManager::isRunning()); + QVERIFY(Control::start()); + + QVERIFY(startSpy.isEmpty()); + QVERIFY(stopSpy.isEmpty()); + + { + QSignalSpy spy(ServerManager::self(), SIGNAL(stopped())); + QVERIFY(ServerManager::stop()); + QTRY_VERIFY(spy.count() >= 1); + } + QVERIFY(!ServerManager::isRunning()); + QVERIFY(startSpy.isEmpty()); + QCOMPARE(stopSpy.count(), 1); + + QVERIFY(!ServerManager::stop()); + { + QSignalSpy spy(ServerManager::self(), SIGNAL(started())); + QVERIFY(ServerManager::start()); + QTRY_VERIFY(spy.count() >= 1); + } + QVERIFY(ServerManager::isRunning()); + QCOMPARE(startSpy.count(), 1); + QCOMPARE(stopSpy.count(), 1); + } + + void testRestart() + { + QVERIFY(ServerManager::isRunning()); + QSignalSpy startSpy(ServerManager::self(), SIGNAL(started())); + QVERIFY(startSpy.isValid()); + + QVERIFY(Control::restart()); + + QVERIFY(ServerManager::isRunning()); + QCOMPARE(startSpy.count(), 1); + } + +}; + +QTEST_AKONADIMAIN(ServerManagerTest) + +#include "servermanagertest.moc" diff --git a/autotests/libs/sharedvaluepooltest.cpp b/autotests/libs/sharedvaluepooltest.cpp new file mode 100644 index 0000000..93d5f84 --- /dev/null +++ b/autotests/libs/sharedvaluepooltest.cpp @@ -0,0 +1,91 @@ +/* + Copyright (c) 2010 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 "../sharedvaluepool_p.h" +#include + +#include +#include +#include +#include + +using namespace Akonadi; + +class SharedValuePoolTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testQVector_data() + { + QTest::addColumn("size"); + QTest::newRow("10") << 10; + QTest::newRow("100") << 100; + } + + void testQVector() + { + QFETCH(int, size); + QVector data; + Internal::SharedValuePool pool; + + for (int i = 0; i < size; ++i) { + QByteArray b(10, (char)i); + data.push_back(b); + QCOMPARE(pool.sharedValue(b), b); + QCOMPARE(pool.sharedValue(b), b); + } + + QBENCHMARK { + foreach (const QByteArray &b, data) + { + pool.sharedValue(b); + } + } + } + + /*void testQSet_data() + { + QTest::addColumn( "size" ); + QTest::newRow( "10" ) << 10; + QTest::newRow( "100" ) << 100; + } + + void testQSet() + { + QFETCH( int, size ); + QVector data; + Internal::SharedValuePool pool; + + for ( int i = 0; i < size; ++i ) { + QByteArray b( 10, (char)i ); + data.push_back( b ); + QCOMPARE( pool.sharedValue( b ), b ); + QCOMPARE( pool.sharedValue( b ), b ); + } + + QBENCHMARK { + foreach ( const QByteArray &b, data ) + pool.sharedValue( b ); + } + }*/ +}; + +QTEST_MAIN(SharedValuePoolTest) + +#include "sharedvaluepooltest.moc" diff --git a/autotests/libs/subscriptiontest.cpp b/autotests/libs/subscriptiontest.cpp new file mode 100644 index 0000000..bc48c8b --- /dev/null +++ b/autotests/libs/subscriptiontest.cpp @@ -0,0 +1,94 @@ +/* + 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 "test_utils.h" + +#include +#include +#include +#include +#include + +#include + +#include + +using namespace Akonadi; + +class SubscriptionTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + } + + void testSubscribe() + { + Collection::List l; + l << Collection(collectionIdFromPath(QStringLiteral("res2/foo2"))); + QVERIFY(l.first().isValid()); + SubscriptionJob *sjob = new SubscriptionJob(this); + sjob->unsubscribe(l); + AKVERIFYEXEC(sjob); + + const Collection res2Col = Collection(collectionIdFromPath(QStringLiteral("res2"))); + QVERIFY(res2Col.isValid()); + CollectionFetchJob *ljob = new CollectionFetchJob(res2Col, CollectionFetchJob::FirstLevel, this); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 1); + + ljob = new CollectionFetchJob(res2Col, CollectionFetchJob::FirstLevel, this); + ljob->fetchScope().setListFilter(CollectionFetchScope::NoFilter); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 2); + + sjob = new SubscriptionJob(this); + sjob->subscribe(l); + AKVERIFYEXEC(sjob); + + ljob = new CollectionFetchJob(res2Col, CollectionFetchJob::FirstLevel, this); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 2); + } + + void testEmptySubscribe() + { + Collection::List l; + SubscriptionJob *sjob = new SubscriptionJob(this); + AKVERIFYEXEC(sjob); + } + + void testInvalidSubscribe() + { + Collection::List l; + l << Collection(1); + SubscriptionJob *sjob = new SubscriptionJob(this); + sjob->subscribe(l); + l << Collection(INT_MAX); + sjob->unsubscribe(l); + QVERIFY(!sjob->exec()); + } +}; + +QTEST_AKONADIMAIN(SubscriptionTest) + +#include "subscriptiontest.moc" diff --git a/autotests/libs/tagmodeltest.cpp b/autotests/libs/tagmodeltest.cpp new file mode 100644 index 0000000..9c7244b --- /dev/null +++ b/autotests/libs/tagmodeltest.cpp @@ -0,0 +1,382 @@ +/* + * Copyright 2015 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include "fakeserverdata.h" +#include "fakesession.h" +#include "fakemonitor.h" +#include "modelspy.h" + +#include "tagmodel.h" +#include "tagmodel_p.h" + + +static const QString serverContent1 = QStringLiteral( + "- T PLAIN 'Tag 1' 4" + "- - T PLAIN 'Tag 2' 3" + "- - - T PLAIN 'Tag 4' 1" + "- - T PLAIN 'Tag 3' 2" + "- T PLAIN 'Tag 5' 5"); + + +class TagModelTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + + void testInitialFetch(); + + void testTagAdded_data(); + void testTagAdded(); + + void testTagChanged_data(); + void testTagChanged(); + + void testTagRemoved_data(); + void testTagRemoved(); + + void testTagMoved_data(); + void testTagMoved(); + +private: + ExpectedSignal getExpectedSignal(SignalType type, int start, int end, const QVariantList newData) + { + return getExpectedSignal(type, start, end, QVariant(), newData); + } + + ExpectedSignal getExpectedSignal(SignalType type, int start, int end, const QVariant &parentData = QVariant(), const QVariantList newData = QVariantList()) + { + ExpectedSignal expectedSignal; + expectedSignal.signalType = type; + expectedSignal.startRow = start; + expectedSignal.endRow = end; + expectedSignal.parentData = parentData; + expectedSignal.newData = newData; + return expectedSignal; + } + + ExpectedSignal getExpectedSignal(SignalType type, int start, int end, const QVariant &sourceParentData, int destRow, const QVariant &destParentData, const QVariantList newData) + { + ExpectedSignal expectedSignal; + expectedSignal.signalType = type; + expectedSignal.startRow = start; + expectedSignal.endRow = end; + expectedSignal.sourceParentData = sourceParentData; + expectedSignal.destRow = destRow; + expectedSignal.parentData = destParentData; + expectedSignal.newData = newData; + return expectedSignal; + } + + QPair populateModel(const QString &serverContent) + { + FakeMonitor *fakeMonitor = new FakeMonitor(this); + + fakeMonitor->setSession(m_fakeSession); + fakeMonitor->setCollectionMonitored(Collection::root()); + fakeMonitor->setTypeMonitored(Akonadi::Monitor::Tags); + + TagModel *model = new TagModel(fakeMonitor, this); + + m_modelSpy = new ModelSpy(this); + m_modelSpy->setModel(model); + + FakeServerData *serverData = new FakeServerData(model, m_fakeSession, fakeMonitor); + QList initialFetchResponse = FakeJobResponse::interpret(serverData, serverContent); + serverData->setCommands(initialFetchResponse); + + // Give the model a chance to populate + QTest::qWait(100); + return qMakePair(serverData, model); + } + +private: + ModelSpy *m_modelSpy; + FakeSession *m_fakeSession; + QByteArray m_sessionName; +}; + +void TagModelTest::initTestCase() +{ + m_sessionName = "TagModelTest fake session"; + m_fakeSession = new FakeSession(m_sessionName, FakeSession::EndJobsImmediately); + m_fakeSession->setAsDefaultSession(); + + qRegisterMetaType("QModelIndex"); +} + +void TagModelTest::testInitialFetch() +{ + FakeMonitor *fakeMonitor = new FakeMonitor(this); + + fakeMonitor->setSession(m_fakeSession); + fakeMonitor->setCollectionMonitored(Collection::root()); + TagModel *model = new TagModel(fakeMonitor, this); + + FakeServerData *serverData = new FakeServerData(model, m_fakeSession, fakeMonitor); + QList initialFetchResponse = FakeJobResponse::interpret(serverData, serverContent1); + serverData->setCommands(initialFetchResponse); + + m_modelSpy = new ModelSpy(this); + m_modelSpy->setModel(model); + m_modelSpy->startSpying(); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 0); + expectedSignals << getExpectedSignal(RowsInserted, 0, 0); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 0, QStringLiteral("Tag 1")); + expectedSignals << getExpectedSignal(RowsInserted, 0, 0, QStringLiteral("Tag 1")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 1, 1, QStringLiteral("Tag 1")); + expectedSignals << getExpectedSignal(RowsInserted, 1, 1, QStringLiteral("Tag 1")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 0, 0, QStringLiteral("Tag 2")); + expectedSignals << getExpectedSignal(RowsInserted, 0, 0, QStringLiteral("Tag 2")); + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, 1, 1); + expectedSignals << getExpectedSignal(RowsInserted, 1, 1); + + m_modelSpy->setExpectedSignals(expectedSignals); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(10); + + // We get all the signals we expected. + QTRY_VERIFY(m_modelSpy->expectedSignals().isEmpty()); + + QTest::qWait(10); + // We didn't get signals we didn't expect. + QVERIFY(m_modelSpy->isEmpty()); +} + + +void TagModelTest::testTagAdded_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("addedTag"); + QTest::addColumn("parentTag"); + + QTest::newRow("add-tag01") << serverContent1 << "new Tag" << "Tag 1"; + QTest::newRow("add-tag02") << serverContent1 << "new Tag" << "Tag 2"; + QTest::newRow("add-tag03") << serverContent1 << "new Tag" << "Tag 3"; + QTest::newRow("add-tag04") << serverContent1 << "new Tag" << "Tag 4"; + QTest::newRow("add-tag05") << serverContent1 << "new Tag" << "Tag 5"; +} + +void TagModelTest::testTagAdded() +{ + QFETCH(QString, serverContent); + QFETCH(QString, addedTag); + QFETCH(QString, parentTag); + + QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::TagModel *model = testDrivers.second; + + const QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, parentTag, 1, Qt::MatchRecursive); + QVERIFY(!list.isEmpty()); + const QModelIndex parentIndex = list.first(); + const int newRow = model->rowCount(parentIndex); + + + FakeTagAddedCommand *addCommand = new FakeTagAddedCommand(addedTag, parentTag, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << addCommand); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(RowsAboutToBeInserted, newRow, newRow, parentTag, QVariantList() << addedTag); + expectedSignals << getExpectedSignal(RowsInserted, newRow, newRow, parentTag, QVariantList() << addedTag); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void TagModelTest::testTagChanged_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("tagName"); + + QTest::newRow("change-tag01") << serverContent1 << "Tag 1"; + QTest::newRow("change-tag02") << serverContent1 << "Tag 2"; + QTest::newRow("change-tag03") << serverContent1 << "Tag 3"; + QTest::newRow("change-tag04") << serverContent1 << "Tag 4"; + QTest::newRow("change-tag05") << serverContent1 << "Tag 5"; +} + +void TagModelTest::testTagChanged() +{ + QFETCH(QString, serverContent); + QFETCH(QString, tagName); + + const QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::TagModel *model = testDrivers.second; + + const QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, tagName, 1, Qt::MatchRecursive); + QVERIFY(!list.isEmpty()); + const QModelIndex changedIndex = list.first(); + const QString parentTag = changedIndex.parent().data().toString(); + const int changedRow = changedIndex.row(); + + FakeTagChangedCommand *changeCommand = new FakeTagChangedCommand(tagName, parentTag, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << changeCommand); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(DataChanged, changedRow, changedRow, parentTag, QVariantList() << tagName); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void TagModelTest::testTagRemoved_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("removedTag"); + + QTest::newRow("remove-tag01") << serverContent1 << "Tag 1"; + QTest::newRow("remove-tag02") << serverContent1 << "Tag 2"; + QTest::newRow("remove-tag03") << serverContent1 << "Tag 3"; + QTest::newRow("remove-tag04") << serverContent1 << "Tag 4"; + QTest::newRow("remove-tag05") << serverContent1 << "Tag 5"; +} + + +void TagModelTest::testTagRemoved() +{ + QFETCH(QString, serverContent); + QFETCH(QString, removedTag); + + const QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::TagModel *model = testDrivers.second; + + const QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, removedTag, 1, Qt::MatchRecursive); + QVERIFY(!list.isEmpty()); + const QModelIndex removedIndex = list.first(); + const QString parentTag = removedIndex.parent().data().toString(); + const int sourceRow = removedIndex.row(); + + + FakeTagRemovedCommand *removeCommand = new FakeTagRemovedCommand(removedTag, parentTag, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << removeCommand); + + QList expectedSignals; + + expectedSignals << getExpectedSignal(RowsAboutToBeRemoved, sourceRow, sourceRow, + parentTag.isEmpty() ? QVariant() : parentTag, + QVariantList() << removedTag); + expectedSignals << getExpectedSignal(RowsRemoved, sourceRow, sourceRow, + parentTag.isEmpty() ? QVariant() : parentTag, + QVariantList() << removedTag); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void TagModelTest::testTagMoved_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("changedTag"); + QTest::addColumn("newParent"); + + QTest::newRow("move-tag01") << serverContent1 << "Tag 1" << "Tag 5"; + QTest::newRow("move-tag02") << serverContent1 << "Tag 2" << "Tag 5"; + QTest::newRow("move-tag03") << serverContent1 << "Tag 3" << "Tag 4"; + QTest::newRow("move-tag04") << serverContent1 << "Tag 4" << "Tag 1"; + QTest::newRow("move-tag05") << serverContent1 << "Tag 5" << "Tag 2"; + QTest::newRow("move-tag06") << serverContent1 << "Tag 3" << QString(); + QTest::newRow("move-tag07") << serverContent1 << "Tag 2" << QString(); +} + +void TagModelTest::testTagMoved() +{ + QFETCH(QString, serverContent); + QFETCH(QString, changedTag); + QFETCH(QString, newParent); + + const QPair testDrivers = populateModel(serverContent); + FakeServerData *serverData = testDrivers.first; + Akonadi::TagModel *model = testDrivers.second; + + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, changedTag, 1, Qt::MatchRecursive); + QVERIFY(!list.isEmpty()); + const QModelIndex changedIndex = list.first(); + const QString parentTag = changedIndex.parent().data().toString(); + const int sourceRow = changedIndex.row(); + + QModelIndex newParentIndex; + if (!newParent.isEmpty()) { + list = model->match(model->index(0, 0), Qt::DisplayRole, newParent, 1, Qt::MatchRecursive); + QVERIFY(!list.isEmpty()); + newParentIndex = list.first(); + } + const int destRow = model->rowCount(newParentIndex); + + FakeTagMovedCommand *moveCommand = new FakeTagMovedCommand(changedTag, parentTag, newParent, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << moveCommand); + + QList expectedSignals; + expectedSignals << getExpectedSignal(RowsAboutToBeMoved, + sourceRow, sourceRow, parentTag.isEmpty() ? QVariant() : parentTag, + destRow, newParent.isEmpty() ? QVariant() : newParent, + QVariantList() << changedTag); + expectedSignals << getExpectedSignal(RowsMoved, + sourceRow, sourceRow, parentTag.isEmpty() ? QVariant() : parentTag, + destRow, newParent.isEmpty() ? QVariant() : newParent, + QVariantList() << changedTag); + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + + +#include "tagmodeltest.moc" + +QTEST_MAIN(TagModelTest) \ No newline at end of file diff --git a/autotests/libs/tagselectwidgettest.cpp b/autotests/libs/tagselectwidgettest.cpp new file mode 100644 index 0000000..2e75778 --- /dev/null +++ b/autotests/libs/tagselectwidgettest.cpp @@ -0,0 +1,40 @@ +/* + Copyright (c) 2015 Montel Laurent + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "tagselectwidgettest.h" +#include "../src/widgets/tagselectwidget.h" +#include "../src/widgets/tageditwidget.h" +#include +TagSelectWidgetTest::TagSelectWidgetTest(QObject *parent) + : QObject(parent) +{ + +} + +TagSelectWidgetTest::~TagSelectWidgetTest() +{ + +} + +void TagSelectWidgetTest::shouldHaveDefaultValue() +{ + Akonadi::TagSelectWidget widget; + Akonadi::TagEditWidget *edit = widget.findChild(QStringLiteral("tageditwidget")); + QVERIFY(edit); +} + +QTEST_MAIN(TagSelectWidgetTest) diff --git a/autotests/libs/tagselectwidgettest.h b/autotests/libs/tagselectwidgettest.h new file mode 100644 index 0000000..482ba6b --- /dev/null +++ b/autotests/libs/tagselectwidgettest.h @@ -0,0 +1,33 @@ +/* + Copyright (c) 2015 Montel Laurent + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef TAGSELECTWIDGETTEST_H +#define TAGSELECTWIDGETTEST_H + +#include + +class TagSelectWidgetTest : public QObject +{ + Q_OBJECT +public: + explicit TagSelectWidgetTest(QObject *parent = 0); + ~TagSelectWidgetTest(); +private Q_SLOTS: + void shouldHaveDefaultValue(); +}; + +#endif // TAGSELECTWIDGETTEST_H diff --git a/autotests/libs/tagsynctest.cpp b/autotests/libs/tagsynctest.cpp new file mode 100644 index 0000000..89cc2e5 --- /dev/null +++ b/autotests/libs/tagsynctest.cpp @@ -0,0 +1,263 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#include "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace Akonadi; + +class TagSyncTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + cleanTags(); + } + + Tag::List getTags() + { + TagFetchJob *fetchJob = new TagFetchJob(); + bool ret = fetchJob->exec(); + Q_ASSERT(ret); + return fetchJob->tags(); + } + + Tag::List getTagsWithRid() + { + Tag::List tags; + Q_FOREACH (const Tag &t, getTags()) { + if (!t.remoteId().isEmpty()) { + tags << t; + qDebug() << t.remoteId(); + } + } + return tags; + } + + void cleanTags() + { + Q_FOREACH (const Tag &t, getTags()) { + TagDeleteJob *job = new TagDeleteJob(t); + bool ret = job->exec(); + Q_ASSERT(ret); + } + } + + void newTag() + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + Tag::List remoteTags; + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + remoteTags << tag1; + + TagSync *syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(QHash()); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTags(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + cleanTags(); + } + + void newTagWithItems() + { + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_2")); + AKVERIFYEXEC(select); + + Tag::List remoteTags; + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + remoteTags << tag1; + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + item1.setRemoteId(QStringLiteral("item1")); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + QHash tagMembers; + tagMembers.insert(QString::fromLatin1(tag1.remoteId()), Item::List() << item1); + + TagSync *syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(tagMembers); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTags(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + + //We need the id of the fetch + tag1 = resultTags.first(); + + ItemFetchJob *fetchJob = new ItemFetchJob(tag1); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().count(), tagMembers.value(QString::fromLatin1(tag1.remoteId())).count()); + QCOMPARE(fetchJob->items(), tagMembers.value(QString::fromLatin1(tag1.remoteId()))); + + cleanTags(); + } + + void existingTag() + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + + TagCreateJob *createJob = new TagCreateJob(tag1, this); + AKVERIFYEXEC(createJob); + + Tag::List remoteTags; + remoteTags << tag1; + + TagSync *syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(QHash()); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTags(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + cleanTags(); + } + + void existingTagWithItems() + { + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_2")); + AKVERIFYEXEC(select); + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + + TagCreateJob *createJob = new TagCreateJob(tag1, this); + AKVERIFYEXEC(createJob); + + Tag::List remoteTags; + remoteTags << tag1; + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + item1.setRemoteId(QStringLiteral("item1")); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + Item item2; + { + item2.setMimeType(QStringLiteral("application/octet-stream")); + item2.setRemoteId(QStringLiteral("item2")); + item2.setTag(tag1); + ItemCreateJob *append = new ItemCreateJob(item2, res3, this); + AKVERIFYEXEC(append); + item2 = append->item(); + } + + QHash tagMembers; + tagMembers.insert(QString::fromLatin1(tag1.remoteId()), Item::List() << item1); + + TagSync *syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(tagMembers); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTags(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + { + ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().count(), 1); + } + { + ItemFetchJob *fetchJob = new ItemFetchJob(item2, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().count(), 0); + } + + cleanTags(); + } + + void removeTag() + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + + TagCreateJob *createJob = new TagCreateJob(tag1, this); + AKVERIFYEXEC(createJob); + + Tag::List remoteTags; + + TagSync *syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(QHash()); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTagsWithRid(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + cleanTags(); + } +}; + +QTEST_AKONADIMAIN(TagSyncTest) + +#include "tagsynctest.moc" diff --git a/autotests/libs/tagtest.cpp b/autotests/libs/tagtest.cpp new file mode 100644 index 0000000..ec1476d --- /dev/null +++ b/autotests/libs/tagtest.cpp @@ -0,0 +1,822 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +class TagTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void testTag(); + void testCreateFetch(); + void testRID(); + void testDelete(); + void testDeleteRIDIsolation(); + void testModify(); + void testModifyFromResource(); + void testCreateMerge(); + void testAttributes(); + void testTagItem(); + void testCreateItem(); + void testRIDIsolation(); + void testFetchTagIdWithItem(); + void testFetchFullTagWithItem(); + void testModifyItemWithTagByGID(); + void testModifyItemWithTagByRID(); + void testMonitor(); + void testFetchItemsByTag(); +}; + +void TagTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + AttributeFactory::registerAttribute(); + qRegisterMetaType(); + qRegisterMetaType >(); + qRegisterMetaType(); + + // Delete the default Knut tag - it's interfering with this test + TagFetchJob *fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->tags().first(), this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testTag() +{ + Tag tag1; + Tag tag2; + + // Invalid tags are equal + QVERIFY(tag1 == tag2); + + // Invalid tags with different GIDs are not equal + tag1.setGid("GID1"); + QVERIFY(tag1 != tag2); + tag2.setGid("GID2"); + QVERIFY(tag1 != tag2); + + // Invalid tags with equal GIDs are equal + tag1.setGid("GID2"); + QVERIFY(tag1 == tag2); + + // Valid tags with different IDs are not equal + tag1 = Tag(1); + tag2 = Tag(2); + QVERIFY(tag1 != tag2); + + // Valid tags with different IDs and equal GIDs are still not equal + tag1.setGid("GID1"); + tag2.setGid("GID1"); + QVERIFY(tag1 != tag2); + + // Valid tags with equal ID are equal regardless of GIDs + tag2 = Tag(1); + tag2.setGid("GID2"); + QVERIFY(tag1 == tag2); +} + +void TagTest::testCreateFetch() +{ + Tag tag; + tag.setGid("gid"); + tag.setType("mytype"); + TagCreateJob *createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + + { + TagFetchJob *fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); + QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); + + TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->tags().first(), this); + AKVERIFYEXEC(deleteJob); + } + + { + TagFetchJob *fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 0); + } +} + +void TagTest::testRID() +{ + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + Tag tag; + tag.setGid("gid"); + tag.setType("mytype"); + tag.setRemoteId("rid"); + TagCreateJob *createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + + { + TagFetchJob *fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); + QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); + QCOMPARE(fetchJob->tags().first().remoteId(), QByteArray("rid")); + + TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->tags().first(), this); + AKVERIFYEXEC(deleteJob); + } + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } +} + +void TagTest::testRIDIsolation() +{ + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + + Tag tag; + tag.setGid("gid"); + tag.setType("mytype"); + tag.setRemoteId("rid_0"); + + TagCreateJob *createJob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createJob); + QVERIFY(createJob->tag().isValid()); + + qint64 tagId; + { + TagFetchJob *fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + Q_FOREACH (const Tag &tag, fetchJob->tags()) { + qDebug() << tag.gid(); + } + QCOMPARE(fetchJob->tags().count(), 1); + QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); + QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); + QCOMPARE(fetchJob->tags().first().remoteId(), QByteArray("rid_0")); + tagId = fetchJob->tags().first().id(); + } + + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_1")); + AKVERIFYEXEC(select); + } + + tag.setRemoteId("rid_1"); + createJob = new TagCreateJob(tag, this); + createJob->setMergeIfExisting(true); + AKVERIFYEXEC(createJob); + QVERIFY(createJob->tag().isValid()); + + { + TagFetchJob *fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().count(), 1); + QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); + QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); + QCOMPARE(fetchJob->tags().first().remoteId(), QByteArray("rid_1")); + + QCOMPARE(fetchJob->tags().first().id(), tagId); + + } + + TagDeleteJob *deleteJob = new TagDeleteJob(Tag(tagId), this); + AKVERIFYEXEC(deleteJob); + + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } +} + +void TagTest::testDelete() +{ + Akonadi::Monitor monitor; + monitor.setTypeMonitored(Monitor::Tags); + QSignalSpy spy(&monitor, SIGNAL(tagRemoved(Akonadi::Tag))); + + Tag tag1; + { + tag1.setGid("tag1"); + TagCreateJob *createjob = new TagCreateJob(tag1, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag1 = createjob->tag(); + } + Tag tag2; + { + tag2.setGid("tag2"); + TagCreateJob *createjob = new TagCreateJob(tag2, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag2 = createjob->tag(); + } + { + TagDeleteJob *deleteJob = new TagDeleteJob(tag1, this); + AKVERIFYEXEC(deleteJob); + } + + { + TagFetchJob *fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QCOMPARE(fetchJob->tags().first().gid(), tag2.gid()); + } + { + TagDeleteJob *deleteJob = new TagDeleteJob(tag2, this); + AKVERIFYEXEC(deleteJob); + } + + // Collect Remove notification, so that they don't interfere with testDeleteRIDIsolation + QTRY_VERIFY(!spy.isEmpty()); +} + +void TagTest::testDeleteRIDIsolation() +{ + Tag tag; + tag.setGid("gid"); + tag.setType("mytype"); + tag.setRemoteId("rid_0"); + + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + TagCreateJob *createJob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createJob); + QVERIFY(createJob->tag().isValid()); + tag.setId(createJob->tag().id()); + } + + tag.setRemoteId("rid_1"); + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_1")); + AKVERIFYEXEC(select); + + TagCreateJob *createJob = new TagCreateJob(tag, this); + createJob->setMergeIfExisting(true); + AKVERIFYEXEC(createJob); + QVERIFY(createJob->tag().isValid()); + } + + Akonadi::Monitor monitor; + monitor.setTypeMonitored(Akonadi::Monitor::Tags); + QSignalSpy signalSpy(&monitor, SIGNAL(tagRemoved(Akonadi::Tag))); + + TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); + + // Other tests notifications might interfere due to notification compression on server + QTRY_VERIFY(signalSpy.count() >= 1); + + Tag removedTag; + while (!signalSpy.isEmpty()) { + const Tag t = signalSpy.takeFirst().takeFirst().value(); + if (t.id() == tag.id()) { + removedTag = t; + break; + } + } + + QVERIFY(removedTag.isValid()); + QVERIFY(removedTag.remoteId().isEmpty()); + + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral(""), this); + AKVERIFYEXEC(select); + } +} + +void TagTest::testModify() +{ + Tag tag; + { + tag.setGid("gid"); + TagCreateJob *createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag = createjob->tag(); + } + + //We can add an attribute + { + Akonadi::TagAttribute *attr = tag.attribute(Tag::AddIfMissing); + attr->setDisplayName(QStringLiteral("display name")); + tag.addAttribute(attr); + tag.setParent(Tag(0)); + tag.setType("mytype"); + TagModifyJob *modJob = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob); + + TagFetchJob *fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QVERIFY(fetchJob->tags().first().hasAttribute()); + } + //We can update an attribute + { + Akonadi::TagAttribute *attr = tag.attribute(Tag::AddIfMissing); + attr->setDisplayName(QStringLiteral("display name2")); + TagModifyJob *modJob = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob); + + TagFetchJob *fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QVERIFY(fetchJob->tags().first().hasAttribute()); + QCOMPARE(fetchJob->tags().first().attribute()->displayName(), attr->displayName()); + } + //We can clear an attribute + { + tag.removeAttribute(); + TagModifyJob *modJob = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob); + + TagFetchJob *fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QVERIFY(!fetchJob->tags().first().hasAttribute()); + } + + TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testModifyFromResource() +{ + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + Tag tag; + { + tag.setGid("gid"); + tag.setRemoteId("rid"); + TagCreateJob *createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag = createjob->tag(); + } + + { + tag.setRemoteId(QByteArray("")); + TagModifyJob *modJob = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob); + + // The tag is removed on the server, because we just removed the last + // RemoteID + TagFetchJob *fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 0); + } +} + +void TagTest::testCreateMerge() +{ + Tag tag; + { + tag.setGid("gid"); + TagCreateJob *createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag = createjob->tag(); + } + { + Tag tag2; + tag2.setGid("gid"); + TagCreateJob *createjob = new TagCreateJob(tag2, this); + createjob->setMergeIfExisting(true); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + QCOMPARE(createjob->tag().id(), tag.id()); + } + + TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testAttributes() +{ + Tag tag; + { + tag.setGid("gid2"); + TagAttribute *attr = tag.attribute(Tag::AddIfMissing); + attr->setDisplayName(QStringLiteral("name")); + attr->setInToolbar(true); + tag.addAttribute(attr); + TagCreateJob *createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag = createjob->tag(); + + { + TagFetchJob *fetchJob = new TagFetchJob(createjob->tag(), this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QVERIFY(fetchJob->tags().first().hasAttribute()); + //we need to clone because the returned attribute is just a reference and destroyed on the next line + //FIXME we should find a better solution for this (like returning a smart pointer or value object) + QScopedPointer tagAttr(fetchJob->tags().first().attribute()->clone()); + QVERIFY(tagAttr); + QCOMPARE(tagAttr->displayName(), QStringLiteral("name")); + QCOMPARE(tagAttr->inToolbar(), true); + } + } + //Try fetching multiple items + Tag tag2; + { + tag2.setGid("gid22"); + TagAttribute *attr = tag.attribute(Tag::AddIfMissing)->clone(); + attr->setDisplayName(QStringLiteral("name2")); + attr->setInToolbar(true); + tag2.addAttribute(attr); + TagCreateJob *createjob = new TagCreateJob(tag2, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag2 = createjob->tag(); + + { + TagFetchJob *fetchJob = new TagFetchJob(Tag::List() << tag << tag2, this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 2); + QVERIFY(fetchJob->tags().at(0).hasAttribute()); + QVERIFY(fetchJob->tags().at(1).hasAttribute()); + } + } + + TagDeleteJob *deleteJob = new TagDeleteJob(Tag::List() << tag << tag2, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testTagItem() +{ + Akonadi::Monitor monitor; + monitor.itemFetchScope().setFetchTags(true); + monitor.setAllMonitored(true); + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + item1.setTag(tag); + + QSignalSpy tagsSpy(&monitor, SIGNAL(itemsTagsChanged(Akonadi::Item::List,QSet,QSet))); + QVERIFY(tagsSpy.isValid()); + + ItemModifyJob *modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + QTRY_VERIFY(tagsSpy.count() >= 1); + QTRY_COMPARE(tagsSpy.last().first().value().first().id(), item1.id()); + QTRY_COMPARE(tagsSpy.last().at(1).value< QSet >().size(), 1); //1 added tag + + ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + + TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testCreateItem() +{ + // Akonadi::Monitor monitor; + // monitor.itemFetchScope().setFetchTags(true); + // monitor.setAllMonitored(true); + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + // QSignalSpy tagsSpy(&monitor, SIGNAL(itemsTagsChanged(Akonadi::Item::List,QSet,QSet))); + // QVERIFY(tagsSpy.isValid()); + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + item1.setTag(tag); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + // QTRY_VERIFY(tagsSpy.count() >= 1); + // QTest::qWait(10); + // kDebug() << tagsSpy.count(); + // QTRY_COMPARE(tagsSpy.last().first().value().first().id(), item1.id()); + // QTRY_COMPARE(tagsSpy.last().at(1).value< QSet >().size(), 1); //1 added tag + + ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + + TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testFetchTagIdWithItem() +{ + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + item1.setTag(tag); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + fetchJob->fetchScope().tagFetchScope().setFetchIdOnly(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + Tag t = fetchJob->items().first().tags().first(); + QCOMPARE(t.id(), tag.id()); + QVERIFY(t.gid().isEmpty()); + + TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testFetchFullTagWithItem() +{ + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + //FIXME This should also be possible with create, but isn't + item1.setTag(tag); + } + + ItemModifyJob *modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + fetchJob->fetchScope().tagFetchScope().setFetchIdOnly(false); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + Tag t = fetchJob->items().first().tags().first(); + QCOMPARE(t, tag); + QVERIFY(!t.gid().isEmpty()); + + TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testModifyItemWithTagByGID() +{ + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + { + Tag tag; + tag.setGid("gid2"); + TagCreateJob *createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + Tag tag; + tag.setGid("gid2"); + item1.setTag(tag); + + ItemModifyJob *modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + + TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->items().first().tags().first(), this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testModifyItemWithTagByRID() +{ + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + Tag tag3; + { + tag3.setGid("gid3"); + tag3.setRemoteId("rid3"); + TagCreateJob *createjob = new TagCreateJob(tag3, this); + AKVERIFYEXEC(createjob); + tag3 = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + Tag tag; + tag.setRemoteId("rid2"); + item1.setTag(tag); + + ItemModifyJob *modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + + { + TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->items().first().tags().first(), this); + AKVERIFYEXEC(deleteJob); + } + + { + TagDeleteJob *deleteJob = new TagDeleteJob(tag3, this); + AKVERIFYEXEC(deleteJob); + } + + { + ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } +} + +void TagTest::testMonitor() +{ + Akonadi::Monitor monitor; + monitor.setTypeMonitored(Akonadi::Monitor::Tags); + monitor.tagFetchScope().fetchAttribute(); + + Akonadi::Tag createdTag; + { + QSignalSpy addedSpy(&monitor, SIGNAL(tagAdded(Akonadi::Tag))); + QVERIFY(addedSpy.isValid()); + Tag tag; + tag.setGid("gid2"); + tag.setName(QStringLiteral("name2")); + tag.setType("type2"); + TagCreateJob *createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + createdTag = createjob->tag(); + //We usually pick up signals from the previous tests as well (due to server-side notification caching) + QTRY_VERIFY(addedSpy.count() >= 1); + QTRY_COMPARE(addedSpy.last().first().value().id(), createdTag.id()); + QVERIFY(addedSpy.last().first().value().hasAttribute()); + } + + { + QSignalSpy modifedSpy(&monitor, SIGNAL(tagChanged(Akonadi::Tag))); + QVERIFY(modifedSpy.isValid()); + createdTag.setName(QStringLiteral("name3")); + + TagModifyJob *modJob = new TagModifyJob(createdTag, this); + AKVERIFYEXEC(modJob); + //We usually pick up signals from the previous tests as well (due to server-side notification caching) + QTRY_VERIFY(modifedSpy.count() >= 1); + QTRY_COMPARE(modifedSpy.last().first().value().id(), createdTag.id()); + QVERIFY(modifedSpy.last().first().value().hasAttribute()); + } + + { + QSignalSpy removedSpy(&monitor, SIGNAL(tagRemoved(Akonadi::Tag))); + QVERIFY(removedSpy.isValid()); + TagDeleteJob *deletejob = new TagDeleteJob(createdTag, this); + AKVERIFYEXEC(deletejob); + QTRY_VERIFY(removedSpy.count() >= 1); + QTRY_COMPARE(removedSpy.last().first().value().id(), createdTag.id()); + } +} + +void TagTest::testFetchItemsByTag() +{ + const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + ItemCreateJob *append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + //FIXME This should also be possible with create, but isn't + item1.setTag(tag); + } + + ItemModifyJob *modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + ItemFetchJob *fetchJob = new ItemFetchJob(tag, this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().size(), 1); + Item i = fetchJob->items().first(); + QCOMPARE(i, item1); + + TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +#include "tagtest.moc" + +QTEST_AKONADIMAIN(TagTest) diff --git a/autotests/libs/tagtest_simple.cpp b/autotests/libs/tagtest_simple.cpp new file mode 100644 index 0000000..6de5cfd --- /dev/null +++ b/autotests/libs/tagtest_simple.cpp @@ -0,0 +1,74 @@ +/* + Copyright (c) 2015 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 +#include "testattribute.h" + +#include +#include +#include + +using namespace Akonadi; + +// Tag tests not requiring a full Akonadi test environment +// this is mainly to test memory management of attributes, so this is best used with valgrind/ASan +class TagTestSimple : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testCustomAttributes(); + void testTagAttribute(); +}; + +void TagTestSimple::testCustomAttributes() +{ + Tag t2; + { + Tag t1; + auto attr = new TestAttribute; + attr->deserialize("hello"); + t1.addAttribute(attr); + t2 = t1; + } + QVERIFY(t2.hasAttribute("EXTRA")); + auto attr = t2.attribute(); + QCOMPARE(attr->serialized(), QByteArray("hello")); +} + +void TagTestSimple::testTagAttribute() +{ + Tag t2; + { + Tag t1; + auto attr = AttributeFactory::createAttribute("TAG"); + t1.addAttribute(attr); + t1.setName(QStringLiteral("hello")); + t2 = t1; + } + QVERIFY(t2.hasAttribute()); + auto attr = t2.attribute(); + QVERIFY(attr); + QCOMPARE(t2.name(), attr->displayName()); +} + +#include "tagtest_simple.moc" + +QTEST_MAIN(TagTestSimple) diff --git a/autotests/libs/test_utils.h b/autotests/libs/test_utils.h new file mode 100644 index 0000000..4d285e6 --- /dev/null +++ b/autotests/libs/test_utils.h @@ -0,0 +1,96 @@ +/* + 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. +*/ + +#ifndef AKONADI_TEST_UTILS_H +#define AKONADI_TEST_UTILS_H + +#include "collectionpathresolver.h" +#include "kdbusconnectionpool.h" +#include "servermanager.h" +#include "qtest_akonadi.h" + +#include +#include + +qint64 collectionIdFromPath(const QString &path) +{ + Akonadi::CollectionPathResolver *resolver = new Akonadi::CollectionPathResolver(path); + bool success = resolver->exec(); + if (!success) { + qDebug() << "path resolution for " << path << " failed: " << resolver->errorText(); + return -1; + } + qint64 id = resolver->collection(); + return id; +} + +QString testrunnerServiceName() +{ + const QString pid = QString::fromLocal8Bit(qgetenv("AKONADI_TESTRUNNER_PID")); + Q_ASSERT(!pid.isEmpty()); + return QStringLiteral("org.kde.Akonadi.Testrunner-") + pid; +} + +bool restartAkonadiServer() +{ + QDBusInterface testrunnerIface(testrunnerServiceName(), + QStringLiteral("/"), + QStringLiteral("org.kde.Akonadi.Testrunner"), + KDBusConnectionPool::threadConnection()); + if (!testrunnerIface.isValid()) { + qWarning() << "Unable to get a dbus interface to the testrunner!"; + } + + QDBusReply reply = testrunnerIface.call(QStringLiteral("restartAkonadiServer")); + if (!reply.isValid()) { + qWarning() << reply.error(); + return false; + } else if (Akonadi::ServerManager::isRunning()) { + return true; + } else { + bool ok = false; + [&]() { + QSignalSpy spy(Akonadi::ServerManager::self(), SIGNAL(started())); + QTRY_VERIFY_WITH_TIMEOUT(spy.count() > 0, 10000); + ok = true; + }(); + return ok; + } +} + +bool trackAkonadiProcess(bool track) +{ + QDBusInterface testrunnerIface(testrunnerServiceName(), + QStringLiteral("/"), + QStringLiteral("org.kde.Akonadi.Testrunner"), + KDBusConnectionPool::threadConnection()); + if (!testrunnerIface.isValid()) { + qWarning() << "Unable to get a dbus interface to the testrunner!"; + } + + QDBusReply reply = testrunnerIface.call(QStringLiteral("trackAkonadiProcess"), track); + if (!reply.isValid()) { + qWarning() << reply.error(); + return false; + } else { + return true; + } +} + +#endif diff --git a/autotests/libs/testattribute.h b/autotests/libs/testattribute.h new file mode 100644 index 0000000..069f393 --- /dev/null +++ b/autotests/libs/testattribute.h @@ -0,0 +1,54 @@ +/* + 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 TESTATTRIBUTE_H +#define TESTATTRIBUTE_H + +#include "attribute.h" + +#include + +/* Attribute used for testing by various unit tests. */ +class TestAttribute : public Akonadi::Attribute +{ +public: + TestAttribute() + { + } + QByteArray type() const Q_DECL_OVERRIDE + { + return "EXTRA"; + } + QByteArray serialized() const Q_DECL_OVERRIDE + { + return data; + } + void deserialize(const QByteArray &ba) Q_DECL_OVERRIDE { + data = ba; + } + TestAttribute *clone() const Q_DECL_OVERRIDE + { + TestAttribute *a = new TestAttribute; + a->data = data; + return a; + } + QByteArray data; +}; + +#endif diff --git a/autotests/libs/testenvironmenttest.cpp b/autotests/libs/testenvironmenttest.cpp new file mode 100644 index 0000000..3639dbc --- /dev/null +++ b/autotests/libs/testenvironmenttest.cpp @@ -0,0 +1,68 @@ +/* + 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 "KDBusConnectionPool" + +#include +#include + +#include +#include +#include +#include + +using namespace Akonadi; + +/** + This test verifies that the testrunner set everything up correctly, so all the + other tests work as expected. +*/ +class TestEnvironmentTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testDBus() + { + QVERIFY(KDBusConnectionPool::threadConnection().isConnected()); + } + + void testAkonadiServer() + { + QVERIFY(ServerManager::isRunning()); + } + + void testResources() + { + QVERIFY(KDBusConnectionPool::threadConnection().interface()->isServiceRegistered( + ServerManager::agentServiceName(ServerManager::Resource, QStringLiteral("akonadi_knut_resource_0")))); + QVERIFY(KDBusConnectionPool::threadConnection().interface()->isServiceRegistered( + ServerManager::agentServiceName(ServerManager::Resource, QStringLiteral("akonadi_knut_resource_1")))); + QVERIFY(KDBusConnectionPool::threadConnection().interface()->isServiceRegistered( + ServerManager::agentServiceName(ServerManager::Resource, QStringLiteral("akonadi_knut_resource_2")))); + } +}; + +QTEST_AKONADIMAIN(TestEnvironmentTest) + +#include "testenvironmenttest.moc" diff --git a/autotests/libs/testresource/CMakeLists.txt b/autotests/libs/testresource/CMakeLists.txt new file mode 100644 index 0000000..ea357dd --- /dev/null +++ b/autotests/libs/testresource/CMakeLists.txt @@ -0,0 +1,61 @@ +include_directories( + ${Boost_INCLUDE_DIR} +) + +kde_enable_exceptions() + +add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_knut_resource\") + +find_package(LibXslt) +set_package_properties(LibXslt PROPERTIES DESCRIPTION "xsltproc" URL "http://xmlsoft.org/XSLT/" TYPE REQUIRED PURPOSE "Needed to generate D-Bus interface specifications") + +find_program(XSLTPROC_EXECUTABLE xsltproc DOC "Path to the xsltproc executable") + +# generates a D-Bus interface description from a KConfigXT file +macro(kcfg_generate_dbus_interface _kcfg _name) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml + COMMAND ${XSLTPROC_EXECUTABLE} --stringparam interfaceName ${_name} + ${Akonadi_SOURCE_DIR}/src/core/kcfg2dbus.xsl + ${_kcfg} + > ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml + DEPENDS ${Akonadi_SOURCE_DIR}/src/core/kcfg2dbus.xsl + ${_kcfg} + ) +endmacro() + +# Disabled for now, resourcetester remained in kdepim-runtime +#add_subdirectory( tests ) + +set( knutresource_SRCS knutresource.cpp) + +kconfig_add_kcfg_files(knutresource_SRCS settings.kcfgc) + +kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/knutresource.kcfg org.kde.Akonadi.Knut.Settings) + +qt5_add_dbus_adaptor(knutresource_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Knut.Settings.xml settings.h KnutSettings +) + +add_executable(akonadi_knut_resource ${knutresource_SRCS}) + +if (APPLE) + set_target_properties(akonadi_knut_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.template) + set_target_properties(akonadi_knut_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.Knut") + set_target_properties(akonadi_knut_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Knut Resource") +endif () + +target_link_libraries(akonadi_knut_resource + KF5::AkonadiXml + KF5::AkonadiCore + KF5::KIOCore + KF5::AkonadiAgentBase + KF5::DBusAddons + Qt5::Xml + KF5::I18n +) + +install( TARGETS akonadi_knut_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install( FILES knutresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" ) +install( FILES knut-template.xml DESTINATION ${KDE_INSTALL_DATADIR_KF5}/akonadi_knut_resource/ ) + diff --git a/autotests/libs/testresource/Info.plist.template b/autotests/libs/testresource/Info.plist.template new file mode 100644 index 0000000..c39ddb9 --- /dev/null +++ b/autotests/libs/testresource/Info.plist.template @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + LSUIElement + 1 + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + diff --git a/autotests/libs/testresource/Messages.sh b/autotests/libs/testresource/Messages.sh new file mode 100644 index 0000000..28b7871 --- /dev/null +++ b/autotests/libs/testresource/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/akonadi_knut_resource.pot diff --git a/autotests/libs/testresource/knut-template.xml b/autotests/libs/testresource/knut-template.xml new file mode 100644 index 0000000..4319a8b --- /dev/null +++ b/autotests/libs/testresource/knut-template.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/autotests/libs/testresource/knutresource.cpp b/autotests/libs/testresource/knutresource.cpp new file mode 100644 index 0000000..141f22c --- /dev/null +++ b/autotests/libs/testresource/knutresource.cpp @@ -0,0 +1,368 @@ +/* + Copyright (c) 2006 Tobias Koenig + 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 "knutresource.h" +#include "settings.h" +#include "settingsadaptor.h" +#include "xmlwriter.h" +#include "xmlreader.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +KnutResource::KnutResource(const QString &id) + : ResourceBase(id) + , mWatcher(new QFileSystemWatcher(this)) + , mSettings(new KnutSettings()) +{ + changeRecorder()->itemFetchScope().fetchFullPayload(); + changeRecorder()->fetchCollection(true); + + new SettingsAdaptor(mSettings); + KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Settings"), + mSettings, QDBusConnection::ExportAdaptors); + connect(this, &KnutResource::reloadConfiguration, this, &KnutResource::load); + connect(mWatcher, &QFileSystemWatcher::fileChanged, this, &KnutResource::load); + load(); +} + +KnutResource::~KnutResource() +{ + delete mSettings; +} + +void KnutResource::load() +{ + if (!mWatcher->files().isEmpty()) { + mWatcher->removePaths(mWatcher->files()); + } + + // file loading + QString fileName = mSettings->dataFile(); + if (fileName.isEmpty()) { + emit status(Broken, i18n("No data file selected.")); + return; + } + + if (!QFile::exists(fileName)) { + fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/akonadi_knut_resource/knut-template.xml")); + } + + if (!mDocument.loadFile(fileName)) { + emit status(Broken, mDocument.lastError()); + return; + } + + if (mSettings->fileWatchingEnabled()) { + mWatcher->addPath(fileName); + } + + emit status(Idle, i18n("File '%1' loaded successfully.", fileName)); + synchronize(); +} + +void KnutResource::save() +{ + if (mSettings->readOnly()) { + return; + } + const QString fileName = mSettings->dataFile(); + if (!mDocument.writeToFile(fileName)) { + emit error(mDocument.lastError()); + return; + } +} + +void KnutResource::configure(WId windowId) +{ + QString oldFile = mSettings->dataFile(); + if (oldFile.isEmpty()) { + oldFile = QDir::homePath(); + } + + // TODO: Use winId + const QString newFile = QFileDialog::getSaveFileName( + 0, i18n("Select Data File"), QString(), + QStringLiteral("*.xml |") + i18nc("Filedialog filter for Akonadi data file", "Akonadi Knut Data File")); + + if (newFile.isEmpty() || oldFile == newFile) { + return; + } + + mSettings->setDataFile(newFile); + mSettings->save(); + load(); + + emit configurationDialogAccepted(); +} + +void KnutResource::retrieveCollections() +{ + const Collection::List collections = mDocument.collections(); + collectionsRetrieved(collections); + const Tag::List tags = mDocument.tags(); + Q_FOREACH (const Tag &tag, tags) { + TagCreateJob *createjob = new TagCreateJob(tag); + createjob->setMergeIfExisting(true); + } +} + +void KnutResource::retrieveItems(const Akonadi::Collection &collection) +{ + Item::List items = mDocument.items(collection, false); + if (!mDocument.lastError().isEmpty()) { + cancelTask(mDocument.lastError()); + return; + } + + itemsRetrieved(items); +} + +bool KnutResource::retrieveItem(const Item &item, const QSet &parts) +{ + Q_UNUSED(parts); + + const QDomElement itemElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (itemElem.isNull()) { + cancelTask(i18n("No item found for remoteid %1", item.remoteId())); + return false; + } + + Item i = XmlReader::elementToItem(itemElem, true); + i.setId(item.id()); + itemRetrieved(i); + return true; +} + +void KnutResource::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ +#if 0 //PORT QT5 + QDomElement parentElem = mDocument.collectionElementByRemoteId(parent.remoteId()); + if (parentElem.isNull()) { + emit error(i18n("Parent collection not found in DOM tree.")); + changeProcessed(); + return; + } + + Collection c(collection); + c.setRemoteId(QUuid::createUuid().toString()); + if (XmlWriter::writeCollection(c, parentElem).isNull()) { + emit error(i18n("Unable to write collection.")); + changeProcessed(); + } else { + save(); + changeCommitted(c); + } +#endif +} + +void KnutResource::collectionChanged(const Akonadi::Collection &collection) +{ +#if 0 //PORT QT5 + QDomElement oldElem = mDocument.collectionElementByRemoteId(collection.remoteId()); + if (oldElem.isNull()) { + emit error(i18n("Modified collection not found in DOM tree.")); + changeProcessed(); + return; + } + + Collection c(collection); + QDomElement newElem; + newElem = XmlWriter::collectionToElement(c, mDocument.document()); + // move all items/collections over to the new node + const QDomNodeList children = oldElem.childNodes(); + const int numberOfChildren = children.count(); + for (int i = 0; i < numberOfChildren; ++i) { + const QDomElement child = children.at(i).toElement(); + qDebug() << "reparenting " << child.tagName() << child.attribute(QStringLiteral("rid")); + if (child.isNull()) { + continue; + } + if (child.tagName() == QStringLiteral("item") || child.tagName() == QStringLiteral("collection")) { + newElem.appendChild(child); // reparents + --i; // children, despite being const is modified by the reparenting + } + } + oldElem.parentNode().replaceChild(newElem, oldElem); + save(); + changeCommitted(c); +#endif +} + +void KnutResource::collectionRemoved(const Akonadi::Collection &collection) +{ +#if 0 //PORT QT5 + const QDomElement colElem = mDocument.collectionElementByRemoteId(collection.remoteId()); + if (colElem.isNull()) { + emit error(i18n("Deleted collection not found in DOM tree.")); + changeProcessed(); + return; + } + + colElem.parentNode().removeChild(colElem); + save(); + changeProcessed(); +#endif +} + +void KnutResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ +#if 0 //PORT QT5 + QDomElement parentElem = mDocument.collectionElementByRemoteId(collection.remoteId()); + if (parentElem.isNull()) { + emit error(i18n("Parent collection '%1' not found in DOM tree." , collection.remoteId())); + changeProcessed(); + return; + } + + Item i(item); + i.setRemoteId(QUuid::createUuid().toString()); + if (XmlWriter::writeItem(i, parentElem).isNull()) { + emit error(i18n("Unable to write item.")); + changeProcessed(); + } else { + save(); + changeCommitted(i); + } +#endif +} + +void KnutResource::itemChanged(const Akonadi::Item &item, const QSet &parts) +{ + Q_UNUSED(parts); + + const QDomElement oldElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (oldElem.isNull()) { + emit error(i18n("Modified item not found in DOM tree.")); + changeProcessed(); + return; + } + + Item i(item); + const QDomElement newElem = XmlWriter::itemToElement(i, mDocument.document()); + oldElem.parentNode().replaceChild(newElem, oldElem); + save(); + changeCommitted(i); +} + +void KnutResource::itemRemoved(const Akonadi::Item &item) +{ + const QDomElement itemElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (itemElem.isNull()) { + emit error(i18n("Deleted item not found in DOM tree.")); + changeProcessed(); + return; + } + + itemElem.parentNode().removeChild(itemElem); + save(); + changeProcessed(); +} + +void KnutResource::itemMoved(const Item &item, const Collection &collectionSource, const Collection &collectionDestination) +{ + const QDomElement oldElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (oldElem.isNull()) { + qWarning() << "Moved item not found in DOM tree"; + changeProcessed(); + return; + } +#if 0 //PORT QT5 + QDomElement sourceParentElem = mDocument.collectionElementByRemoteId(collectionSource.remoteId()); + if (sourceParentElem.isNull()) { + emit error(i18n("Parent collection '%1' not found in DOM tree.", collectionSource.remoteId())); + changeProcessed(); + return; + } + + QDomElement destParentElem = mDocument.collectionElementByRemoteId(collectionDestination.remoteId()); + if (destParentElem.isNull()) { + emit error(i18n("Parent collection '%1' not found in DOM tree.", collectionDestination.remoteId())); + changeProcessed(); + return; + } + QDomElement itemElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (itemElem.isNull()) { + emit error(i18n("No item found for remoteid %1", item.remoteId())); + } + + sourceParentElem.removeChild(itemElem); + destParentElem.appendChild(itemElem); + + if (XmlWriter::writeItem(item, destParentElem).isNull()) { + emit error(i18n("Unable to write item.")); + } else { + save(); + } + changeProcessed(); +#endif +} + +QSet KnutResource::parseQuery(const QString &queryString) +{ + QSet resultSet; + Akonadi::SearchQuery query = Akonadi::SearchQuery::fromJSON(queryString.toLatin1()); + foreach (const Akonadi::SearchTerm &term, query.term().subTerms()) { + if (term.key() == QStringLiteral("resource")) { + resultSet << term.value().toInt(); + } + } + return resultSet; +} + +void KnutResource::search(const QString &query, const Collection &collection) +{ + const QVector result = parseQuery(query).toList().toVector(); + qDebug() << "KNUT QUERY:" << query; + qDebug() << "KNUT RESOURCE:" << result; + searchFinished(result, Akonadi::AgentSearchInterface::Uid); +} + +void KnutResource::addSearch(const QString &query, const QString &queryLanguage, const Collection &resultCollection) +{ + qDebug(); +} + +void KnutResource::removeSearch(const Collection &resultCollection) +{ + qDebug(); +} + +AKONADI_RESOURCE_MAIN(KnutResource) diff --git a/autotests/libs/testresource/knutresource.desktop b/autotests/libs/testresource/knutresource.desktop new file mode 100644 index 0000000..7607b5b --- /dev/null +++ b/autotests/libs/testresource/knutresource.desktop @@ -0,0 +1,56 @@ +[Desktop Entry] +Name=Knut +Name[ca]=Knut +Name[ca@valencia]=Knut +Name[cs]=Knut +Name[de]=Knut +Name[en_GB]=Knut +Name[es]=Knut +Name[fi]=Knut +Name[it]=Knut +Name[nl]=Knut +Name[pl]=Knut +Name[pt]=Knut +Name[pt_BR]=Knut +Name[ru]=Knut +Name[sk]=Knut +Name[sl]=Knut +Name[sr]=КНУТ +Name[sr@ijekavian]=КНУТ +Name[sr@ijekavianlatin]=KNUT +Name[sr@latin]=KNUT +Name[sv]=Knut +Name[uk]=Knut +Name[x-test]=xxKnutxx +Name[zh_CN]=Knut +Comment=An agent for debugging purpose +Comment[ca]=Un agent per a propòsits de depuració +Comment[ca@valencia]=Un agent per a propòsits de depuració +Comment[cs]=Agent pro ladicí účely +Comment[de]=Ein Agent zur Fehlersuche +Comment[en_GB]=An agent for debugging purpose +Comment[es]=Un agente para el propósito de depuración +Comment[fi]=Virheenpaikannukseen tarkoitettu agentti +Comment[it]=Un agente per scopi di debug +Comment[nl]=Een agent voor debugging-doeleinden +Comment[pl]=Agent na potrzeby diagnostyczne +Comment[pt]=Um agente para fins de depuração +Comment[pt_BR]=Um agente para depuração +Comment[ru]=Агент Akonadi для целей отладки +Comment[sk]=Agent na ladiace účely +Comment[sl]=Posrednik za namene razhroščevanja +Comment[sr]=Агент за потребе исправљања +Comment[sr@ijekavian]=Агент за потребе исправљања +Comment[sr@ijekavianlatin]=Agent za potrebe ispravljanja +Comment[sr@latin]=Agent za potrebe ispravljanja +Comment[sv]=En modul för felsökningssyften +Comment[uk]=Агент для діагностики +Comment[x-test]=xxAn agent for debugging purposexx +Comment[zh_CN]=调试用代理 +Type=AkonadiResource +Exec=akonadi_knut_resource +Icon=tools-report-bug + +X-Akonadi-MimeTypes=text/calendar,text/directory +X-Akonadi-Capabilities=Resource +X-Akonadi-Identifier=akonadi_knut_resource diff --git a/autotests/libs/testresource/knutresource.h b/autotests/libs/testresource/knutresource.h new file mode 100644 index 0000000..b7f9548 --- /dev/null +++ b/autotests/libs/testresource/knutresource.h @@ -0,0 +1,83 @@ +/* + Copyright (c) 2006 Tobias Koenig + 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. +*/ + +#ifndef KNUTRESOURCE_H +#define KNUTRESOURCE_H + +#include +#include +#include + +#include +#include +#include + +#include "settings.h" + +class QFileSystemWatcher; + +class KnutResource : public Akonadi::ResourceBase, + public Akonadi::AgentBase::ObserverV2, + public Akonadi::AgentSearchInterface +{ + Q_OBJECT + +public: + using Akonadi::AgentBase::ObserverV2::collectionChanged; // So we don't trigger -Woverloaded-virtual + KnutResource(const QString &id); + ~KnutResource(); + +public Q_SLOTS: + void configure(WId windowId) Q_DECL_OVERRIDE; + +protected: + void retrieveCollections() Q_DECL_OVERRIDE; + void retrieveItems(const Akonadi::Collection &collection) Q_DECL_OVERRIDE; + bool retrieveItem(const Akonadi::Item &item, const QSet &parts) Q_DECL_OVERRIDE; + + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE; + void collectionChanged(const Akonadi::Collection &collection) Q_DECL_OVERRIDE; + void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE; + + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE; + void itemChanged(const Akonadi::Item &item, const QSet &parts) Q_DECL_OVERRIDE; + void itemRemoved(const Akonadi::Item &ref) Q_DECL_OVERRIDE; + void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) Q_DECL_OVERRIDE; + + void search(const QString &query, const Akonadi::Collection &collection) Q_DECL_OVERRIDE; + void addSearch(const QString &query, const QString &queryLanguage, const Akonadi::Collection &resultCollection) Q_DECL_OVERRIDE; + void removeSearch(const Akonadi::Collection &resultCollection) Q_DECL_OVERRIDE; + +private: + QDomElement findElementByRid(const QString &rid) const; + + static QSet parseQuery(const QString &queryString); + +private Q_SLOTS: + void load(); + void save(); + +private: + Akonadi::XmlDocument mDocument; + QFileSystemWatcher *mWatcher; + KnutSettings *mSettings; +}; + +#endif diff --git a/autotests/libs/testresource/knutresource.kcfg b/autotests/libs/testresource/knutresource.kcfg new file mode 100644 index 0000000..fbe4549 --- /dev/null +++ b/autotests/libs/testresource/knutresource.kcfg @@ -0,0 +1,21 @@ + + + + + + + + + + + false + + + true + + + diff --git a/autotests/libs/testresource/settings.kcfgc b/autotests/libs/testresource/settings.kcfgc new file mode 100644 index 0000000..4d50009 --- /dev/null +++ b/autotests/libs/testresource/settings.kcfgc @@ -0,0 +1,8 @@ +File=knutresource.kcfg +ClassName=KnutSettings +Mutators=true +ItemAccessors=true +SetUserTexts=true +Singleton=false +#IncludeFiles= +GlobalEnums=true diff --git a/autotests/libs/testresource/tests/CMakeLists.txt b/autotests/libs/testresource/tests/CMakeLists.txt new file mode 100644 index 0000000..37e23ed --- /dev/null +++ b/autotests/libs/testresource/tests/CMakeLists.txt @@ -0,0 +1,6 @@ +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/knut-empty.xml ${CMAKE_CURRENT_BINARY_DIR}/knut-empty.xml COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/knut-step1.xml ${CMAKE_CURRENT_BINARY_DIR}/knut-step1.xml COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/knut-step2.xml ${CMAKE_CURRENT_BINARY_DIR}/knut-step2.xml COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/knutdemo.xml ${CMAKE_CURRENT_BINARY_DIR}/knutdemo.xml COPYONLY) + +akonadi_add_resourcetest( knutdemo knutdemo.js ) diff --git a/autotests/libs/testresource/tests/knut-empty.xml b/autotests/libs/testresource/tests/knut-empty.xml new file mode 100644 index 0000000..4319a8b --- /dev/null +++ b/autotests/libs/testresource/tests/knut-empty.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/autotests/libs/testresource/tests/knut-step1.xml b/autotests/libs/testresource/tests/knut-step1.xml new file mode 100644 index 0000000..338c69a --- /dev/null +++ b/autotests/libs/testresource/tests/knut-step1.xml @@ -0,0 +1,28 @@ + + + + + From: Volker Krause <vkrause@kde.org> +To: kde-commits@kde.org +Subject: playground/pim/akonaditest/resourcetester +MIME-Version: 1.0 +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 8bit +Date: Sun, 22 Mar 2009 12:50:30 +0000 +Message-Id: <1237726230.394911.25706.nullmailer@svn.kde.org> + +SVN commit 942677 by vkrause: + +Add a safety timeout in case we do not receive the synchronized() signal +or the resource hangs during syncing. The first seems to happen randomly +if syncing is extremely fast. + + + M +40 -0 resourcesynchronizationjob.cpp + M +1 -1 resourcesynchronizationjob.h + + + + + diff --git a/autotests/libs/testresource/tests/knut-step2.xml b/autotests/libs/testresource/tests/knut-step2.xml new file mode 100644 index 0000000..d518526 --- /dev/null +++ b/autotests/libs/testresource/tests/knut-step2.xml @@ -0,0 +1,28 @@ + + + + + From: Volker Krause <vkrause@kde.org> +To: kde-commits@kde.org +Subject: playground/pim/akonaditest/resourcetester +MIME-Version: 1.0 +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 8bit +Date: Sun, 22 Mar 2009 12:50:30 +0000 +Message-Id: <1237726230.394911.25706.nullmailer@svn.kde.org> + +SVN commit 942677 by vkrause: + +Add a safety timeout in case we do not receive the synchronized() signal +or the resource hangs during syncing. The first seems to happen randomly +if syncing is extremely fast. + + + M +40 -0 resourcesynchronizationjob.cpp + M +1 -1 resourcesynchronizationjob.h + + + + + diff --git a/autotests/libs/testresource/tests/knutdemo.js b/autotests/libs/testresource/tests/knutdemo.js new file mode 100644 index 0000000..607df87 --- /dev/null +++ b/autotests/libs/testresource/tests/knutdemo.js @@ -0,0 +1,65 @@ +Resource.setType( "akonadi_knut_resource" ); + +// read test +Resource.setPathOption( "DataFile", "knutdemo.xml" ); +Resource.setOption( "FileWatchingEnabled", false ); +Resource.create(); + +XmlOperations.setXmlFile( "knutdemo.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.assertEqual(); + +Resource.destroy(); + +// empty resource +Resource.setPathOption( "DataFile", "newfile.xml" ); +Resource.create(); + +XmlOperations.setXmlFile( "knut-empty.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.assertEqual(); + +// folder creation +CollectionTest.setParent( "Knut test data" ); +CollectionTest.addContentType( "message/rfc822" ); +CollectionTest.setName( "test folder" ); +CollectionTest.create(); +//Resource.recreate(); + +// item creation +ItemTest.setParentCollection( "Knut test data/test folder" ); +ItemTest.setMimeType( "message/rfc822" ); +ItemTest.setPayloadFromFile( "testmail.mbox" ); +ItemTest.create(); + +Resource.recreate(); + +XmlOperations.setXmlFile( "knut-step1.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.setCollectionKey( "None" ); +XmlOperations.ignoreCollectionField( "RemoteId" ); +XmlOperations.setItemKey( "None" ); +XmlOperations.ignoreItemField( "RemoteId" ); +XmlOperations.assertEqual(); + +// folder modification +CollectionTest.setCollection( "Knut test data/test folder" ); +CollectionTest.setName( "changed folder" ); +CollectionTest.update(); + +Resource.recreate(); + +XmlOperations.setXmlFile( "knut-step2.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.assertEqual(); + +// folder deletion +CollectionTest.setCollection( "Knut test data/changed folder" ); +CollectionTest.remove(); + +Resource.recreate(); + +XmlOperations.setXmlFile( "knut-empty.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.assertEqual(); + diff --git a/autotests/libs/testresource/tests/knutdemo.xml b/autotests/libs/testresource/tests/knutdemo.xml new file mode 100644 index 0000000..88b75ca --- /dev/null +++ b/autotests/libs/testresource/tests/knutdemo.xml @@ -0,0 +1,72 @@ + + + + ("Posteingang" "mail-folder-inbox") + + + + wcW + + + Subject: Welcome to the Knut resource +To: new-user@this-computer.local +From: knut@your.computer.local +MIME-Version: 1.0 +Content-Type: text/plain +Date: Thu, 01 Jan 2009 15:08:50 +0000 + +This is a mail body + + \SEEN + + + + + + + + + +BEGIN:VCARD +EMAIL:vkrause@kde.org +FN:Volker Krause +GEO:52.500000;13.366667 +N:Krause;Volker;;; +NAME:Volker Krause +ORG:KDE +REV:2003-02-27T20:08:42Z +ROLE:Author of this file +TZ:+02:00 +UID:bb2slGmqxb +URL:http://www.akonadi-project.org +VERSION:3.0 +END:VCARD + + + + + + +BEGIN:VCALENDAR +PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN +VERSION:2.0 +BEGIN:VTODO +DTSTAMP:20090101T154017Z +ORGANIZER:MAILTO:vkrause@kde.org +CREATED:20040505T094143Z +UID:libkcal-1506191911.958 +LAST-MODIFIED:20040512T133925Z +SUMMARY:Add a demo task to this file +PRIORITY:3 +DUE;VALUE=DATE:20090101 +COMPLETED:20090101T133925Z +PERCENT-COMPLETE:100 +END:VTODO +END:VCALENDAR + + + + + + + diff --git a/autotests/libs/testresource/tests/testmail.mbox b/autotests/libs/testresource/tests/testmail.mbox new file mode 100644 index 0000000..f14d60d --- /dev/null +++ b/autotests/libs/testresource/tests/testmail.mbox @@ -0,0 +1,19 @@ +From: Volker Krause +To: kde-commits@kde.org +Subject: playground/pim/akonaditest/resourcetester +MIME-Version: 1.0 +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 8bit +Date: Sun, 22 Mar 2009 12:50:30 +0000 +Message-Id: <1237726230.394911.25706.nullmailer@svn.kde.org> + +SVN commit 942677 by vkrause: + +Add a safety timeout in case we do not receive the synchronized() signal +or the resource hangs during syncing. The first seems to happen randomly +if syncing is extremely fast. + + + M +40 -0 resourcesynchronizationjob.cpp + M +1 -1 resourcesynchronizationjob.h diff --git a/autotests/libs/testrunner/CMakeLists.txt b/autotests/libs/testrunner/CMakeLists.txt new file mode 100644 index 0000000..f8589fc --- /dev/null +++ b/autotests/libs/testrunner/CMakeLists.txt @@ -0,0 +1,25 @@ +kde_enable_exceptions() + +set(akonaditest_SRCS + main.cpp + setup.cpp + config.cpp + shellscript.cpp + testrunner.cpp +) + +add_executable(akonaditest ${akonaditest_SRCS}) + +target_link_libraries(akonaditest + KF5::AkonadiCore + KF5::I18n + KF5::ConfigCore + Qt5::Xml + Qt5::DBus + Qt5::Widgets +) + +install(TARGETS akonaditest ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) + +# Set the akonaditest path (needed by AkonadiMacros.cmake when invoked in kdepimlibs) +set(_akonaditest_DIR ${CMAKE_CURRENT_BINARY_DIR} CACHE PATH "akonaditest path") diff --git a/autotests/libs/testrunner/config.cpp b/autotests/libs/testrunner/config.cpp new file mode 100644 index 0000000..2402fa8 --- /dev/null +++ b/autotests/libs/testrunner/config.cpp @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2008 Igor Trindade Oliveira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "config.h" //krazy:exclude=includes + +#include + +#include +#include +#include +#include +#include +#include +#include + +Q_GLOBAL_STATIC(Config, globalConfig) + +Config::Config() +{ +} + +Config::~Config() +{ +} + +Config *Config::instance(const QString &pathToConfig) +{ + if (!pathToConfig.isEmpty()) { + globalConfig()->readConfiguration(pathToConfig); + } + + return globalConfig(); +} + +void Config::readConfiguration(const QString &configfile) +{ + QDomDocument doc; + QFile file(configfile); + + if (!file.open(QIODevice::ReadOnly)) { + qFatal("error reading file: %s", qPrintable(configfile)); + } + + QString errorMsg; + if (!doc.setContent(&file, &errorMsg)) { + qFatal("unable to parse config file: %s", qPrintable(errorMsg)); + } + + const QDomElement root = doc.documentElement(); + if (root.tagName() != QStringLiteral("config")) { + qFatal("could not file root tag"); + } + + mBasePath = QFileInfo(configfile).absolutePath() + QLatin1Char('/'); + + QDomNode node = root.firstChild(); + while (!node.isNull()) { + const QDomElement element = node.toElement(); + if (!element.isNull()) { + if (element.tagName() == QStringLiteral("confighome")) { + setXdgConfigHome(mBasePath + element.text()); + } else if (element.tagName() == QStringLiteral("datahome")) { + setXdgDataHome(mBasePath + element.text()); + } else if (element.tagName() == QStringLiteral("agent")) { + insertAgent(element.text(), element.attribute(QStringLiteral("synchronize"), QStringLiteral("false")) == QStringLiteral("true")); + } else if (element.tagName() == QStringLiteral("envvar")) { + const QString name = element.attribute(QStringLiteral("name")); + if (name.isEmpty()) { + qWarning() << "Given envvar with no name."; + } else { + mEnvVars[name] = element.text(); + } + } + } + + node = node.nextSibling(); + } +} + +QString Config::xdgDataHome() const +{ + return mXdgDataHome; +} + +QString Config::xdgConfigHome() const +{ + return mXdgConfigHome; +} + +QString Config::basePath() const +{ + return mBasePath; +} + +void Config::setXdgDataHome(const QString &dataHome) +{ + const QDir dataHomeDir(dataHome); + mXdgDataHome = dataHomeDir.absolutePath(); +} + +void Config::setXdgConfigHome(const QString &configHome) +{ + const QDir configHomeDir(configHome); + mXdgConfigHome = configHomeDir.absolutePath(); +} + +void Config::insertAgent(const QString &agent, bool sync) +{ + mAgents.append(qMakePair(agent, sync)); +} + +QList > Config::agents() const +{ + return mAgents; +} + +QHash Config::envVars() const +{ + return mEnvVars; +} diff --git a/autotests/libs/testrunner/config.h b/autotests/libs/testrunner/config.h new file mode 100644 index 0000000..8bcd77c --- /dev/null +++ b/autotests/libs/testrunner/config.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2008 Igor Trindade Oliveira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include + +class Config +{ +public: + Config(); + ~Config(); + static Config *instance(const QString &pathToConfig = QString()); + static void destroyInstance(); + QString xdgDataHome() const; + QString xdgConfigHome() const; + QString basePath() const; + QList > agents() const; + QHash envVars() const; + +protected: + void setXdgDataHome(const QString &dataHome); + void setXdgConfigHome(const QString &configHome); + void insertAgent(const QString &agent, bool sync); + +private: + void readConfiguration(const QString &configFile); + +private: + QString mBasePath; + QString mXdgDataHome; + QString mXdgConfigHome; + QList > mAgents; + QHash mEnvVars; +}; + +#endif diff --git a/autotests/libs/testrunner/config.xml b/autotests/libs/testrunner/config.xml new file mode 100644 index 0000000..86e5a2e --- /dev/null +++ b/autotests/libs/testrunner/config.xml @@ -0,0 +1,6 @@ + + kde + config + local/share + + diff --git a/autotests/libs/testrunner/main.cpp b/autotests/libs/testrunner/main.cpp new file mode 100644 index 0000000..7c3a141 --- /dev/null +++ b/autotests/libs/testrunner/main.cpp @@ -0,0 +1,130 @@ +/* + * + * Copyright (c) 2008 Igor Trindade Oliveira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "config.h" //krazy:exclude=includes +#include "setup.h" +#include "shellscript.h" +#include "testrunner.h" + +#include + +#include +#include + +#include +#include +#include +#include + +static SetupTest *setup = 0; +static TestRunner *runner = 0; + +void sigHandler(int signal) +{ + qDebug() << "Received signal" << signal; + static int sigCounter = 0; + if (sigCounter == 0) { // try clean shutdown + if (runner) { + runner->terminate(); + } + if (setup) { + setup->shutdown(); + } + } else if (sigCounter == 1) { // force shutdown + if (setup) { + setup->shutdownHarder(); + } + } else { // give up and just exit + exit(255); + } + ++sigCounter; +} + +int main(int argc, char **argv) +{ + KAboutData aboutdata(QStringLiteral("akonadi-TES"), + i18n("Akonadi Testing Environment Setup"), + QStringLiteral("1.0"), + i18n("Setup Environmnet"), + KAboutLicense::GPL, + i18n("(c) 2008 Igor Trindade Oliveira")); + + QApplication app(argc, argv); + app.setQuitLockEnabled(false); + QCommandLineParser parser; + KAboutData::setApplicationData(aboutdata); + parser.addVersionOption(); + parser.addHelpOption(); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("c") << QStringLiteral("config"), i18n("Configuration file to open"), QStringLiteral("configfile"), QStringLiteral("config.xml"))); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("!+[test]"), i18n("Test to run automatically, interactive if none specified"))); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("testenv"), i18n("Path where testenvironment would be saved"), QStringLiteral("path"))); + + aboutdata.setupCommandLine(&parser); + parser.process(app); + aboutdata.processCommandLine(&parser); + + //QT5 app.disableSessionManagement(); + + if (parser.isSet(QStringLiteral("config"))) { + Config::instance(parser.value(QStringLiteral("config"))); + } + +#ifdef Q_OS_UNIX + signal(SIGINT, sigHandler); + signal(SIGQUIT, sigHandler); +#endif + + setup = new SetupTest(); + + if (!setup->startAkonadiDaemon()) { + delete setup; + qCritical("Failed to start Akonadi server!"); + return 1; + } + + ShellScript sh; + sh.setEnvironmentVariables(setup->environmentVariables()); + + if (parser.isSet(QStringLiteral("testenv"))) { + sh.makeShellScript(parser.value(QStringLiteral("testenv"))); + } else { + sh.makeShellScript(setup->basePath() + QLatin1String("testenvironment.sh")); + } + + if (!parser.positionalArguments().isEmpty()) { + QStringList testArgs; + for (int i = 0; i < parser.positionalArguments().count(); ++i) { + testArgs << parser.positionalArguments().at(i); + } + runner = new TestRunner(testArgs); + QObject::connect(setup, &SetupTest::setupDone, runner, &TestRunner::run); + QObject::connect(setup, &SetupTest::serverExited, runner, &TestRunner::triggerTermination); + QObject::connect(runner, &TestRunner::finished, setup, &SetupTest::shutdown); + } + + int exitCode = app.exec(); + if (runner) { + exitCode += runner->exitCode(); + delete runner; + } + + delete setup; + setup = 0; + + return exitCode; +} diff --git a/autotests/libs/testrunner/setup.cpp b/autotests/libs/testrunner/setup.cpp new file mode 100644 index 0000000..cec56ca --- /dev/null +++ b/autotests/libs/testrunner/setup.cpp @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2008 Igor Trindade Oliveira + * Copyright (c) 2013 Volker Krause + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "setup.h" +#include "config.h" //krazy:exclude=includes + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +bool SetupTest::startAkonadiDaemon() +{ + Q_ASSERT(Akonadi::ServerManager::hasInstanceIdentifier()); + + if (!mAkonadiDaemonProcess) { + mAkonadiDaemonProcess = new KProcess(this); + connect(mAkonadiDaemonProcess, SIGNAL(finished(int)), + this, SLOT(slotAkonadiDaemonProcessFinished(int))); + } + + mAkonadiDaemonProcess->setProgram(QStringLiteral("akonadi_control"), QStringList() << QStringLiteral("--instance") << instanceId()); + mAkonadiDaemonProcess->start(); + const bool started = mAkonadiDaemonProcess->waitForStarted(5000); + qDebug() << "Started akonadi daemon with pid:" << mAkonadiDaemonProcess->pid(); + return started; +} + +void SetupTest::stopAkonadiDaemon() +{ + if (!mAkonadiDaemonProcess) { + return; + } + disconnect(mAkonadiDaemonProcess, SIGNAL(finished(int)), this, 0); + mAkonadiDaemonProcess->terminate(); + const bool finished = mAkonadiDaemonProcess->waitForFinished(5000); + if (!finished) { + qDebug() << "Problem finishing process."; + } + mAkonadiDaemonProcess->close(); + mAkonadiDaemonProcess->deleteLater(); + mAkonadiDaemonProcess = 0; +} + +void SetupTest::setupAgents() +{ + if (mAgentsCreated) { + return; + } + mAgentsCreated = true; + Config *config = Config::instance(); + const QList > agents = config->agents(); + typedef QPair StringBoolPair; + foreach (const StringBoolPair &agent, agents) { + qDebug() << "Creating agent" << agent.first << "..."; + ++mSetupJobCount; + Akonadi::AgentInstanceCreateJob *job = new Akonadi::AgentInstanceCreateJob(agent.first, this); + job->setProperty("sync", agent.second); + connect(job, &Akonadi::AgentInstanceCreateJob::result, this, &SetupTest::agentCreationResult); + job->start(); + } + + if (isSetupDone()) { + emit setupDone(); + } +} + +void SetupTest::agentCreationResult(KJob *job) +{ + qDebug() << "Agent created"; + --mSetupJobCount; + if (job->error()) { + qCritical() << job->errorString(); + setupFailed(); + return; + } + const bool needsSync = job->property("sync").toBool(); + if (needsSync) { + ++mSetupJobCount; + qDebug() << "Scheduling Agent sync"; + Akonadi::ResourceSynchronizationJob *sync = new Akonadi::ResourceSynchronizationJob( + qobject_cast(job)->instance(), this); + connect(sync, &Akonadi::ResourceSynchronizationJob::result, this, &SetupTest::synchronizationResult); + sync->start(); + } + + if (isSetupDone()) { + emit setupDone(); + } +} + +void SetupTest::synchronizationResult(KJob *job) +{ + qDebug() << "Sync done"; + --mSetupJobCount; + if (job->error()) { + qCritical() << job->errorString(); + setupFailed(); + } + + if (isSetupDone()) { + emit setupDone(); + } +} + +void SetupTest::serverStateChanged(Akonadi::ServerManager::State state) +{ + if (state == Akonadi::ServerManager::Running) { + setupAgents(); + } else if (mShuttingDown && state == Akonadi::ServerManager::NotRunning) { + shutdownHarder(); + } +} + +void SetupTest::copyXdgDirectory(const QString &src, const QString &dst) +{ + const QDir srcDir(src); + foreach (const QFileInfo &fi, srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot)) { + if (fi.isDir()) { + if (fi.fileName() == QStringLiteral("akonadi")) { + // namespace according to instance identifier + copyDirectory(fi.absoluteFilePath(), dst + QDir::separator() + QStringLiteral("akonadi") + QDir::separator() + + QStringLiteral("instance") + QDir::separator() + instanceId()); + } else { + copyDirectory(fi.absoluteFilePath(), dst + QDir::separator() + fi.fileName()); + } + } else { + if (fi.fileName().startsWith(QStringLiteral("akonadi_")) && fi.fileName().endsWith(QStringLiteral("rc"))) { + // namespace according to instance identifier + const QString baseName = fi.fileName().left(fi.fileName().size() - 2); + QFile::copy(fi.absoluteFilePath(), dst + QDir::separator() + Akonadi::ServerManager::addNamespace(baseName) + QStringLiteral("rc")); + } else { + QFile::copy(fi.absoluteFilePath(), dst + QDir::separator() + fi.fileName()); + } + } + } +} + +void SetupTest::copyDirectory(const QString &src, const QString &dst) +{ + const QDir srcDir(src); + QDir::root().mkpath(dst); + + foreach (const QFileInfo &fi, srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot)) { + if (fi.isDir()) { + copyDirectory(fi.absoluteFilePath(), dst + QDir::separator() + fi.fileName()); + } else { + QFile::copy(fi.absoluteFilePath(), dst + QDir::separator() + fi.fileName()); + } + } +} + +void SetupTest::createTempEnvironment() +{ + qDebug() << "Creating test environment in" << basePath(); + + const QDir tmpDir(basePath()); + const QString testRunnerDataDir = QStringLiteral("data"); + const QString testRunnerConfigDir = QStringLiteral("config"); + const QString testRunnerTmpDir = QStringLiteral("tmp"); + + tmpDir.mkdir(testRunnerConfigDir); + tmpDir.mkdir(testRunnerDataDir); + tmpDir.mkdir(testRunnerTmpDir); + + const Config *config = Config::instance(); + // Always copy the generic xdgconfig dir + copyXdgDirectory(config->basePath() + QStringLiteral("/xdgconfig"), basePath() + testRunnerConfigDir); + copyXdgDirectory(config->xdgConfigHome(), basePath() + testRunnerConfigDir); + copyXdgDirectory(config->xdgDataHome(), basePath() + testRunnerDataDir); + + setEnvironmentVariable("XDG_DATA_HOME", basePath() + testRunnerDataDir); + setEnvironmentVariable("XDG_CONFIG_HOME", basePath() + testRunnerConfigDir); + setEnvironmentVariable("TMPDIR", basePath() + testRunnerTmpDir); +} + +void SetupTest::cleanTempEnvironment() +{ + QDir(basePath()).removeRecursively(); +} + +SetupTest::SetupTest() + : mAkonadiDaemonProcess(0) + , mShuttingDown(false) + , mAgentsCreated(false) + , mTrackAkonadiProcess(true) + , mSetupJobCount(0) + , mExitCode(0) +{ + setupInstanceId(); + cleanTempEnvironment(); + createTempEnvironment(); + + // switch off agent auto-starting by default, can be re-enabled if really needed inside the config.xml + setEnvironmentVariable("AKONADI_DISABLE_AGENT_AUTOSTART", QStringLiteral("true")); + setEnvironmentVariable("AKONADI_TESTRUNNER_PID", QString::number(QCoreApplication::instance()->applicationPid())); + + QHashIterator iter(Config::instance()->envVars()); + while (iter.hasNext()) { + iter.next(); + qDebug() << "Setting environment variable" << iter.key() << "=" << iter.value(); + setEnvironmentVariable(iter.key().toLocal8Bit(), iter.value()); + } + + // No kres-migrator please + KConfig migratorConfig(basePath() + QStringLiteral("config/kres-migratorrc")); + KConfigGroup migrationCfg(&migratorConfig, "Migration"); + migrationCfg.writeEntry("Enabled", false); + + connect(Akonadi::ServerManager::self(), SIGNAL(stateChanged(Akonadi::ServerManager::State)), + SLOT(serverStateChanged(Akonadi::ServerManager::State))); + + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.Akonadi.Testrunner-") + QString::number(QCoreApplication::instance()->applicationPid())); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/"), this, QDBusConnection::ExportScriptableSlots); +} + +SetupTest::~SetupTest() +{ + cleanTempEnvironment(); +} + +void SetupTest::shutdown() +{ + if (mShuttingDown) { + return; + } + mShuttingDown = true; + + switch (Akonadi::ServerManager::self()->state()) { + case Akonadi::ServerManager::Running: + case Akonadi::ServerManager::Starting: + case Akonadi::ServerManager::Upgrading: + qDebug() << "Shutting down Akonadi control..."; + Akonadi::ServerManager::self()->stop(); + // safety timeout + QTimer::singleShot(30 * 1000, this, SLOT(shutdownHarder())); + break; + case Akonadi::ServerManager::NotRunning: + case Akonadi::ServerManager::Broken: + shutdownHarder(); + case Akonadi::ServerManager::Stopping: + // safety timeout + QTimer::singleShot(30 * 1000, this, SLOT(shutdownHarder())); + break; + } +} + +void SetupTest::shutdownHarder() +{ + qDebug(); + mShuttingDown = false; + stopAkonadiDaemon(); + QCoreApplication::instance()->exit(mExitCode); +} + +void SetupTest::restartAkonadiServer() +{ + qDebug(); + disconnect(mAkonadiDaemonProcess, SIGNAL(finished(int)), this, 0); + Akonadi::ServerManager::self()->stop(); + const bool shutdownResult = mAkonadiDaemonProcess->waitForFinished(); + if (!shutdownResult) { + qWarning() << "Akonadi control did not shut down in time, killing it."; + mAkonadiDaemonProcess->kill(); + } + // we don't use Control::start() since we want to be able to kill + // it forcefully, if necessary, and know the pid + startAkonadiDaemon(); + // from here on, the server exiting is an error again + connect(mAkonadiDaemonProcess, SIGNAL(finished(int)), + this, SLOT(slotAkonadiDaemonProcessFinished(int))); +} + +QString SetupTest::basePath() const +{ + QString sysTempDirPath = QDir::tempPath(); +#ifdef Q_OS_UNIX + // QDir::tempPath() makes sure to use the fully sym-link exploded + // absolute path to the temp dir. That is nice, but on OSX it makes + // that path really long. MySQL chokes on this, for it's socket path, + // so work around that + sysTempDirPath = QStringLiteral("/tmp"); +#endif + + const QDir sysTempDir(sysTempDirPath); + const QString tempDir = QString::fromLatin1("akonadi_testrunner-%1") + .arg(QCoreApplication::instance()->applicationPid()); + if (!sysTempDir.exists(tempDir)) { + sysTempDir.mkdir(tempDir); + } + return sysTempDirPath + QDir::separator() + tempDir + QDir::separator(); +} + +void SetupTest::slotAkonadiDaemonProcessFinished(int exitCode) +{ + if (mTrackAkonadiProcess || exitCode != EXIT_SUCCESS) { + qWarning() << "Akonadi server process was terminated externally!"; + emit serverExited(exitCode); + } + mAkonadiDaemonProcess = 0; +} + +void SetupTest::trackAkonadiProcess(bool track) +{ + mTrackAkonadiProcess = track; +} + +QString SetupTest::instanceId() const +{ + return QStringLiteral("testrunner-") + QString::number(QCoreApplication::instance()->applicationPid()); +} + +void SetupTest::setupInstanceId() +{ + setEnvironmentVariable("AKONADI_INSTANCE", instanceId()); +} + +bool SetupTest::isSetupDone() const +{ + qDebug() << "isSetupDone:" << mSetupJobCount << mExitCode; + return mSetupJobCount == 0 && mExitCode == 0; +} + +void SetupTest::setupFailed() +{ + mExitCode = 1; + shutdown(); +} + +void SetupTest::setEnvironmentVariable(const QByteArray &name, const QString &value) +{ + mEnvVars.push_back(qMakePair(name, value.toLocal8Bit())); + setenv(name.constData(), qPrintable(value), 1); +} + +QVector< SetupTest::EnvVar > SetupTest::environmentVariables() const +{ + return mEnvVars; +} diff --git a/autotests/libs/testrunner/setup.h b/autotests/libs/testrunner/setup.h new file mode 100644 index 0000000..5ad50d0 --- /dev/null +++ b/autotests/libs/testrunner/setup.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2008 Igor Trindade Oliveira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#ifndef SETUP_H +#define SETUP_H + +#include + +#include +#include +#include + +class KProcess; +class KJob; + +class SetupTest : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.Testrunner") + +public: + SetupTest(); + ~SetupTest(); + + /** + Sets the instance identifier for the Akonadi session. + Call this before using any other Akonadi API! + */ + void setupInstanceId(); + bool startAkonadiDaemon(); + void stopAkonadiDaemon(); + QString basePath() const; + + /// Identifier used for the Akonadi session + QString instanceId() const; + + /// set an environment variable + void setEnvironmentVariable(const QByteArray &name, const QString &value); + + /// retrieve all modified environment variables, for writing the shell script + typedef QPair EnvVar; + QVector environmentVariables() const; + +public Q_SLOTS: + Q_SCRIPTABLE void shutdown(); + Q_SCRIPTABLE void shutdownHarder(); + /** Synchronously restarts the server. */ + Q_SCRIPTABLE void restartAkonadiServer(); + Q_SCRIPTABLE void trackAkonadiProcess(bool track); + +Q_SIGNALS: + void setupDone(); + void serverExited(int exitCode); + +private Q_SLOTS: + void serverStateChanged(Akonadi::ServerManager::State state); + void slotAkonadiDaemonProcessFinished(int exitCode); + void agentCreationResult(KJob *job); + void synchronizationResult(KJob *job); + +private: + void setupAgents(); + void copyXdgDirectory(const QString &src, const QString &dst); + void copyDirectory(const QString &src, const QString &dst); + void createTempEnvironment(); + void cleanTempEnvironment(); + bool isSetupDone() const; + void setupFailed(); + +private: + KProcess *mAkonadiDaemonProcess; + bool mShuttingDown; + bool mAgentsCreated; + bool mTrackAkonadiProcess; + int mSetupJobCount; + int mExitCode; + QVector mEnvVars; +}; + +#endif diff --git a/autotests/libs/testrunner/shellscript.cpp b/autotests/libs/testrunner/shellscript.cpp new file mode 100644 index 0000000..f037305 --- /dev/null +++ b/autotests/libs/testrunner/shellscript.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2008 Igor Trindade Oliveira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "shellscript.h" + +#include "config.h" //krazy:exclude=includes + +#include +#include +#include +#include + +ShellScript::ShellScript() +{ +} + +void ShellScript::writeEnvironmentVariables() +{ + foreach (const EnvVar &envvar, mEnvVars) { + mScript += QLatin1String("_old_") + QLatin1String(envvar.first) + QLatin1String("=") + QLatin1String(envvar.first) + QLatin1String("\n"); + mScript.append(QLatin1String(envvar.first)); + mScript.append(QLatin1Char('=')); + mScript.append(QLatin1String(envvar.second)); + mScript.append(QLatin1Char('\n')); + + mScript.append(QLatin1String("export ")); + mScript.append(QLatin1String(envvar.first)); + mScript.append(QLatin1Char('\n')); + } + + mScript.append(QLatin1String("\n\n")); +} + +void ShellScript::writeShutdownFunction() +{ + QString s = + QLatin1String("function shutdown-testenvironment()\n" + "{\n" + " qdbus org.kde.Akonadi.Testrunner-") + QString::number(QCoreApplication::instance()->applicationPid()) + QLatin1String(" / org.kde.Akonadi.Testrunner.shutdown\n"); + + foreach (const EnvVar &envvar, mEnvVars) { + s += QLatin1String(" ") + QLatin1String(envvar.first) + QLatin1String("=$_old_") + QLatin1String(envvar.first) + QLatin1String("\n"); + s += QLatin1String(" export ") + QLatin1String(envvar.first) + QLatin1String("\n"); + } + s.append(QLatin1String("}\n\n")); + mScript.append(s); +} + +void ShellScript::makeShellScript(const QString &fileName) +{ + qDebug() << fileName; + QFile file(fileName); //can user define the file name/location? + + if (file.open(QIODevice::WriteOnly)) { + writeEnvironmentVariables(); + writeShutdownFunction(); + + file.write(mScript.toLatin1().constData(), qstrlen(mScript.toLatin1().constData())); + file.close(); + } else { + qCritical() << "Failed to write" << fileName; + } +} + +void ShellScript::setEnvironmentVariables(const QVector< ShellScript::EnvVar > &envVars) +{ + mEnvVars = envVars; +} diff --git a/autotests/libs/testrunner/shellscript.h b/autotests/libs/testrunner/shellscript.h new file mode 100644 index 0000000..8d7199a --- /dev/null +++ b/autotests/libs/testrunner/shellscript.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2008 Igor Trindade Oliveira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#ifndef SHELLSCRIPT_H +#define SHELLSCRIPT_H + +#include +#include +#include + +class ShellScript +{ +public: + ShellScript(); + void makeShellScript(const QString &filename); + + typedef QPair EnvVar; + void setEnvironmentVariables(const QVector &envVars); + +private: + void writeEnvironmentVariables(); + void writeShutdownFunction(); + + QString mScript; + QVector mEnvVars; +}; +#endif diff --git a/autotests/libs/testrunner/testrunner-config.xsd b/autotests/libs/testrunner/testrunner-config.xsd new file mode 100644 index 0000000..3265358 --- /dev/null +++ b/autotests/libs/testrunner/testrunner-config.xsd @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autotests/libs/testrunner/testrunner.cpp b/autotests/libs/testrunner/testrunner.cpp new file mode 100644 index 0000000..cf45de4 --- /dev/null +++ b/autotests/libs/testrunner/testrunner.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2009 Volker Krause + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "testrunner.h" + +#include +#include + +#include +#include + +TestRunner::TestRunner(const QStringList &args, QObject *parent) + : QObject(parent) + , mArguments(args) + , mExitCode(0) + , mProcess(0) +{ +} + +int TestRunner::exitCode() const +{ + return mExitCode; +} + +void TestRunner::run() +{ + qDebug() << mArguments; + mProcess = new KProcess(this); + mProcess->setProgram(mArguments); + connect(mProcess, SIGNAL(finished(int)), SLOT(processFinished(int))); + connect(mProcess, SIGNAL(error(QProcess::ProcessError)), SLOT(processError(QProcess::ProcessError))); + // environment setup seems to have been done by setuptest globally already + mProcess->start(); + if (!mProcess->waitForStarted()) { + qWarning() << mArguments << "failed to start!"; + mExitCode = 255; + emit finished(); + } +} + +void TestRunner::triggerTermination(int exitCode) +{ + processFinished(exitCode); +} + +void TestRunner::processFinished(int exitCode) +{ + // Only update the exit code when it is 0. This prevents overwriting a non-zero + // value with 0. This can happen when multiple processes finish or triggerTermination + // is called after a proces has finished. + if (mExitCode == 0) { + mExitCode = exitCode; + qDebug() << exitCode; + } + emit finished(); +} + +void TestRunner::processError(QProcess::ProcessError error) +{ + qWarning() << mArguments << "exited with an error:" << error; + mExitCode = 255; + emit finished(); +} + +void TestRunner::terminate() +{ + if (mProcess) { + mProcess->terminate(); + } +} diff --git a/autotests/libs/testrunner/testrunner.h b/autotests/libs/testrunner/testrunner.h new file mode 100644 index 0000000..3f520ce --- /dev/null +++ b/autotests/libs/testrunner/testrunner.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2009 Volker Krause + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#ifndef TESTRUNNER_H +#define TESTRUNNER_H + +#include +#include +#include + +class KProcess; + +class TestRunner : public QObject +{ + Q_OBJECT + +public: + TestRunner(const QStringList &args, QObject *parent = Q_NULLPTR); + int exitCode() const; + void terminate(); + +public Q_SLOTS: + void run(); + void triggerTermination(int); + +Q_SIGNALS: + void finished(); + +private Q_SLOTS: + void processFinished(int exitCode); + void processError(QProcess::ProcessError error); + +private: + QStringList mArguments; + int mExitCode; + KProcess *mProcess; +}; + +#endif // TESTRUNNER_H diff --git a/autotests/libs/testsearchplugin/CMakeLists.txt b/autotests/libs/testsearchplugin/CMakeLists.txt new file mode 100644 index 0000000..6ef2229 --- /dev/null +++ b/autotests/libs/testsearchplugin/CMakeLists.txt @@ -0,0 +1,12 @@ + +include_directories( + ${Boost_INCLUDE_DIR} +) + +kde_enable_exceptions() + +add_library(akonadi_test_searchplugin MODULE testsearchplugin.cpp) + +target_link_libraries(akonadi_test_searchplugin KF5::AkonadiCore) + +install( TARGETS akonadi_test_searchplugin DESTINATION ${KDE_INSTALL_PLUGINDIR}/akonadi ) diff --git a/autotests/libs/testsearchplugin/akonadi_test_searchplugin.json b/autotests/libs/testsearchplugin/akonadi_test_searchplugin.json new file mode 100644 index 0000000..d65f0c0 --- /dev/null +++ b/autotests/libs/testsearchplugin/akonadi_test_searchplugin.json @@ -0,0 +1,7 @@ +{ + "X-Akonadi-PluginType": "SearchPlugin", + "X-Akonadi-Library": "akonadi_test_searchplugin", + "X-Akonadi-LoadByDefault": false, + + "X-Akonadi-PluginName": "Akonadi Test Search Plugin" +} diff --git a/autotests/libs/testsearchplugin/testsearchplugin.cpp b/autotests/libs/testsearchplugin/testsearchplugin.cpp new file mode 100644 index 0000000..7d21f9a --- /dev/null +++ b/autotests/libs/testsearchplugin/testsearchplugin.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2014 Christian Mollekopf + * + * 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 "testsearchplugin.h" + +#include +#include +#include "searchquery.h" + +QSet TestSearchPlugin::search(const QString &query, const QList &collections, const QStringList &mimeTypes) +{ + const QSet result = parseQuery(query); + qDebug() << "PLUGIN QUERY:" << query; + qDebug() << "PLUGIN RESULT:" << result; + return parseQuery(query); +} + +QSet TestSearchPlugin::parseQuery(const QString &queryString) +{ + QSet resultSet; + Akonadi::SearchQuery query = Akonadi::SearchQuery::fromJSON(queryString.toLatin1()); + foreach (const Akonadi::SearchTerm &term, query.term().subTerms()) { + if (term.key() == QStringLiteral("plugin")) { + resultSet << term.value().toInt(); + } + } + return resultSet; +} diff --git a/autotests/libs/testsearchplugin/testsearchplugin.h b/autotests/libs/testsearchplugin/testsearchplugin.h new file mode 100644 index 0000000..80e7032 --- /dev/null +++ b/autotests/libs/testsearchplugin/testsearchplugin.h @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 TESTSEARCHPLUGIN_H +#define TESTSEARCHPLUGIN_H + +#include "../../../src/server/search/abstractsearchplugin.h" +#include +#include + +class TestSearchPlugin : public QObject, public Akonadi::AbstractSearchPlugin +{ + Q_OBJECT + Q_INTERFACES(Akonadi::AbstractSearchPlugin) + Q_PLUGIN_METADATA(IID "org.kde.akonadi.TestSearchPlugin" FILE "akonadi_test_searchplugin.json") +public: + QSet search(const QString &query, const QList &collections, const QStringList &mimeTypes) Q_DECL_OVERRIDE; + + static QSet parseQuery(const QString &queryString); +}; + +#endif diff --git a/autotests/libs/transactiontest.cpp b/autotests/libs/transactiontest.cpp new file mode 100644 index 0000000..0b8cad8 --- /dev/null +++ b/autotests/libs/transactiontest.cpp @@ -0,0 +1,102 @@ +/* + 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. +*/ + +#include "transactiontest.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(TransactionTest) + +void TransactionTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); +} + +void TransactionTest::testTransaction() +{ + Collection basisCollection; + + CollectionFetchJob *listJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); + AKVERIFYEXEC(listJob); + Collection::List list = listJob->collections(); + foreach (const Collection &col, list) + if (col.name() == QLatin1String("res3")) { + basisCollection = col; + } + + Collection testCollection; + testCollection.setParentCollection(basisCollection); + testCollection.setName(QStringLiteral("transactionTest")); + testCollection.setRemoteId(QStringLiteral("transactionTestRemoteId")); + CollectionCreateJob *job = new CollectionCreateJob(testCollection, Session::defaultSession()); + + AKVERIFYEXEC(job); + + testCollection = job->collection(); + + TransactionBeginJob *beginTransaction1 = new TransactionBeginJob(Session::defaultSession()); + AKVERIFYEXEC(beginTransaction1); + + TransactionBeginJob *beginTransaction2 = new TransactionBeginJob(Session::defaultSession()); + AKVERIFYEXEC(beginTransaction2); + + TransactionCommitJob *commitTransaction2 = new TransactionCommitJob(Session::defaultSession()); + AKVERIFYEXEC(commitTransaction2); + + TransactionCommitJob *commitTransaction1 = new TransactionCommitJob(Session::defaultSession()); + AKVERIFYEXEC(commitTransaction1); + + TransactionCommitJob *commitTransactionX = new TransactionCommitJob(Session::defaultSession()); + QVERIFY(commitTransactionX->exec() == false); + + TransactionBeginJob *beginTransaction3 = new TransactionBeginJob(Session::defaultSession()); + AKVERIFYEXEC(beginTransaction3); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("body data"); + ItemCreateJob *appendJob = new ItemCreateJob(item, testCollection, Session::defaultSession()); + AKVERIFYEXEC(appendJob); + + TransactionRollbackJob *rollbackTransaction3 = new TransactionRollbackJob(Session::defaultSession()); + AKVERIFYEXEC(rollbackTransaction3); + + ItemFetchJob *fetchJob = new ItemFetchJob(testCollection, Session::defaultSession()); + AKVERIFYEXEC(fetchJob); + + QVERIFY(fetchJob->items().isEmpty()); + + CollectionDeleteJob *deleteJob = new CollectionDeleteJob(testCollection, Session::defaultSession()); + AKVERIFYEXEC(deleteJob); +} + diff --git a/autotests/libs/transactiontest.h b/autotests/libs/transactiontest.h new file mode 100644 index 0000000..52986ff --- /dev/null +++ b/autotests/libs/transactiontest.h @@ -0,0 +1,33 @@ +/* + 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 TRANSACTIONTEST_H +#define TRANSACTIONTEST_H + +#include + +class TransactionTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testTransaction(); +}; + +#endif diff --git a/autotests/libs/unittestenv/config-mysql-db.xml b/autotests/libs/unittestenv/config-mysql-db.xml new file mode 100644 index 0000000..cfdd1eb --- /dev/null +++ b/autotests/libs/unittestenv/config-mysql-db.xml @@ -0,0 +1,10 @@ + + xdgconfig-mysql.db + xdglocal + akonadi_knut_resource + akonadi_knut_resource + akonadi_knut_resource + true + mysql + testsearchplugin + diff --git a/autotests/libs/unittestenv/config-mysql-fs.xml b/autotests/libs/unittestenv/config-mysql-fs.xml new file mode 100644 index 0000000..63fb5e9 --- /dev/null +++ b/autotests/libs/unittestenv/config-mysql-fs.xml @@ -0,0 +1,10 @@ + + xdgconfig-mysql.fs + xdglocal + akonadi_knut_resource + akonadi_knut_resource + akonadi_knut_resource + true + mysql + testsearchplugin + diff --git a/autotests/libs/unittestenv/config-postgresql-db.xml b/autotests/libs/unittestenv/config-postgresql-db.xml new file mode 100644 index 0000000..436d0b9 --- /dev/null +++ b/autotests/libs/unittestenv/config-postgresql-db.xml @@ -0,0 +1,10 @@ + + xdgconfig-postgresql.db + xdglocal + akonadi_knut_resource + akonadi_knut_resource + akonadi_knut_resource + true + postgresql + testsearchplugin + diff --git a/autotests/libs/unittestenv/config-postgresql-fs.xml b/autotests/libs/unittestenv/config-postgresql-fs.xml new file mode 100644 index 0000000..7200eb7 --- /dev/null +++ b/autotests/libs/unittestenv/config-postgresql-fs.xml @@ -0,0 +1,10 @@ + + xdgconfig-postgresql.fs + xdglocal + akonadi_knut_resource + akonadi_knut_resource + akonadi_knut_resource + true + postgresql + testsearchplugin + diff --git a/autotests/libs/unittestenv/config-sqlite-db.xml b/autotests/libs/unittestenv/config-sqlite-db.xml new file mode 100644 index 0000000..8f793b4 --- /dev/null +++ b/autotests/libs/unittestenv/config-sqlite-db.xml @@ -0,0 +1,10 @@ + + xdgconfig-sqlite.db + xdglocal + akonadi_knut_resource + akonadi_knut_resource + akonadi_knut_resource + true + sqlite + akonadi_test_searchplugin + diff --git a/autotests/libs/unittestenv/config.xml b/autotests/libs/unittestenv/config.xml new file mode 100644 index 0000000..98c2dfa --- /dev/null +++ b/autotests/libs/unittestenv/config.xml @@ -0,0 +1,8 @@ + + xdgconfig + xdglocal + akonadi_knut_resource + akonadi_knut_resource + akonadi_knut_resource + true + diff --git a/autotests/libs/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc b/autotests/libs/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc new file mode 100644 index 0000000..fa9b2d4 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc @@ -0,0 +1,5 @@ +[%General] +ExternalPayload=false + +[Search] +Manager=Dummy diff --git a/autotests/libs/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc b/autotests/libs/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc new file mode 100644 index 0000000..a7bb0c2 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc @@ -0,0 +1,6 @@ +[%General] +SizeThreshold=0 +ExternalPayload=true + +[Search] +Manager=Dummy diff --git a/autotests/libs/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc b/autotests/libs/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc new file mode 100644 index 0000000..b2c8b1a --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc @@ -0,0 +1,9 @@ +[%General] +Driver=QPSQL +ExternalPayload=false + +[Search] +Manager=Dummy + +[QPSQL] +StartServer=true diff --git a/autotests/libs/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc b/autotests/libs/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc new file mode 100644 index 0000000..8333c73 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc @@ -0,0 +1,10 @@ +[%General] +Driver=QPSQL +SizeThreshold=0 +ExternalPayload=true + +[Search] +Manager=Dummy + +[QPSQL] +StartServer=true diff --git a/autotests/libs/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc b/autotests/libs/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc new file mode 100644 index 0000000..33e7f81 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc @@ -0,0 +1,10 @@ +[%General] +# This is a slightly adjusted version of the QSQLITE driver from Qt +# It is provided by akonadi itself +Driver=QSQLITE3 + +[Debug] +Tracer=null + +[Search] +Manager=Dummy diff --git a/autotests/libs/unittestenv/xdgconfig/akonadi-firstrunrc b/autotests/libs/unittestenv/xdgconfig/akonadi-firstrunrc new file mode 100644 index 0000000..c5e90d8 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/akonadi-firstrunrc @@ -0,0 +1,4 @@ +[ProcessedDefaults] +defaultaddressbook=done +defaultcalendar=done +defaultnotebook=done diff --git a/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_0rc b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_0rc new file mode 100644 index 0000000..0d9e3cf --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_0rc @@ -0,0 +1,4 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res1.xml +FileWatchingEnabled=false + diff --git a/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_1rc b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_1rc new file mode 100644 index 0000000..87df3c6 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_1rc @@ -0,0 +1,3 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res2.xml +FileWatchingEnabled=false diff --git a/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_2rc b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_2rc new file mode 100644 index 0000000..274fbfc --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_2rc @@ -0,0 +1,3 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res3.xml +FileWatchingEnabled=false diff --git a/autotests/libs/unittestenv/xdgconfig/kdebugrc b/autotests/libs/unittestenv/xdgconfig/kdebugrc new file mode 100644 index 0000000..a8438b9 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/kdebugrc @@ -0,0 +1,80 @@ +DisableAll=false + +[0] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=2 +FatalFilename[$e]=kdebug.dbg +FatalOutput=2 +InfoFilename[$e]=kdebug.dbg +InfoOutput=2 +WarnFilename[$e]=kdebug.dbg +WarnOutput=2 + +[264] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 + +[5250] +InfoOutput=2 + +[7009] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +InfoOutput=4 +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 + +[7011] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +InfoOutput=4 +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 + +[7012] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +InfoOutput=4 +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 + +[7014] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=0 +FatalFilename[$e]=kdebug.dbg +FatalOutput=0 +InfoFilename[$e]=kdebug.dbg +InfoOutput=0 +WarnFilename[$e]=kdebug.dbg +WarnOutput=0 + +[7021] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +InfoOutput=4 +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 diff --git a/autotests/libs/unittestenv/xdgconfig/kdedrc b/autotests/libs/unittestenv/xdgconfig/kdedrc new file mode 100644 index 0000000..41d1781 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/kdedrc @@ -0,0 +1,3 @@ +[General] +CheckSycoca=false +CheckFileStamps=false diff --git a/autotests/libs/unittestenv/xdglocal/testdata-res1.xml b/autotests/libs/unittestenv/xdglocal/testdata-res1.xml new file mode 100644 index 0000000..db51834 --- /dev/null +++ b/autotests/libs/unittestenv/xdglocal/testdata-res1.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + testmailbody + From: <test@user.tst> + \SEEN + \FLAGGED + \DRAFT + + + testmailbody1 + From: <test1@user.tst> + \FLAGGED + tagrid + + + testmailbody2 + From: <test2@user.tst> + + + testmailbody3 + From: <test3@user.tst> + + + testmailbody4 + From: <test4@user.tst> + + + testmailbody5 + From: <test5@user.tst> + + + testmailbody6 + From: <test6@user.tst> + + + testmailbody7 + From: <test7@user.tst> + + + testmailbody8 + From: <test8@user.tst> + + + testmailbody9 + From: <test9@user.tst> + + + testmailbody10 + From: <test10@user.tst> + + + testmailbody11 + From: <test11@user.tst> + + + testmailbody12 + From: <test12@user.tst> + + + testmailbody13 + From: <test13@user.tst> + + + testmailbody14 + From: <test14@user.tst> + + + + + diff --git a/autotests/libs/unittestenv/xdglocal/testdata-res2.xml b/autotests/libs/unittestenv/xdglocal/testdata-res2.xml new file mode 100644 index 0000000..b12f3b3 --- /dev/null +++ b/autotests/libs/unittestenv/xdglocal/testdata-res2.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/autotests/libs/unittestenv/xdglocal/testdata-res3.xml b/autotests/libs/unittestenv/xdglocal/testdata-res3.xml new file mode 100644 index 0000000..0c3b7a8 --- /dev/null +++ b/autotests/libs/unittestenv/xdglocal/testdata-res3.xml @@ -0,0 +1,4 @@ + + + + diff --git a/autotests/libs/unittestenv/xdglocal/testdata.xml b/autotests/libs/unittestenv/xdglocal/testdata.xml new file mode 100644 index 0000000..06559c2 --- /dev/null +++ b/autotests/libs/unittestenv/xdglocal/testdata.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + testmailbody + From: <test@user.tst> + \SEEN + \FLAGGED + \DRAFT + + + testmailbody1 + From: <test1@user.tst> + \FLAGGED + + + testmailbody2 + From: <test2@user.tst> + + + testmailbody3 + From: <test3@user.tst> + + + testmailbody4 + From: <test4@user.tst> + + + testmailbody5 + From: <test5@user.tst> + + + testmailbody6 + From: <test6@user.tst> + + + testmailbody7 + From: <test7@user.tst> + + + testmailbody8 + From: <test8@user.tst> + + + testmailbody9 + From: <test9@user.tst> + + + testmailbody10 + From: <test10@user.tst> + + + testmailbody11 + From: <test11@user.tst> + + + testmailbody12 + From: <test12@user.tst> + + + testmailbody13 + From: <test13@user.tst> + + + testmailbody14 + From: <test14@user.tst> + + + + + + + + + + diff --git a/autotests/libs/virtualresource.cpp b/autotests/libs/virtualresource.cpp new file mode 100644 index 0000000..b126bfd --- /dev/null +++ b/autotests/libs/virtualresource.cpp @@ -0,0 +1,111 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "virtualresource.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EXEC(job) \ + do { \ + if (!job->exec()) { \ + kFatal() << "Job failed: " << job->errorString(); \ + } \ + } while ( 0 ) + +using namespace Akonadi; + +VirtualResource::VirtualResource(const QString &name, QObject *parent) + : QObject(parent), + mResourceName(name) +{ + // QDBusInterface *interface = new QDBusInterface(ServerManager::serviceName(ServerManager::Control), + // QString::fromLatin1("/"), + // QString::fromLatin1("org.freedesktop.Akonadi.AgentManager"), + // DBusConnectionPool::threadConnection(), this); + // if (interface->isValid()) { + // const QDBusMessage reply = interface->call(QString::fromUtf8("createAgentInstance"), name, QStringList()); + // if (reply.type() == QDBusMessage::ErrorMessage) { + // // This means that the resource doesn't provide a synchronizeCollectionAttributes method, so we just finish the job + // return; + // } + // } else { + // Q_ASSERT(false); + // } + // mSession = new Akonadi::Session(name.toLatin1(), this); + + // Since this is in the same process as the test, all jobs in the test get executed in the resource session by default + SessionPrivate::createDefaultSession(name.toLatin1()); + mSession = Session::defaultSession(); + ResourceSelectJob *select = new ResourceSelectJob(name, mSession); + EXEC(select); +} + +VirtualResource::~VirtualResource() +{ + if (mRootCollection.isValid()) { + CollectionDeleteJob *d = new CollectionDeleteJob(mRootCollection, mSession); + EXEC(d); + } +} + +Akonadi::Collection VirtualResource::createCollection(const Akonadi::Collection &collection) +{ + // kDebug() << collection.name() << collection.parentCollection().remoteId(); + // kDebug() << "contentMimeTypes: " << collection.contentMimeTypes(); + + Q_ASSERT(!collection.name().isEmpty()); + Collection col = collection; + if (!col.parentCollection().isValid()) { + col.setParentCollection(mRootCollection); + } + CollectionCreateJob *create = new CollectionCreateJob(col, mSession); + EXEC(create); + return create->collection(); +} +Akonadi::Collection VirtualResource::createRootCollection(const Akonadi::Collection &collection) +{ + kDebug() << collection.name(); + mRootCollection = createCollection(collection); + return mRootCollection; +} + +Akonadi::Item VirtualResource::createItem(const Akonadi::Item &item, const Collection &parent) +{ + ItemCreateJob *create = new ItemCreateJob(item, parent, mSession); + EXEC(create); + return create->item(); +} + +void VirtualResource::reset() +{ + Q_ASSERT(mRootCollection.isValid()); + Akonadi::Collection col = mRootCollection; + CollectionDeleteJob *d = new CollectionDeleteJob(mRootCollection, mSession); + EXEC(d); + col.setId(-1); + createRootCollection(col); +} + diff --git a/autotests/libs/virtualresource.h b/autotests/libs/virtualresource.h new file mode 100644 index 0000000..092deda --- /dev/null +++ b/autotests/libs/virtualresource.h @@ -0,0 +1,54 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 VIRTUALRESOURCE_H +#define VIRTUALRESOURCE_H + +#include +#include +#include + +namespace Akonadi +{ + +/** + * For testing only. + * + */ +class AKONADI_EXPORT VirtualResource : public QObject +{ + Q_OBJECT +public: + VirtualResource(const QString &name, QObject *parent = 0); + ~VirtualResource(); + + Akonadi::Collection createCollection(const Akonadi::Collection &collection); + Akonadi::Collection createRootCollection(const Akonadi::Collection &collection); + Akonadi::Item createItem(const Akonadi::Item &item, const Akonadi::Collection &parent); + + void reset(); +private: + Akonadi::Collection mRootCollection; + QString mResourceName; + Akonadi::Session *mSession; +}; + +} + +#endif diff --git a/autotests/private/CMakeLists.txt b/autotests/private/CMakeLists.txt new file mode 100644 index 0000000..44a45fa --- /dev/null +++ b/autotests/private/CMakeLists.txt @@ -0,0 +1,54 @@ +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) + +include_directories(${Akonadi_SOURCE_DIR}/src/private + ${Akonadi_BINARY_DIR}/src/private) + +macro(add_unit_test _source) + set(_test ${_source}) + get_filename_component(_name ${_source} NAME_WE) + add_executable(${_name} ${_source}) + add_test(AkonadiPrivate-${_name} ${_name}) + if (ENABLE_ASAN) + set_tests_properties(AkonadiPrivate-${_name} PROPERTIES + ENVIRONMENT ASAN_OPTIONS=symbolize=1 + ) + endif() + target_link_libraries(${_name} + akonadi_shared + KF5AkonadiPrivate + Qt5::Network + Qt5::Widgets + Qt5::Test + ${CMAKE_EXE_LINKER_FLAGS_ASAN} + ) +endmacro() + +macro(add_protocol_test _source) + set(_test ${_source} + protocoltest.cpp + ${Akonadi_SOURCE_DIR}/src/private/imapset.cpp + ${Akonadi_SOURCE_DIR}/src/private/scope.cpp + ${Akonadi_SOURCE_DIR}/src/private/protocol.cpp + ${Akonadi_SOURCE_DIR}/src/private/datastream_p.cpp + ${Akonadi_SOURCE_DIR}/src/private/tristate.cpp + ) + get_filename_component(_name ${_source} NAME_WE) + add_executable(${_name} ${_test}) + add_test(AkonadiPrivate-${_name} ${_name}) + if (ENABLE_ASAN) + set_tests_properties(AkonadiPrivate-${_name} PROPERTIES + ENVIRONMENT ASAN_OPTIONS=symbolize=1 + ) + endif() + target_link_libraries(${_name} + Qt5::Network + Qt5::Test + ${CMAKE_EXE_LINKER_FLAGS_ASAN} + ) +endmacro() + +add_unit_test(akstandarddirstest.cpp) +add_unit_test(akdbustest.cpp) +add_unit_test(notificationmessagetest.cpp) +add_unit_test(externalpartstoragetest.cpp) +add_protocol_test(protocoltest.cpp) diff --git a/autotests/private/akdbustest.cpp b/autotests/private/akdbustest.cpp new file mode 100644 index 0000000..3d6c068 --- /dev/null +++ b/autotests/private/akdbustest.cpp @@ -0,0 +1,97 @@ +/* + Copyright (c) 2011 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 + +#include + +#include + +using namespace Akonadi; + +Q_DECLARE_METATYPE(DBus::AgentType) + +class DBusTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testServiceName() + { + akTestSetInstanceIdentifier(QString()); + QCOMPARE(DBus::serviceName(DBus::Server), QLatin1String("org.freedesktop.Akonadi")); + akTestSetInstanceIdentifier(QLatin1String("foo")); + QCOMPARE(DBus::serviceName(DBus::Server), QLatin1String("org.freedesktop.Akonadi.foo")); + } + + void testParseAgentServiceName_data() + { + QTest::addColumn("instanceId"); + QTest::addColumn("serviceName"); + QTest::addColumn("agentId"); + QTest::addColumn("agentType"); + + // generic invalid + QTest::newRow("empty") << QString() << QString() << QString() << DBus::Unknown; + QTest::newRow("wrong base") << QString() << "org.freedesktop.Agent.foo" << QString() << DBus::Unknown; + QTest::newRow("wrong type") << QString() << "org.freedesktop.Akonadi.Randomizer.akonadi_maildir_resource_0" << QString() << DBus::Unknown; + QTest::newRow("too long") << QString() << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo.bar" << QString() << DBus::Unknown; + + // single instance cases + QTest::newRow("agent, no multi-instance") << QString() << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0" << "akonadi_maildir_resource_0" << DBus::Agent; + QTest::newRow("resource, no multi-instance") << QString() << "org.freedesktop.Akonadi.Resource.akonadi_maildir_resource_0" << "akonadi_maildir_resource_0" << DBus::Resource; + QTest::newRow("preproc, no multi-instance") << QString() << "org.freedesktop.Akonadi.Preprocessor.akonadi_maildir_resource_0" << "akonadi_maildir_resource_0" << DBus::Preprocessor; + QTest::newRow("multi-instance name in single-instance setup") << QString() << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo" << QString() << DBus::Unknown; + + // multi-instance cases + QTest::newRow("agent, multi-instance") << "foo" << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo" << "akonadi_maildir_resource_0" << DBus::Agent; + QTest::newRow("resource, multi-instance") << "foo" << "org.freedesktop.Akonadi.Resource.akonadi_maildir_resource_0.foo" << "akonadi_maildir_resource_0" << DBus::Resource; + QTest::newRow("preproc, multi-instance") << "foo" << "org.freedesktop.Akonadi.Preprocessor.akonadi_maildir_resource_0.foo" << "akonadi_maildir_resource_0" << DBus::Preprocessor; + QTest::newRow("single-instance name in multi-instance setup") << "foo" << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0" << QString() << DBus::Unknown; + } + + void testParseAgentServiceName() + { + QFETCH(QString, instanceId); + QFETCH(QString, serviceName); + QFETCH(QString, agentId); + QFETCH(DBus::AgentType, agentType); + + akTestSetInstanceIdentifier(instanceId); + + DBus::AgentType parsedType; + QString parsedName = DBus::parseAgentServiceName(serviceName, parsedType); + + QCOMPARE(parsedName, agentId); + QCOMPARE(parsedType, agentType); + } + + void testAgentServiceName() + { + akTestSetInstanceIdentifier(QString()); + QCOMPARE(DBus::agentServiceName(QLatin1String("akonadi_maildir_resource_0"), DBus::Agent), QLatin1String("org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0")); + + akTestSetInstanceIdentifier(QLatin1String("foo")); + QCOMPARE(DBus::agentServiceName(QLatin1String("akonadi_maildir_resource_0"), DBus::Agent), QLatin1String("org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo")); + } +}; + +AKTEST_MAIN(DBusTest) + +#include "akdbustest.moc" diff --git a/autotests/private/akstandarddirstest.cpp b/autotests/private/akstandarddirstest.cpp new file mode 100644 index 0000000..8ff9d77 --- /dev/null +++ b/autotests/private/akstandarddirstest.cpp @@ -0,0 +1,60 @@ +/* + Copyright (c) 2011 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 + +#include +#include +#include + +#define QL1S(x) QStringLiteral(x) + +using namespace Akonadi; + +class AkStandardDirsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testCondigFile() + { + akTestSetInstanceIdentifier(QString()); + QVERIFY(StandardDirs::agentConfigFile(XdgBaseDirs::ReadOnly).endsWith(QL1S("agentsrc"))); + QVERIFY(StandardDirs::agentConfigFile(XdgBaseDirs::ReadWrite).endsWith(QL1S("agentsrc"))); + QVERIFY(!StandardDirs::agentConfigFile(XdgBaseDirs::ReadWrite).endsWith(QL1S("foo/agentsrc"))); + + akTestSetInstanceIdentifier(QL1S("foo")); + QVERIFY(StandardDirs::agentConfigFile(XdgBaseDirs::ReadOnly).endsWith(QL1S("agentsrc"))); + QVERIFY(StandardDirs::agentConfigFile(XdgBaseDirs::ReadWrite).endsWith(QL1S("instance/foo/agentsrc"))); + } + + void testSaveDir() + { + akTestSetInstanceIdentifier(QString()); + QVERIFY(StandardDirs::saveDir("data").endsWith(QL1S("/akonadi"))); + QVERIFY(!StandardDirs::saveDir("data").endsWith(QL1S("foo/akonadi"))); + + akTestSetInstanceIdentifier(QL1S("foo")); + QVERIFY(StandardDirs::saveDir("data").endsWith(QL1S("/akonadi/instance/foo"))); + } +}; + +AKTEST_MAIN(AkStandardDirsTest) + +#include "akstandarddirstest.moc" diff --git a/autotests/private/externalpartstoragetest.cpp b/autotests/private/externalpartstoragetest.cpp new file mode 100644 index 0000000..9025695 --- /dev/null +++ b/autotests/private/externalpartstoragetest.cpp @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2015 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; + +class ExternalPartStorageTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testResolveAbsolutePath_data(); + void testResolveAbsolutePath(); + + void testUpdateFileNameRevision_data(); + void testUpdateFileNameRevision(); + + void testNameForPartId_data(); + void testNameForPartId(); + + void testPartCreate(); + void testPartUpdate(); + void testPartDelete(); + void testPartCreateTrxRollback(); + void testPartUpdateTrxRollback(); + void testPartDeleteTrxRollback(); + void testPartCreateTrxCommit(); + void testPartUpdateTrxCommit(); + void testPartDeleteTrxCommit(); +}; + +void ExternalPartStorageTest::testResolveAbsolutePath_data() +{ + QTest::addColumn("filename"); + QTest::addColumn("expectedLevelCache"); + QTest::addColumn("isAbsolute"); + + QTest::newRow("1_r0") << QStringLiteral("1_r0") << QStringLiteral("01") << false; + QTest::newRow("23_r0") << QStringLiteral("23_r0") << QStringLiteral("23") << false; + QTest::newRow("567_r0") << QStringLiteral("567_r0") << QStringLiteral("67") << false; + QTest::newRow("123456_r0") << QStringLiteral("123456_r0") << QStringLiteral("56") << false; + QTest::newRow("absolute path") << QStringLiteral("/tmp/akonadi/file_db_data/99_r3") << QString() << true; +} + + +void ExternalPartStorageTest::testResolveAbsolutePath() +{ + QFETCH(QString, filename); + QFETCH(QString, expectedLevelCache); + QFETCH(bool, isAbsolute); + + // Calculate the new levelled-cache hierarchy path + const QString expectedBasePath = StandardDirs::saveDir("data", QStringLiteral("file_db_data")) + QDir::separator(); + const QString expectedPath = expectedBasePath + expectedLevelCache + QDir::separator(); + const QString expectedFilePath = expectedPath + filename; + + // File does not exist, will return path for the new levelled hierarchy + // (unless the path is absolute, then it returns the absolute path) + bool exists = false; + QString path = ExternalPartStorage::resolveAbsolutePath(filename, &exists); + QVERIFY(!exists); + if (isAbsolute) { + // Absolute path is no further resolved + QCOMPARE(path, filename); + return; + } + QCOMPARE(path, expectedFilePath); + + // Create the file in the old flat heirarchy + QFile(expectedBasePath + filename).open(QIODevice::WriteOnly); + QCOMPARE(ExternalPartStorage::resolveAbsolutePath(filename, &exists), QString(expectedBasePath + filename)); + QVERIFY(exists); + QVERIFY(QFile::remove(expectedBasePath + filename)); + + // Create the file in the new hierarchy - will return the same as the first + QDir().mkpath(expectedPath); + QFile(expectedFilePath).open(QIODevice::WriteOnly); + exists = false; + // Check that this time we got the new path and exists flag is correctly set + path = ExternalPartStorage::resolveAbsolutePath(filename, &exists); + QCOMPARE(path, expectedFilePath); + QVERIFY(exists); + + // Clean up + QVERIFY(QFile::remove(path)); +} + +void ExternalPartStorageTest::testUpdateFileNameRevision_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expectedName"); + + QTest::newRow("no revision") << QByteArray("1234") << QByteArray("1234_r0"); + QTest::newRow("r0") << QByteArray("1234_r0") << QByteArray("1234_r1"); + QTest::newRow("r12") << QByteArray("1234_r12") << QByteArray("1234_r13"); + QTest::newRow("r123456") << QByteArray("1234_r123456") << QByteArray("1234_r123457"); +} + +void ExternalPartStorageTest::testUpdateFileNameRevision() +{ + QFETCH(QByteArray, name); + QFETCH(QByteArray, expectedName); + + const QByteArray newName = ExternalPartStorage::updateFileNameRevision(name); + QCOMPARE(newName, expectedName); +} + +void ExternalPartStorageTest::testNameForPartId_data() +{ + QTest::addColumn("id"); + QTest::addColumn("expectedName"); + + QTest::newRow("0") << 0ll << QByteArray("0_r0"); + QTest::newRow("12") << 12ll << QByteArray("12_r0"); + QTest::newRow("9876543") << 9876543ll << QByteArray("9876543_r0"); +} + + +void ExternalPartStorageTest::testNameForPartId() +{ + QFETCH(qint64, id); + QFETCH(QByteArray, expectedName); + + const QByteArray name = ExternalPartStorage::nameForPartId(id); + QCOMPARE(name, expectedName); +} + +void ExternalPartStorageTest::testPartCreate() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 1, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + QFile f(filePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("blabla")); + f.close(); + QVERIFY(f.remove()); +} + +void ExternalPartStorageTest::testPartUpdate() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 10, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + QByteArray newfilename; + QVERIFY(ExternalPartStorage::self()->updatePartFile("newdata", filename, newfilename)); + QCOMPARE(ExternalPartStorage::updateFileNameRevision(filename), newfilename); + const QString newFilePath = ExternalPartStorage::resolveAbsolutePath(newfilename); + QVERIFY(!QFile::exists(filePath)); + QVERIFY(QFile::exists(newFilePath)); + + QFile f(newFilePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("newdata")); + f.close(); + QVERIFY(f.remove()); +} + +void ExternalPartStorageTest::testPartDelete() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 2, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + QVERIFY(ExternalPartStorage::self()->removePartFile(filePath)); + QVERIFY(!QFile::exists(filePath)); +} + + +void ExternalPartStorageTest::testPartCreateTrxRollback() +{ + ExternalPartStorageTransaction trx; + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 3, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + QFile f(filePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("blabla")); + f.close(); + QVERIFY(trx.rollback()); + QVERIFY(!QFile::exists(filePath)); +} + +void ExternalPartStorageTest::testPartUpdateTrxRollback() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 10, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + ExternalPartStorageTransaction trx; + + QByteArray newfilename; + QVERIFY(ExternalPartStorage::self()->updatePartFile("newdata", filename, newfilename)); + QCOMPARE(ExternalPartStorage::updateFileNameRevision(filename), newfilename); + const QString newFilePath = ExternalPartStorage::resolveAbsolutePath(newfilename); + QVERIFY(QFile::exists(filePath)); + QVERIFY(QFile::exists(newFilePath)); + + QFile f(newFilePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("newdata")); + f.close(); + + trx.rollback(); + QVERIFY(QFile::exists(filePath)); + QVERIFY(!QFile::exists(newFilePath)); + + QFile f2(filePath); + QVERIFY(f2.open(QIODevice::ReadOnly)); + QCOMPARE(f2.readAll(), QByteArray("blabla")); + f2.close(); + QVERIFY(f2.remove()); +} + +void ExternalPartStorageTest::testPartDeleteTrxRollback() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 4, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + ExternalPartStorageTransaction trx; + QVERIFY(ExternalPartStorage::self()->removePartFile(filePath)); + QVERIFY(QFile::exists(filePath)); + QVERIFY(trx.rollback()); + QVERIFY(QFile::exists(filePath)); + + QFile::remove(filePath); +} + +void ExternalPartStorageTest::testPartCreateTrxCommit() +{ + ExternalPartStorageTransaction trx; + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 6, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + QVERIFY(trx.commit()); + QVERIFY(QFile::exists(filePath)); + + QFile f(filePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("blabla")); + f.close(); + QVERIFY(f.remove()); +} + +void ExternalPartStorageTest::testPartUpdateTrxCommit() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 10, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + ExternalPartStorageTransaction trx; + + QByteArray newfilename; + QVERIFY(ExternalPartStorage::self()->updatePartFile("newdata", filename, newfilename)); + QCOMPARE(ExternalPartStorage::updateFileNameRevision(filename), newfilename); + const QString newFilePath = ExternalPartStorage::resolveAbsolutePath(newfilename); + QVERIFY(QFile::exists(filePath)); + QVERIFY(QFile::exists(newFilePath)); + + QFile f(newFilePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("newdata")); + f.close(); + + trx.commit(); + QVERIFY(!QFile::exists(filePath)); + QVERIFY(QFile::exists(newFilePath)); + QVERIFY(QFile::remove(newFilePath)); +} + +void ExternalPartStorageTest::testPartDeleteTrxCommit() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 7, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + ExternalPartStorageTransaction trx; + QVERIFY(ExternalPartStorage::self()->removePartFile(filePath)); + QVERIFY(QFile::exists(filePath)); + QVERIFY(trx.commit()); + QVERIFY(!QFile::exists(filePath)); +} + + + + +AKTEST_MAIN(ExternalPartStorageTest) + +#include "externalpartstoragetest.moc" diff --git a/autotests/private/notificationmessagetest.cpp b/autotests/private/notificationmessagetest.cpp new file mode 100644 index 0000000..759fd71 --- /dev/null +++ b/autotests/private/notificationmessagetest.cpp @@ -0,0 +1,105 @@ +/* + 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 "notificationmessagetest.h" +#include + +#include +#include + +QTEST_APPLESS_MAIN(NotificationMessageTest) + +using namespace Akonadi; +using namespace Akonadi::Protocol; + +void NotificationMessageTest::testCompress() +{ + ChangeNotification::List list; + ChangeNotification msg; + msg.setType(ChangeNotification::Collections); + msg.setOperation(ChangeNotification::Add); + + ChangeNotification::appendAndCompress(list, msg); + QCOMPARE(list.count(), 1); + + msg.setOperation(ChangeNotification::Modify); + ChangeNotification::appendAndCompress(list, msg); + QCOMPARE(list.count(), 1); + QCOMPARE(list.first().operation(), ChangeNotification::Add); + + msg.setOperation(ChangeNotification::Remove); + ChangeNotification::appendAndCompress(list, msg); + QCOMPARE(list.count(), 2); +} + +void NotificationMessageTest::testCompress2() +{ + ChangeNotification::List list; + ChangeNotification msg; + msg.setType(ChangeNotification::Collections); + msg.setOperation(ChangeNotification::Modify); + + ChangeNotification::appendAndCompress(list, msg); + QCOMPARE(list.count(), 1); + + msg.setOperation(ChangeNotification::Remove); + ChangeNotification::appendAndCompress(list, msg); + QCOMPARE(list.count(), 2); + QCOMPARE(list.first().operation(), ChangeNotification::Modify); + QCOMPARE(list.last().operation(), ChangeNotification::Remove); +} + +void NotificationMessageTest::testCompress3() +{ + ChangeNotification::List list; + ChangeNotification msg; + msg.setType(ChangeNotification::Collections); + msg.setOperation(ChangeNotification::Modify); + + ChangeNotification::appendAndCompress(list, msg); + QCOMPARE(list.count(), 1); + + ChangeNotification::appendAndCompress(list, msg); + QCOMPARE(list.count(), 1); +} + +void NotificationMessageTest::testPartModificationMerge_data() +{ + QTest::addColumn("type"); + QTest::newRow("collection") << ChangeNotification::Collections; +} + +void NotificationMessageTest::testPartModificationMerge() +{ + QFETCH(ChangeNotification::Type, type); + + ChangeNotification::List list; + ChangeNotification msg; + msg.setType(type); + msg.setOperation(ChangeNotification::Modify); + msg.setItemParts(QSet() << "PART1"); + + ChangeNotification::appendAndCompress(list, msg); + QCOMPARE(list.count(), 1); + + msg.setItemParts(QSet() << "PART2"); + ChangeNotification::appendAndCompress(list, msg); + QCOMPARE(list.count(), 1); + QCOMPARE(list.first().itemParts(), (QSet() << "PART1" << "PART2")); +} diff --git a/autotests/private/notificationmessagetest.h b/autotests/private/notificationmessagetest.h new file mode 100644 index 0000000..3f07090 --- /dev/null +++ b/autotests/private/notificationmessagetest.h @@ -0,0 +1,36 @@ +/* + 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_NOTIFICATIONMESSAGETEST_H +#define AKONADI_NOTIFICATIONMESSAGETEST_H + +#include + +class NotificationMessageTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testCompress(); + void testCompress2(); + void testCompress3(); + void testPartModificationMerge_data(); + void testPartModificationMerge(); +}; + +#endif diff --git a/autotests/private/protocoltest.cpp b/autotests/private/protocoltest.cpp new file mode 100644 index 0000000..17bb636 --- /dev/null +++ b/autotests/private/protocoltest.cpp @@ -0,0 +1,701 @@ +/* + * Copyright (C) 2015 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "protocoltest.h" + +#include "private/scope_p.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Protocol; + +void ProtocolTest::testProtocolVersion() +{ + // So, this test started failing because you bumped protocol version in + // protocol.cpp. That means that you most probably changed something + // in the protocol. Before you just bump the number here to make the test + // pass, please please pretty please make sure to extend the respective + // test somewhere below to cover the change you've made. Protocol is the + // most critical part of Akonadi and we can't afford not having it tested + // properly. + // + // If it wasn't you who broke it, please go find that person who was too + // lazy to extend the test case and beat them with a stick. -- Dan + + QCOMPARE(Akonadi::Protocol::version(), 53); +} + +void ProtocolTest::testFactory_data() +{ + QTest::addColumn("type"); + QTest::addColumn("response"); + QTest::addColumn("success"); + + QTest::newRow("invalid cmd") << Command::Invalid << false << false; + QTest::newRow("invalid resp") << Command::Invalid << true << false; + QTest::newRow("hello cmd") << Command::Hello << false << false; + QTest::newRow("hello resp") << Command::Hello << true << true; + QTest::newRow("login cmd") << Command::Login << false << true; + QTest::newRow("login resp") << Command::Login << true << true; + QTest::newRow("logout cmd") << Command::Logout << false << true; + QTest::newRow("logout resp") << Command::Logout << true << true; + QTest::newRow("transaction cmd") << Command::Transaction << false << true; + QTest::newRow("transaction resp") << Command::Transaction << true << true; + QTest::newRow("createItem cmd") << Command::CreateItem << false << true; + QTest::newRow("createItem resp") << Command::CreateItem << true << true; + QTest::newRow("copyItems cmd") << Command::CopyItems << false << true; + QTest::newRow("copyItems resp") << Command::CopyItems << true << true; + QTest::newRow("deleteItems cmd") << Command::DeleteItems << false << true; + QTest::newRow("deleteItems resp") << Command::DeleteItems << true << true; + QTest::newRow("fetchItems cmd") << Command::FetchItems << false << true; + QTest::newRow("fetchItems resp") << Command::FetchItems << true << true; + QTest::newRow("linkItems cmd") << Command::LinkItems << false << true; + QTest::newRow("linkItems resp") << Command::LinkItems << true << true; + QTest::newRow("modifyItems cmd") << Command::ModifyItems << false << true; + QTest::newRow("modifyItems resp") << Command::ModifyItems << true << true; + QTest::newRow("moveItems cmd") << Command::MoveItems << false << true; + QTest::newRow("moveItems resp") << Command::MoveItems << true << true; + QTest::newRow("createCollection cmd") << Command::CreateCollection << false << true; + QTest::newRow("createCollection resp") << Command::CreateCollection << true << true; + QTest::newRow("copyCollection cmd") << Command::CopyCollection << false << true; + QTest::newRow("copyCollection resp") << Command::CopyCollection << true << true; + QTest::newRow("deleteCollection cmd") << Command::DeleteCollection << false << true; + QTest::newRow("deleteCollection resp") << Command::DeleteCollection << true << true; + QTest::newRow("fetchCollections cmd") << Command::FetchCollections << false << true; + QTest::newRow("fetchCollections resp") << Command::FetchCollections << true << true; + QTest::newRow("fetchCollectionStats cmd") << Command::FetchCollectionStats << false << true; + QTest::newRow("fetchCollectionStats resp") << Command::FetchCollectionStats << false << true; + QTest::newRow("modifyCollection cmd") << Command::ModifyCollection << false << true; + QTest::newRow("modifyCollection resp") << Command::ModifyCollection << true << true; + QTest::newRow("moveCollection cmd") << Command::MoveCollection << false << true; + QTest::newRow("moveCollection resp") << Command::MoveCollection << true << true; + QTest::newRow("search cmd") << Command::Search << false << true; + QTest::newRow("search resp") << Command::Search << true << true; + QTest::newRow("searchResult cmd") << Command::SearchResult << false << true; + QTest::newRow("searchResult resp") << Command::SearchResult << true << true; + QTest::newRow("storeSearch cmd") << Command::StoreSearch << false << true; + QTest::newRow("storeSearch resp") << Command::StoreSearch << true << true; + QTest::newRow("createTag cmd") << Command::CreateTag << false << true; + QTest::newRow("createTag resp") << Command::CreateTag << true << true; + QTest::newRow("deleteTag cmd") << Command::DeleteTag << false << true; + QTest::newRow("deleteTag resp") << Command::DeleteTag << true << true; + QTest::newRow("fetchTags cmd") << Command::FetchTags << false << true; + QTest::newRow("fetchTags resp") << Command::FetchTags << true << true; + QTest::newRow("modifyTag cmd") << Command::ModifyTag << false << true; + QTest::newRow("modifyTag resp") << Command::ModifyTag << true << true; + QTest::newRow("fetchRelations cmd") << Command::FetchRelations << false << true; + QTest::newRow("fetchRelations resp") << Command::FetchRelations << true << true; + QTest::newRow("modifyRelation cmd") << Command::ModifyRelation << false << true; + QTest::newRow("modifyRelation resp") << Command::ModifyRelation << true << true; + QTest::newRow("removeRelations cmd") << Command::RemoveRelations << false << true; + QTest::newRow("removeRelations resp") << Command::RemoveRelations << true << true; + QTest::newRow("selectResource cmd") << Command::SelectResource << false << true; + QTest::newRow("selectResource resp") << Command::SelectResource << true << true; + QTest::newRow("streamPayload cmd") << Command::StreamPayload << false << true; + QTest::newRow("streamPayload resp") << Command::StreamPayload << true << true; + QTest::newRow("changeNotification cmd") << Command::ChangeNotification << false << true; + QTest::newRow("changeNotification resp") << Command::ChangeNotification << true << false; + QTest::newRow("_responseBit cmd") << Command::_ResponseBit << false << false; + QTest::newRow("_responseBit resp") << Command::_ResponseBit << true << false; +} + +void ProtocolTest::testFactory() +{ + QFETCH(Command::Type, type); + QFETCH(bool, response); + QFETCH(bool, success); + + Command result; + if (response) { + result = Factory::response(type); + } else { + result = Factory::command(type); + } + + QCOMPARE(result.isValid(), success); + QCOMPARE(result.isResponse(), response); + if (success) { + QCOMPARE(result.type(), type); + } +} + + + +void ProtocolTest::testCommand() +{ + // There is no way to construct a valid Command directly + Command cmd; + QCOMPARE(cmd.type(), Command::Invalid); + QVERIFY(!cmd.isValid()); + QVERIFY(!cmd.isResponse()); + + Command cmdTest = serializeAndDeserialize(cmd); + QCOMPARE(cmdTest.type(), Command::Invalid); + QVERIFY(!cmd.isValid()); + QVERIFY(!cmd.isResponse()); +} + +void ProtocolTest::testResponse_data() +{ + QTest::addColumn("isError"); + QTest::addColumn("errorCode"); + QTest::addColumn("errorString"); + + QTest::newRow("no error") << false << 0 << QString(); + QTest::newRow("error") << true << 10 << QStringLiteral("Oh noes, there was an error!"); +} + +void ProtocolTest::testResponse() +{ + QFETCH(bool, isError); + QFETCH(int, errorCode); + QFETCH(QString, errorString); + + Response response; + if (isError) { + response.setError(errorCode, errorString); + } + + const Response res = serializeAndDeserialize(response); + QCOMPARE(res.type(), Command::Invalid); + QVERIFY(!res.isValid()); + QVERIFY(res.isResponse()); + QCOMPARE(res.isError(), isError); + QCOMPARE(res.errorCode(), errorCode); + QCOMPARE(res.errorMessage(), errorString); + QVERIFY(res == response); + const bool notEquals = (res != response); + QVERIFY(!notEquals); +} + +void ProtocolTest::testAncestor() +{ + Ancestor in; + in.setId(42); + in.setRemoteId(QStringLiteral("remoteId")); + in.setName(QStringLiteral("Col 42")); + in.setAttributes({{ "Attr1", "Val 1" }, { "Attr2", "Röndom útéef řetězec" }}); + + const Ancestor out = serializeAndDeserialize(in); + QCOMPARE(out.id(), 42); + QCOMPARE(out.remoteId(), QStringLiteral("remoteId")); + QCOMPARE(out.name(), QStringLiteral("Col 42")); + QCOMPARE(out.attributes(), Attributes({{ "Attr1", "Val 1" }, { "Attr2", "Röndom útéef řetězec" }})); + QVERIFY(out == in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testFetchScope_data() +{ + QTest::addColumn("fullPayload"); + QTest::addColumn>("requestedParts"); + QTest::addColumn>("expectedParts"); + QTest::addColumn>("expectedPayloads"); + QTest::newRow("full payload (via flag") << true + << QVector{ "PLD:HEAD", "ATR:MYATR" } + << QVector{ "PLD:HEAD", "ATR:MYATR", "PLD:RFC822" } + << QVector{ "PLD:HEAD", "PLD:RFC822" }; + QTest::newRow("full payload (via part name") << false + << QVector{ "PLD:HEAD", "ATR:MYATR", "PLD:RFC822" } + << QVector{ "PLD:HEAD", "ATR:MYATR", "PLD:RFC822" } + << QVector{ "PLD:HEAD", "PLD:RFC822" }; + QTest::newRow("full payload (via both") << true + << QVector{ "PLD:HEAD", "ATR:MYATR", "PLD:RFC822" } + << QVector{ "PLD:HEAD", "ATR:MYATR", "PLD:RFC822" } + << QVector{ "PLD:HEAD", "PLD:RFC822" }; + QTest::newRow("without full payload") << false + << QVector{ "PLD:HEAD", "ATR:MYATR" } + << QVector{ "PLD:HEAD", "ATR:MYATR" } + << QVector{ "PLD:HEAD" }; +} + + +void ProtocolTest::testFetchScope() +{ + QFETCH(bool, fullPayload); + QFETCH(QVector, requestedParts); + QFETCH(QVector, expectedParts); + QFETCH(QVector, expectedPayloads); + + FetchScope in; + for (int i = FetchScope::CacheOnly; i <= FetchScope::VirtReferences; i = i << 1) { + QVERIFY(!in.fetch(static_cast(i))); + } + QVERIFY(in.fetch(FetchScope::None)); + + in.setRequestedParts(requestedParts); + in.setChangedSince(QDateTime(QDate(2015, 8, 10), QTime(23, 52, 20), Qt::UTC)); + in.setTagFetchScope({ "TAGID" }); + in.setAncestorDepth(Ancestor::AllAncestors); + in.setFetch(FetchScope::CacheOnly); + in.setFetch(FetchScope::CheckCachedPayloadPartsOnly); + in.setFetch(FetchScope::FullPayload, fullPayload); + in.setFetch(FetchScope::AllAttributes); + in.setFetch(FetchScope::Size); + in.setFetch(FetchScope::MTime); + in.setFetch(FetchScope::RemoteRevision); + in.setFetch(FetchScope::IgnoreErrors); + in.setFetch(FetchScope::Flags); + in.setFetch(FetchScope::RemoteID); + in.setFetch(FetchScope::GID); + in.setFetch(FetchScope::Tags); + in.setFetch(FetchScope::Relations); + in.setFetch(FetchScope::VirtReferences); + + const FetchScope out = serializeAndDeserialize(in); + QCOMPARE(out.requestedParts(), expectedParts); + QCOMPARE(out.requestedPayloads(), expectedPayloads); + QCOMPARE(out.changedSince(), QDateTime(QDate(2015, 8, 10), QTime(23, 52, 20), Qt::UTC)); + QCOMPARE(out.tagFetchScope(), QSet{ "TAGID" }); + QCOMPARE(out.ancestorDepth(), Ancestor::AllAncestors); + QCOMPARE(out.fetch(FetchScope::None), false); + QCOMPARE(out.cacheOnly(), true); + QCOMPARE(out.checkCachedPayloadPartsOnly(), true); + QCOMPARE(out.fullPayload(), fullPayload); + QCOMPARE(out.allAttributes(), true); + QCOMPARE(out.fetchSize(), true); + QCOMPARE(out.fetchMTime(), true); + QCOMPARE(out.fetchRemoteRevision(), true); + QCOMPARE(out.ignoreErrors(), true); + QCOMPARE(out.fetchFlags(), true); + QCOMPARE(out.fetchRemoteId(), true); + QCOMPARE(out.fetchGID(), true); + QCOMPARE(out.fetchRelations(), true); + QCOMPARE(out.fetchVirtualReferences(), true); +} + +void ProtocolTest::testScopeContext_data() +{ + QTest::addColumn("colId"); + QTest::addColumn("colRid"); + QTest::addColumn("tagId"); + QTest::addColumn("tagRid"); + + QTest::newRow("collection - id") << 42ll << QString() + << 0ll << QString(); + QTest::newRow("collection - rid") << 0ll << QStringLiteral("rid") + << 0ll << QString(); + QTest::newRow("collection - both") << 42ll << QStringLiteral("rid") + << 0ll << QString(); + + QTest::newRow("tag - id") << 0ll << QString() + << 42ll << QString(); + QTest::newRow("tag - rid") << 0ll << QString() + << 0ll << QStringLiteral("rid"); + QTest::newRow("tag - both") << 0ll << QString() + << 42ll << QStringLiteral("rid"); + + QTest::newRow("both - id") << 42ll << QString() + << 10ll << QString(); + QTest::newRow("both - rid") << 0ll << QStringLiteral("colRid") + << 0ll << QStringLiteral("tagRid"); + QTest::newRow("col - id, tag - rid") << 42ll << QString() + << 0ll << QStringLiteral("tagRid"); + QTest::newRow("col - rid, tag - id") << 0ll << QStringLiteral("colRid") + << 42ll << QString(); + QTest::newRow("both - both") << 42ll << QStringLiteral("colRid") + << 10ll << QStringLiteral("tagRid"); +} + +void ProtocolTest::testScopeContext() +{ + QFETCH(qint64, colId); + QFETCH(QString, colRid); + QFETCH(qint64, tagId); + QFETCH(QString, tagRid); + + const bool hasColId = colId > 0; + const bool hasColRid = !colRid.isEmpty(); + const bool hasTagId = tagId > 0; + const bool hasTagRid = !tagRid.isEmpty(); + + ScopeContext in; + QVERIFY(in.isEmpty()); + if (hasColId) { + in.setContext(ScopeContext::Collection, colId); + } + if (hasColRid) { + in.setContext(ScopeContext::Collection, colRid); + } + if (hasTagId) { + in.setContext(ScopeContext::Tag, tagId); + } + if (hasTagRid) { + in.setContext(ScopeContext::Tag, tagRid); + } + + QCOMPARE(in.hasContextId(ScopeContext::Any), false); + QCOMPARE(in.hasContextRID(ScopeContext::Any), false); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(in.hasContextId(ScopeContext::Collection), hasColId); + QCOMPARE(in.hasContextRID(ScopeContext::Collection), hasColRid); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(in.hasContextId(ScopeContext::Tag), hasTagId); + QCOMPARE(in.hasContextRID(ScopeContext::Tag), hasTagRid); + QVERIFY(!in.isEmpty()); + + ScopeContext out = serializeAndDeserialize(in); + QCOMPARE(out.isEmpty(), false); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.hasContextId(ScopeContext::Collection), hasColId); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.contextId(ScopeContext::Collection), colId); + QCOMPARE(out.hasContextRID(ScopeContext::Collection), hasColRid); + QCOMPARE(out.contextRID(ScopeContext::Collection), colRid); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.hasContextId(ScopeContext::Tag), hasTagId); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.contextId(ScopeContext::Tag), tagId); + QCOMPARE(out.hasContextRID(ScopeContext::Tag), hasTagRid); + QCOMPARE(out.contextRID(ScopeContext::Tag), tagRid); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); + + // Clearing "any" should not do anything + out.clearContext(ScopeContext::Any); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.hasContextId(ScopeContext::Collection), hasColId); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.contextId(ScopeContext::Collection), colId); + QCOMPARE(out.hasContextRID(ScopeContext::Collection), hasColRid); + QCOMPARE(out.contextRID(ScopeContext::Collection), colRid); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.hasContextId(ScopeContext::Tag), hasTagId); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.contextId(ScopeContext::Tag), tagId); + QCOMPARE(out.hasContextRID(ScopeContext::Tag), hasTagRid); + QCOMPARE(out.contextRID(ScopeContext::Tag), tagRid); + + if (hasColId || hasColRid) { + ScopeContext clear = out; + clear.clearContext(ScopeContext::Collection); + QCOMPARE(clear.hasContextId(ScopeContext::Collection), false); + QCOMPARE(clear.hasContextRID(ScopeContext::Collection), false); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(clear.hasContextId(ScopeContext::Tag), hasTagId); + QCOMPARE(clear.hasContextRID(ScopeContext::Tag), hasTagRid); + } + if (hasTagId || hasTagRid) { + ScopeContext clear = out; + clear.clearContext(ScopeContext::Tag); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(clear.hasContextId(ScopeContext::Collection), hasColId); + QCOMPARE(clear.hasContextRID(ScopeContext::Collection), hasColRid); + QCOMPARE(clear.hasContextId(ScopeContext::Tag), false); + QCOMPARE(clear.hasContextRID(ScopeContext::Tag), false); + } + + out.clearContext(ScopeContext::Collection); + out.clearContext(ScopeContext::Tag); + QVERIFY(out.isEmpty()); +} + +void ProtocolTest::testPartMetaData() +{ + PartMetaData in; + in.setName("PLD:HEAD"); + in.setSize(42); + in.setVersion(1); + in.setIsExternal(true); + + const PartMetaData out = serializeAndDeserialize(in); + QCOMPARE(out.name(), QByteArray("PLD:HEAD")); + QCOMPARE(out.size(), 42); + QCOMPARE(out.version(), 1); + QCOMPARE(out.isExternal(), true); + QCOMPARE(out, in); + const bool notEquals = (in != out); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCachePolicy() +{ + CachePolicy in; + in.setInherit(true); + in.setCheckInterval(42); + in.setCacheTimeout(10); + in.setSyncOnDemand(true); + in.setLocalParts({ QStringLiteral("PLD:HEAD"), QStringLiteral("PLD:ENVELOPE") }); + + const CachePolicy out = serializeAndDeserialize(in); + QCOMPARE(out.inherit(), true); + QCOMPARE(out.checkInterval(), 42); + QCOMPARE(out.cacheTimeout(), 10); + QCOMPARE(out.syncOnDemand(), true); + QCOMPARE(out.localParts(), QStringList() << QStringLiteral("PLD:HEAD") << QStringLiteral("PLD:ENVELOPE")); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testHelloResponse() +{ + HelloResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setServerName(QStringLiteral("AkonadiTest")); + in.setMessage(QStringLiteral("Oh, hello there!")); + in.setProtocolVersion(42); + in.setError(10, QStringLiteral("Ooops")); + + const HelloResponse out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(out.isResponse()); + QVERIFY(out.isError()); + QCOMPARE(out.errorCode(), 10); + QCOMPARE(out.errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(out.serverName(), QStringLiteral("AkonadiTest")); + QCOMPARE(out.message(), QStringLiteral("Oh, hello there!")); + QCOMPARE(out.protocolVersion(), 42); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testLoginCommand() +{ + LoginCommand in; + QVERIFY(!in.isResponse()); + QVERIFY(in.isValid()); + in.setSessionId("MySession-123-notifications"); + in.setSessionMode(LoginCommand::NotificationBus); + + const LoginCommand out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(!out.isResponse()); + QCOMPARE(out.sessionId(), QByteArray("MySession-123-notifications")); + QCOMPARE(out.sessionMode(), LoginCommand::NotificationBus); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testLoginResponse() +{ + LoginResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const LoginResponse out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(out.isResponse()); + QVERIFY(out.isError()); + QCOMPARE(out.errorCode(), 42); + QCOMPARE(out.errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testLogoutCommand() +{ + LogoutCommand in; + QVERIFY(!in.isResponse()); + QVERIFY(in.isValid()); + + const LogoutCommand out = serializeAndDeserialize(in); + QVERIFY(!out.isResponse()); + QVERIFY(out.isValid()); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testLogoutResponse() +{ + LogoutResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const LogoutResponse out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(out.isResponse()); + QVERIFY(out.isError()); + QCOMPARE(out.errorCode(), 42); + QCOMPARE(out.errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + + +void ProtocolTest::testTransactionCommand() +{ + TransactionCommand in; + QVERIFY(!in.isResponse()); + QVERIFY(in.isValid()); + in.setMode(TransactionCommand::Begin); + + const TransactionCommand out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(!out.isResponse()); + QCOMPARE(out.mode(), TransactionCommand::Begin); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testTransactionResponse() +{ + TransactionResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const TransactionResponse out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(out.isResponse()); + QVERIFY(out.isError()); + QCOMPARE(out.errorCode(), 42); + QCOMPARE(out.errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCreateItemCommand() +{ + Scope addedTags(QVector{ 3, 4 }); + Scope removedTags(QVector{ 5, 6 }); + Attributes attrs{ { "ATTR1", "MyAttr" }, { "ATTR2", "Můj chlupaťoučký kůň" } }; + QSet parts{ "PLD:HEAD", "PLD:ENVELOPE" }; + + CreateItemCommand in; + QVERIFY(!in.isResponse()); + QVERIFY(in.isValid()); + QCOMPARE(in.mergeModes(), CreateItemCommand::None); + in.setMergeModes(CreateItemCommand::MergeModes(CreateItemCommand::GID | CreateItemCommand::RemoteID)); + in.setCollection(Scope(1)); + in.setItemSize(100); + in.setMimeType(QStringLiteral("text/directory")); + in.setGID(QStringLiteral("GID")); + in.setRemoteId(QStringLiteral("RID")); + in.setRemoteRevision(QStringLiteral("RREV")); + in.setDateTime(QDateTime(QDate(2015, 8, 11), QTime(14, 32, 21), Qt::UTC)); + in.setFlags({ "\\SEEN", "FLAG" }); + in.setAddedFlags({ "FLAG2" }); + in.setRemovedFlags({ "FLAG3" }); + in.setTags(Scope(2)); + in.setAddedTags(addedTags); + in.setRemovedTags(removedTags); + in.setAttributes(attrs); + in.setParts(parts); + + const CreateItemCommand out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(!out.isResponse()); + QCOMPARE(out.mergeModes(), CreateItemCommand::GID | CreateItemCommand::RemoteID); + QCOMPARE(out.collection(), Scope(1)); + QCOMPARE(out.itemSize(), 100); + QCOMPARE(out.mimeType(), QStringLiteral("text/directory")); + QCOMPARE(out.gid(), QStringLiteral("GID")); + QCOMPARE(out.remoteId(), QStringLiteral("RID")); + QCOMPARE(out.remoteRevision(), QStringLiteral("RREV")); + QCOMPARE(out.dateTime(), QDateTime(QDate(2015, 8, 11), QTime(14, 32, 21), Qt::UTC)); + QCOMPARE(out.flags(), QSet() << "\\SEEN" << "FLAG"); + QCOMPARE(out.addedFlags(), QSet{ "FLAG2" }); + QCOMPARE(out.removedFlags(), QSet{ "FLAG3" }); + QCOMPARE(out.tags(), Scope(2)); + QCOMPARE(out.addedTags(), addedTags); + QCOMPARE(out.removedTags(), removedTags); + QCOMPARE(out.attributes(), attrs); + QCOMPARE(out.parts(), parts); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCreateItemResponse() +{ + CreateItemResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const CreateItemResponse out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(out.isResponse()); + QVERIFY(out.isError()); + QCOMPARE(out.errorCode(), 42); + QCOMPARE(out.errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCopyItemsCommand() +{ + const Scope items(QVector{ 1, 2, 3, 10 }); + + CopyItemsCommand in; + QVERIFY(in.isValid()); + QVERIFY(!in.isResponse()); + in.setItems(items); + in.setDestination(Scope(42)); + + const CopyItemsCommand out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(!out.isResponse()); + QCOMPARE(out.items(), items); + QCOMPARE(out.destination(), Scope(42)); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCopyItemsResponse() +{ + CopyItemsResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const CopyItemsResponse out = serializeAndDeserialize(in); + QVERIFY(out.isValid()); + QVERIFY(out.isResponse()); + QVERIFY(out.isError()); + QCOMPARE(out.errorCode(), 42); + QCOMPARE(out.errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +QTEST_MAIN(ProtocolTest) diff --git a/autotests/private/protocoltest.h b/autotests/private/protocoltest.h new file mode 100644 index 0000000..2d7978d --- /dev/null +++ b/autotests/private/protocoltest.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2015 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef PROTOCOLTEST_H +#define PROTOCOLTEST_H + +#include +#include + +#include +#include + +#include + +class ProtocolTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testProtocolVersion(); + + void testFactory_data(); + void testFactory(); + + void testCommand(); + void testResponse_data(); + void testResponse(); + void testAncestor(); + void testFetchScope_data(); + void testFetchScope(); + void testScopeContext_data(); + void testScopeContext(); + void testPartMetaData(); + void testCachePolicy(); + + void testHelloResponse(); + void testLoginCommand(); + void testLoginResponse(); + void testLogoutCommand(); + void testLogoutResponse(); + void testTransactionCommand(); + void testTransactionResponse(); + void testCreateItemCommand(); + void testCreateItemResponse(); + void testCopyItemsCommand(); + void testCopyItemsResponse(); + +private: + template + typename std::enable_if::value, T>::type + serializeAndDeserialize(const T &in) + { + QBuffer buf; + buf.open(QIODevice::ReadWrite); + + Akonadi::Protocol::serialize(&buf, in); + buf.seek(0); + return T(Akonadi::Protocol::deserialize(&buf)); + + } + + template + typename std::enable_if::value == false, T>::type + serializeAndDeserialize(const T &in, int * = 0) + { + QBuffer buf; + buf.open(QIODevice::ReadWrite); + + Akonadi::Protocol::DataStream stream(&buf); + stream << in; + buf.seek(0); + T out; + stream >> out; + + return out; + } +}; + +#endif // PROTOCOLTEST_H diff --git a/autotests/server/CMakeLists.txt b/autotests/server/CMakeLists.txt new file mode 100644 index 0000000..db51c41 --- /dev/null +++ b/autotests/server/CMakeLists.txt @@ -0,0 +1,101 @@ +########### next target ############### + +# QTEST_MAIN is using QApplication when QT_GUI_LIB is defined +remove_definitions(-DQT_GUI_LIB) + +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) + +include_directories(${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_BINARY_DIR}/src/server + ${Akonadi_SOURCE_DIR}/src/server) + +akonadi_generate_schema(${CMAKE_CURRENT_SOURCE_DIR}/dbtest_data/unittest_schema.xml UnitTestSchema unittestschema) + +set(AKONADI_DB_DATA ${CMAKE_CURRENT_SOURCE_DIR}/dbtest_data/dbdata.xml) + +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/dbpopulator.cpp + COMMAND ${XSLTPROC_EXECUTABLE} + --output ${CMAKE_CURRENT_BINARY_DIR}/dbpopulator.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/dbpopulator.xsl + ${AKONADI_DB_DATA} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/dbpopulator.xsl + ${AKONADI_DB_DATA} +) + +set(common_SRCS + unittestschema.cpp + fakeconnection.cpp + fakedatastore.cpp + fakeclient.cpp + fakeakonadiserver.cpp + fakesearchmanager.cpp + dbinitializer.cpp + ${CMAKE_CURRENT_BINARY_DIR}/dbpopulator.cpp +) +ecm_qt_declare_logging_category(common_SRCS HEADER akonadiserver_debug.h IDENTIFIER AKONADISERVER_LOG CATEGORY_NAME log_akonadiserver) + +add_library(akonadi_unittest_common STATIC ${common_SRCS}) +target_link_libraries(akonadi_unittest_common + KF5AkonadiPrivate + libakonadiserver + Qt5::Core + Qt5::DBus + Qt5::Test + Qt5::Sql + Qt5::Network +) + +macro(add_server_test _source) + set(_test ${_source} ../../src/server/akonadiserver_debug.cpp) + get_filename_component(_name ${_source} NAME_WE) + qt5_add_resources(_test dbtest_data/dbtest_data.qrc) + add_executable(${_name} ${_test}) + add_test(AkonadiServer-${_name} ${_name}) + if (ENABLE_ASAN) + set_tests_properties(AkonadiServer-${_name} PROPERTIES + ENVIRONMENT ASAN_OPTIONS=symbolize=1 + ) + endif() + set_tests_properties(AkonadiServer-${_name} PROPERTIES + ENVIRONMENT "QT_HASH_SEED=0;QT_NO_CPU_FEATURE=sse4.2" + ) + target_link_libraries(${_name} + akonadi_shared + akonadi_unittest_common + libakonadiserver + KF5AkonadiPrivate + Qt5::Core + Qt5::DBus + Qt5::Test + Qt5::Sql + Qt5::Network + ${CMAKE_SHARED_LINKER_FLAGS_ASAN} + ) +endmacro() + +add_server_test(dbtypetest.cpp) +add_server_test(dbintrospectortest.cpp) +add_server_test(querybuildertest.cpp) +add_server_test(dbinitializertest.cpp) +add_server_test(dbupdatertest.cpp) +add_server_test(handlertest.cpp) +add_server_test(dbconfigtest.cpp) +add_server_test(parthelpertest.cpp) +add_server_test(itemretrievertest.cpp) +add_server_test(notificationmanagertest.cpp) +add_server_test(parttypehelpertest.cpp) + +if (SQLITE_FOUND) # tests using the fake server need the QSQLITE3 plugin +add_server_test(partstreamertest.cpp) +add_server_test(akappendhandlertest.cpp) +add_server_test(linkhandlertest.cpp) +add_server_test(listhandlertest.cpp) +add_server_test(modifyhandlertest.cpp) +add_server_test(createhandlertest.cpp) +add_server_test(collectionreferencetest.cpp) +add_server_test(searchtest.cpp akonadiprivate) +add_server_test(relationhandlertest.cpp akonadiprivate) +add_server_test(taghandlertest.cpp akonadiprivate) +add_server_test(fetchhandlertest.cpp akonadiprivate) +endif() diff --git a/autotests/server/akappendhandlertest.cpp b/autotests/server/akappendhandlertest.cpp new file mode 100644 index 0000000..2165da3 --- /dev/null +++ b/autotests/server/akappendhandlertest.cpp @@ -0,0 +1,772 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 + +#include +#include + +#include +#include + +#include "fakeakonadiserver.h" +#include "fakeentities.h" + +#include + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(PimItem) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(QVector) + +class AkAppendHandlerTest : public QObject +{ + Q_OBJECT + +public: + AkAppendHandlerTest() + { + // Effectively disable external payload parts, we have a dedicated unit-test + // for that + const QString serverConfigFile = StandardDirs::serverConfigFile(XdgBaseDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + settings.setValue(QLatin1String("General/SizeThreshold"), std::numeric_limits::max()); + + try { + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + } + + ~AkAppendHandlerTest() + { + FakeAkonadiServer::instance()->quit(); + } + + void updatePimItem(PimItem &pimItem, const QString &remoteId, const qint64 size) + { + pimItem.setRemoteId(remoteId); + pimItem.setGid(remoteId); + pimItem.setSize(size); + } + + void updateNotifcationEntity(Protocol::ChangeNotification &ntf, const PimItem &pimItem) + { + ntf.clearEntities(); + ntf.addEntity(pimItem.id(), pimItem.remoteId(), pimItem.remoteRevision(), pimItem.mimeType().name()); + } + + struct PartHelper + { + PartHelper(const QString &type_, const QByteArray &data_, int size_, bool external_ = false, int version_ = 0) + : type(type_) + , data(data_) + , size(size_) + , external(external_) + , version(version_) + { + } + QString type; + QByteArray data; + int size; + bool external; + int version; + }; + + void updateParts(QVector &parts, const std::vector &updatedParts) + { + parts.clear(); + Q_FOREACH (const PartHelper &helper, updatedParts) { + FakePart part; + + const QStringList types = helper.type.split(QLatin1Char(':')); + Q_ASSERT(types.count() == 2); + part.setPartType(PartType(types[1], types[0])); + part.setData(helper.data); + part.setDatasize(helper.size); + part.setExternal(helper.external); + part.setVersion(helper.version); + parts << part; + } + } + + void updateFlags(QVector &flags, const QStringList &updatedFlags) + { + flags.clear(); + Q_FOREACH (const QString &flagName, updatedFlags) { + Flag flag; + flag.setName(flagName); + flags << flag; + } + } + + struct TagHelper + { + TagHelper(const QString &tagType_, const QString &gid_, const QString &remoteId_ = QString()) + : tagType(tagType_) + , gid(gid_) + , remoteId(remoteId_) + { + } + QString tagType; + QString gid; + QString remoteId; + }; + void updateTags(QVector &tags, const std::vector &updatedTags) + { + tags.clear(); + Q_FOREACH (const TagHelper &helper, updatedTags) { + FakeTag tag; + + TagType tagType; + tagType.setName(helper.tagType); + + tag.setTagType(tagType); + tag.setGid(helper.gid); + tag.setRemoteId(helper.remoteId); + tags << tag; + } + } + + Protocol::CreateItemCommand createCommand(const PimItem &pimItem, + const QDateTime &dt, + const QSet &parts, + qint64 overrideSize = -1) + { + const qint64 size = overrideSize > -1 ? overrideSize : pimItem.size(); + + Protocol::CreateItemCommand cmd; + cmd.setCollection(Scope(pimItem.collectionId())); + cmd.setItemSize(size); + cmd.setRemoteId(pimItem.remoteId()); + cmd.setRemoteRevision(pimItem.remoteRevision()); + cmd.setMimeType(pimItem.mimeType().name()); + cmd.setGID(pimItem.gid()); + cmd.setDateTime(dt); + cmd.setParts(parts); + + return cmd; + } + + Protocol::FetchItemsResponse createResponse(qint64 expectedId, + const PimItem &pimItem, + const QDateTime &datetime, + const QVector &parts, + qint64 overrideSize = -1) + { + const qint64 size = overrideSize > -1 ? overrideSize : pimItem.size(); + + Protocol::FetchItemsResponse resp(expectedId); + resp.setParentId(pimItem.collectionId()); + resp.setSize(size); + resp.setRemoteId(pimItem.remoteId()); + resp.setRemoteRevision(pimItem.remoteRevision()); + resp.setMimeType(pimItem.mimeType().name()); + resp.setGid(pimItem.gid()); + resp.setMTime(datetime); + resp.setParts(parts); + resp.setAncestors({ Protocol::Ancestor(4, QLatin1String("ColC")) }); + + return resp; + } + + TestScenario errorResponse(const QString &errorMsg) + { + Protocol::CreateItemResponse response; + response.setError(1, errorMsg); + return TestScenario::create(5, TestScenario::ServerCmd, response); + } + +private Q_SLOTS: + void testAkAppend_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn("notification"); + QTest::addColumn("pimItem"); + QTest::addColumn >("parts"); + QTest::addColumn >("flags"); + QTest::addColumn >("tags"); + QTest::addColumn("uidnext"); + QTest::addColumn("datetime"); + QTest::addColumn("expectFail"); + + TestScenario::List scenarios; + Protocol::ChangeNotification notification; + qint64 uidnext = 0; + QDateTime datetime(QDate(2014, 05, 12), QTime(14, 46, 00), Qt::UTC); + PimItem pimItem; + QVector parts; + QVector flags; + QVector tags; + + pimItem.setCollectionId(4); + pimItem.setSize(10); + pimItem.setRemoteId(QLatin1String("TEST-1")); + pimItem.setRemoteRevision(QLatin1String("1")); + pimItem.setGid(QLatin1String("TEST-1")); + pimItem.setMimeType(MimeType::retrieveByName(QLatin1String("application/octet-stream"))); + pimItem.setDatetime(datetime); + updateParts(parts, { { QLatin1String("PLD:DATA"), "0123456789", 10 } }); + notification.setType(Protocol::ChangeNotification::Items); + notification.setOperation(Protocol::ChangeNotification::Add); + notification.setParentCollection(4); + notification.setResource("akonadi_fake_resource_0"); + notification.addEntity(-1, QLatin1String("TEST-1"), QLatin1String("1"), QLatin1String("application/octet-stream")); + notification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + uidnext = 13; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 10))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", "0123456789")) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 10), "0123456789") })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("single-part") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-2"), 20); + updateParts(parts, { { QLatin1String("PLD:DATA"), "Random Data", 11 }, + { QLatin1String("PLD:PLDTEST"), "Test Data", 9 } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA", "PLD:PLDTEST" } )) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 11, 0))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", "Random Data")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:PLDTEST", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:PLDTEST", Protocol::PartMetaData("PLD:PLDTEST", 9, 0))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:PLDTEST", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:PLDTEST", "Test Data")) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 11), "Random Data"), + Protocol::StreamPayloadResponse("PLD:PLDTEST", Protocol::PartMetaData("PLD:PLDTEST", 9), "Test Data") })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("multi-part") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + TestScenario inScenario, outScenario; + { + Protocol::CreateItemCommand cmd; + cmd.setCollection(Scope(100)); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << inScenario + << errorResponse(QLatin1String("Invalid parent collection")); + QTest::newRow("invalid collection") << scenarios << Protocol::ChangeNotification() + << PimItem() << QVector() + << QVector() << QVector() + << -1ll << QDateTime() << true; + + { + Protocol::CreateItemCommand cmd; + cmd.setCollection(Scope(6)); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << inScenario + << errorResponse(QLatin1String("Cannot append item into virtual collection")); + QTest::newRow("virtual collection") << scenarios << Protocol::ChangeNotification() + << PimItem() << QVector() + << QVector() << QVector() + << -1ll << QDateTime() << true; + + updatePimItem(pimItem, QLatin1String("TEST-3"), 5); + updateParts(parts, { { QLatin1String("PLD:DATA"), "12345", 5 } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" }, 1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", "12345")) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5), "12345") })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("mismatch item sizes (smaller)") << scenarios << notification << pimItem + << parts << flags << tags << uidnext + << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-4"), 10); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" }, 10)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", "12345")) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5), "12345") }, 10)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("mismatch item sizes (bigger)") << scenarios << notification << pimItem + << parts << flags << tags << uidnext + << datetime << false; + + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", "123")) + << errorResponse(QLatin1String("Payload size mismatch")); + QTest::newRow("incomplete part data") << scenarios << Protocol::ChangeNotification() + << PimItem() << QVector() + << QVector() << QVector() + << -1ll << QDateTime() << true; + + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 4))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", "1234567890")) + << errorResponse(QLatin1String("Payload size mismatch")); + QTest::newRow("part data larger than advertised") << scenarios << Protocol::ChangeNotification() + << PimItem() << QVector() + << QVector() << QVector() + << -1ll << QDateTime() << true; + + updatePimItem(pimItem, QLatin1String("TEST-5"), 0); + updateParts(parts, { { QLatin1String("PLD:DATA"), QByteArray(), 0 } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 0))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", QByteArray())) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem ,datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 0), QByteArray()) } )) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("empty payload part") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-8"), 1); + updateParts(parts, { { QLatin1String("PLD:DATA"), QByteArray("\0", 1), 1 } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 1))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", QByteArray("\0", 1))) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 1), QByteArray("\0", 1)) })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("part data will null character") << scenarios << notification << pimItem + << parts << flags << tags << uidnext + << datetime << false; + + const QString utf8String = QString::fromUtf8("äöüß@€µøđ¢©®"); + updatePimItem(pimItem, QLatin1String("TEST-9"), utf8String.toUtf8().size()); + updateParts(parts, { { QLatin1String("PLD:DATA"), utf8String.toUtf8(), utf8String.toUtf8().size() } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", utf8String.toUtf8())) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", utf8String.toUtf8().size()), utf8String.toUtf8()) })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("utf8 part data") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + const QByteArray hugeData = QByteArray("a").repeated(1 << 20); + updatePimItem(pimItem, QLatin1String("TEST-10"), 1 << 20); + updateParts(parts, { { QLatin1String("PLD:DATA"), hugeData, 1 << 20 } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", hugeData)) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem ,datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()), hugeData) })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("huge part data") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + const QByteArray dataWithNewLines = "Bernard, Bernard, Bernard, Bernard, look, look Bernard!\nWHAT!!!!!!!\nI'm a prostitute robot from the future!"; + updatePimItem(pimItem, QLatin1String("TEST-11"), dataWithNewLines.size()); + updateParts(parts, { { QLatin1String("PLD:DATA"), dataWithNewLines, dataWithNewLines.size() } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", dataWithNewLines)) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", dataWithNewLines.size()), dataWithNewLines) })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("data with newlines") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + const QByteArray lotsOfNewlines = QByteArray("\n").repeated(1 << 20); + updatePimItem(pimItem, QLatin1String("TEST-12"), lotsOfNewlines.size()); + updateParts(parts, { { QLatin1String("PLD:DATA"), lotsOfNewlines, lotsOfNewlines.size() } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", lotsOfNewlines)) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, + { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()), lotsOfNewlines) })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("data with lots of newlines") << scenarios << notification << pimItem + << parts << flags << tags << uidnext + << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-13"), 20); + updateParts(parts, { { QLatin1String("PLD:NEWPARTTYPE1"), "0123456789", 10 }, + { QLatin1String("PLD:NEWPARTTYPE2"), "9876543210", 10 } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:NEWPARTTYPE1", "PLD:NEWPARTTYPE2" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:NEWPARTTYPE2", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE2", Protocol::PartMetaData("PLD:NEWPARTTYPE2", 10))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:NEWPARTTYPE2", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE2", "9876543210")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:NEWPARTTYPE1", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE1", Protocol::PartMetaData("PLD:NEWPARTTYPE1", 10))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:NEWPARTTYPE1", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE1", "0123456789")) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, + { Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE2", Protocol::PartMetaData("PLD:NEWPARTTYPE2", 10), "9876543210"), + Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE1", Protocol::PartMetaData("PLD:NEWPARTTYPE1", 10), "0123456789") })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("non-existent part types") << scenarios << notification << pimItem + << parts << flags << tags << uidnext + << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-14"), 0); + updateParts(parts, {}); + updateFlags(flags, QStringList() << QLatin1String("\\SEEN") << QLatin1String("\\RANDOM")); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd.setFlags({ "\\SEEN", "\\RANDOM" }); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp.setFlags({ "\\SEEN", "\\RANDOM" }); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << inScenario + << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("item with flags") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-15"), 0); + updateFlags(flags, {}); + updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-1") }, + { QLatin1String("PLAIN"), QLatin1String("TAG-2") } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd.setTags(Scope(Scope::Gid, { QLatin1String("TAG-1"), QLatin1String("TAG-2") })); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp.setTags({ + Protocol::FetchTagsResponse(2, "TAG-1", "PLAIN"), + Protocol::FetchTagsResponse(3, "TAG-2", "PLAIN") }); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << inScenario + << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("item with non-existent tags (GID)") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-16"), 0); + updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-3") }, + { QLatin1String("PLAIN"), QLatin1String("TAG-4") } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd.setTags(Scope(Scope::Rid, { QLatin1String("TAG-3"), QLatin1String("TAG-4") })); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp.setTags({ + Protocol::FetchTagsResponse(4, "TAG-3", "PLAIN"), + Protocol::FetchTagsResponse(5, "TAG-4", "PLAIN") }); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_0")) + << inScenario + << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("item with non-existent tags (RID)") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-17"), 0); + updateNotifcationEntity(notification, pimItem); + updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-1") }, + { QLatin1String("PLAIN"), QLatin1String("TAG-2") } }); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd.setTags(Scope(Scope::Rid, { QLatin1String("TAG-1"), QLatin1String("TAG-2") })); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp.setTags({ + Protocol::FetchTagsResponse(2, "TAG-1", "PLAIN"), + Protocol::FetchTagsResponse(3, "TAG-2", "PLAIN") }); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_0")) + << inScenario + << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("item with existing tags (RID)") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-18"), 0); + updateNotifcationEntity(notification, pimItem); + updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-3") }, + { QLatin1String("PLAIN"), QLatin1String("TAG-4") } }); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd.setTags(Scope(Scope::Gid, { QLatin1String("TAG-3"), QLatin1String("TAG-4") })); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp.setTags({ + Protocol::FetchTagsResponse(4, "TAG-3", "PLAIN"), + Protocol::FetchTagsResponse(5, "TAG-4", "PLAIN") }); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << inScenario + << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("item with existing tags (GID)") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-19"), 0); + updateFlags(flags, QStringList() << QLatin1String("\\SEEN") << QLatin1String("$FLAG")); + updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-1") }, + { QLatin1String("PLAIN"), QLatin1String("TAG-2") } }); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd.setTags(Scope(Scope::Gid, { QLatin1String("TAG-1"), QLatin1String("TAG-2") })); + cmd.setFlags({ "\\SEEN", "$FLAG" }); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp.setTags({ + Protocol::FetchTagsResponse(2, "TAG-1", "PLAIN"), + Protocol::FetchTagsResponse(3, "TAG-2", "PLAIN") }); + rsp.setFlags({ "\\SEEN", "$FLAG" }); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << inScenario + << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("item with flags and tags") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + + updatePimItem(pimItem, QLatin1String("TEST-20"), 0); + updateFlags(flags, {}); + updateTags(tags, { { QLatin1String("PLAIN"), utf8String } }); + updateNotifcationEntity(notification, pimItem);; + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd.setTags(Scope(Scope::Gid, { utf8String })); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp.setTags({ + Protocol::FetchTagsResponse(6, utf8String.toUtf8(), "PLAIN") }); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << inScenario + << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponse()); + QTest::newRow("item with UTF-8 tag") << scenarios << notification << pimItem << parts + << flags << tags << uidnext << datetime << false; + } + + void testAkAppend() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Protocol::ChangeNotification, notification); + QFETCH(PimItem, pimItem); + QFETCH(QVector, parts); + QFETCH(QVector, flags); + QFETCH(QVector, tags); + QFETCH(qint64, uidnext); + QFETCH(bool, expectFail); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + QSignalSpy *notificationSpy = FakeAkonadiServer::instance()->notificationSpy(); + + if (notification.isValid()) { + QCOMPARE(notificationSpy->count(), 1); + Protocol::ChangeNotification::List notifications = notificationSpy->at(0).first().value(); + QCOMPARE(notifications.count(), 1); + const Protocol::ChangeNotification itemNotification = notifications.at(0); + + QVERIFY(AkTest::compareNotifications(itemNotification, notification, QFlag(AkTest::NtfAll & ~ AkTest::NtfEntities))); + QCOMPARE(itemNotification.entities().count(), notification.entities().count()); + } else { + QVERIFY(notificationSpy->isEmpty()); + } + + const PimItem actualItem = PimItem::retrieveById(uidnext); + if (expectFail) { + QVERIFY(!actualItem.isValid()); + } else { + QVERIFY(actualItem.isValid()); + QCOMPARE(actualItem.remoteId(), pimItem.remoteId()); + QCOMPARE(actualItem.remoteRevision(), pimItem.remoteRevision()); + QCOMPARE(actualItem.gid(), pimItem.gid()); + QCOMPARE(actualItem.size(), pimItem.size()); + QCOMPARE(actualItem.datetime(), pimItem.datetime()); + QCOMPARE(actualItem.collectionId(), pimItem.collectionId()); + QCOMPARE(actualItem.mimeTypeId(), pimItem.mimeTypeId()); + + const QList actualFlags = actualItem.flags().toList(); + QCOMPARE(actualFlags.count(), flags.count()); + Q_FOREACH (const Flag &flag, flags) { + const QList::const_iterator actualFlagIter = + std::find_if(actualFlags.constBegin(), actualFlags.constEnd(), + [flag](Flag const & actualFlag) { + return flag.name() == actualFlag.name(); }); + QVERIFY(actualFlagIter != actualFlags.constEnd()); + const Flag actualFlag = *actualFlagIter; + QVERIFY(actualFlag.isValid()); + } + + const QList actualTags = actualItem.tags().toList(); + QCOMPARE(actualTags.count(), tags.count()); + Q_FOREACH (const FakeTag &tag, tags) { + const QList::const_iterator actualTagIter = + std::find_if(actualTags.constBegin(), actualTags.constEnd(), + [tag](Tag const & actualTag) { + return tag.gid() == actualTag.gid(); }); + + QVERIFY(actualTagIter != actualTags.constEnd()); + const Tag actualTag = *actualTagIter; + QVERIFY(actualTag.isValid()); + QCOMPARE(actualTag.tagType().name(), tag.tagType().name()); + QCOMPARE(actualTag.gid(), tag.gid()); + if (!tag.remoteId().isEmpty()) { + SelectQueryBuilder qb; + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, QLatin1String("akonadi_fake_resource_0")); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, actualTag.id()); + QVERIFY(qb.exec()); + QCOMPARE(qb.result().size(), 1); + QCOMPARE(qb.result()[0].remoteId(), tag.remoteId()); + } + } + + const QList actualParts = actualItem.parts().toList(); + QCOMPARE(actualParts.count(), parts.count()); + Q_FOREACH (const FakePart &part, parts) { + const QList::const_iterator actualPartIter = + std::find_if(actualParts.constBegin(), actualParts.constEnd(), + [part](Part const & actualPart) { + return part.partType().ns() == actualPart.partType().ns() && + part.partType().name() == actualPart.partType().name(); }); + + QVERIFY(actualPartIter != actualParts.constEnd()); + const Part actualPart = *actualPartIter; + QVERIFY(actualPart.isValid()); + QCOMPARE(QString::fromUtf8(actualPart.data()), QString::fromUtf8(part.data())); + QCOMPARE(actualPart.data(), part.data()); + QCOMPARE(actualPart.datasize(), part.datasize()); + QCOMPARE(actualPart.external(), part.external()); + } + } + } +}; + +AKTEST_FAKESERVER_MAIN(AkAppendHandlerTest) + +#include "akappendhandlertest.moc" + diff --git a/autotests/server/collectionreferencetest.cpp b/autotests/server/collectionreferencetest.cpp new file mode 100644 index 0000000..4d5ce12 --- /dev/null +++ b/autotests/server/collectionreferencetest.cpp @@ -0,0 +1,268 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 + +#include + +#include "fakeakonadiserver.h" +#include "fakedatastore.h" +#include +#include +#include "entities.h" +#include "collectionreferencemanager.h" +#include "dbinitializer.h" + +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(Collection::List) + +class CollectionReferenceTest : public QObject +{ + Q_OBJECT + + DbInitializer initializer; + +public: + CollectionReferenceTest() + { + try { + FakeAkonadiServer::instance()->setPopulateDb(false); + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + + initializer.createResource("testresource"); + initializer.createCollection("col1"); + Collection col2 = initializer.createCollection("col2"); + col2.setEnabled(false); + col2.update(); + } + + ~CollectionReferenceTest() + { + FakeAkonadiServer::instance()->quit(); + } + +private Q_SLOTS: + void testModify_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn >("expectedNotifications"); + + Akonadi::Protocol::ChangeNotification notificationTemplate; + notificationTemplate.setType(Protocol::ChangeNotification::Collections); + notificationTemplate.setOperation(Protocol::ChangeNotification::Modify); + notificationTemplate.addEntity(initializer.collection("col2").id(), QLatin1String("col2"), QLatin1String("")); + notificationTemplate.setParentCollection(0); + notificationTemplate.setResource("testresource"); + notificationTemplate.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + { + Protocol::FetchCollectionsCommand cmd; + cmd.setDepth(Protocol::FetchCollectionsCommand::AllCollections); + cmd.setResource(QLatin1String("testresource")); + cmd.setEnabled(true); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer.listResponse(initializer.collection("col1"))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("list before referenced first level") << scenarios << QList(); + } + + { + Protocol::ModifyCollectionCommand cmd(initializer.collection("col2").id()); + cmd.setReferenced(true); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.setItemParts(QSet() << "REFERENCED"); + + QTest::newRow("reference") << scenarios << (QList() << notification); + } + + { + Protocol::ModifyCollectionCommand cmd(initializer.collection("col2").id()); + cmd.setReferenced(true); + + Protocol::FetchCollectionsCommand listCmd(initializer.collection("col2").id()); + listCmd.setDepth(Protocol::FetchCollectionsCommand::BaseCollection); + listCmd.setEnabled(true); + + Collection col2 = initializer.collection("col2"); + col2.setReferenced(true); + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponse()) + << TestScenario::create(6, TestScenario::ClientCmd, listCmd) + << TestScenario::create(6, TestScenario::ServerCmd, initializer.listResponse(col2)) + << TestScenario::create(6, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.setItemParts(QSet() << "REFERENCED"); + + QTest::newRow("list referenced base") << scenarios << (QList() << notification); + } + { + Protocol::ModifyCollectionCommand cmd(initializer.collection("col2").id()); + cmd.setReferenced(true); + + Protocol::FetchCollectionsCommand listCmd; + listCmd.setResource(QLatin1String("testresource")); + listCmd.setEnabled(true); + listCmd.setDepth(Protocol::FetchCollectionsCommand::ParentCollection); + + Collection col2 = initializer.collection("col2"); + col2.setReferenced(true); + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponse()) + << TestScenario::create(6, TestScenario::ClientCmd, listCmd) + << TestScenario::create(6, TestScenario::ServerCmd, initializer.listResponse(initializer.collection("col1"))) + << TestScenario::create(6, TestScenario::ServerCmd, initializer.listResponse(col2)) + << TestScenario::create(6, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.setItemParts(QSet() << "REFERENCED"); + + QTest::newRow("list referenced first level") << scenarios << (QList() << notification); + } + { + Protocol::ModifyCollectionCommand cmd1(initializer.collection("col2").id()); + cmd1.setReferenced(true); + + Protocol::ModifyCollectionCommand cmd2(initializer.collection("col2").id()); + cmd2.setReferenced(false); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd1) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponse()) + << TestScenario::create(6, TestScenario::ClientCmd, cmd2) + << TestScenario::create(6, TestScenario::ServerCmd, Protocol::ModifyCollectionResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.setItemParts(QSet() << "REFERENCED"); + + QTest::newRow("dereference") << scenarios << (QList() << notification << notification); + } + } + + void testModify() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(QList, expectedNotifications); + + // Clean all references from previous run + CollectionReferenceManager::cleanup(); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + QSignalSpy *notificationSpy = FakeAkonadiServer::instance()->notificationSpy(); + if (expectedNotifications.isEmpty()) { + QTRY_VERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + } else { + Protocol::ChangeNotification::List receivedNotifications; + for (int q = 0; q < notificationSpy->size(); q++) { + //Only one notify call + QCOMPARE(notificationSpy->first().count(), 1); + const Protocol::ChangeNotification::List n = notificationSpy->first().first().value(); + for (int i = 0; i < n.size(); i++) { + receivedNotifications.append(n.at(i)); + } + } + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < expectedNotifications.size(); i++) { + QCOMPARE(receivedNotifications.at(i), expectedNotifications.at(i)); + } + } + } + + void testReferenceCollection() + { + Collection col = initializer.createCollection("testReferenceCollection"); + + CollectionReferenceManager::instance()->referenceCollection("testReferenceCollectionSession", col, true); + QVERIFY(CollectionReferenceManager::instance()->isReferenced(col.id())); + QVERIFY(CollectionReferenceManager::instance()->isReferenced(col.id(), "testReferenceCollectionSession")); + + CollectionReferenceManager::instance()->referenceCollection("foobar", col, false); + QVERIFY(CollectionReferenceManager::instance()->isReferenced(col.id())); + QVERIFY(CollectionReferenceManager::instance()->isReferenced(col.id(), "testReferenceCollectionSession")); + + CollectionReferenceManager::instance()->referenceCollection("testReferenceCollectionSession", col, false); + QVERIFY(!CollectionReferenceManager::instance()->isReferenced(col.id())); + QVERIFY(!CollectionReferenceManager::instance()->isReferenced(col.id(), "testReferenceCollectionSession")); + QVERIFY(col.remove()); + } + + void testSessionClosed() + { + Collection col = initializer.createCollection("testSessionCollection"); + col.setReferenced(true); + QVERIFY(col.update()); + CollectionReferenceManager::instance()->referenceCollection("testSessionClosedSession", col, true); + CollectionReferenceManager::instance()->referenceCollection("testSessionClosedSession2", col, true); + + //Remove first session + CollectionReferenceManager::instance()->removeSession("testSessionClosedSession2"); + QVERIFY(Collection::retrieveById(col.id()).referenced()); + QVERIFY(!CollectionReferenceManager::instance()->isReferenced(col.id(), "testSessionClosedSession2")); + QVERIFY(CollectionReferenceManager::instance()->isReferenced(col.id(), "testSessionClosedSession")); + + CollectionReferenceManager::instance()->removeSession("testSessionClosedSession"); + QVERIFY(!Collection::retrieveById(col.id()).referenced()); + QVERIFY(!CollectionReferenceManager::instance()->isReferenced(col.id(), "testSessionClosedSession")); + + QVERIFY(col.remove()); + } + + void testCleanup() + { + Collection col = initializer.createCollection("testCleanupCollection"); + col.setReferenced(true); + QVERIFY(col.update()); + + CollectionReferenceManager::cleanup(); + QVERIFY(!Collection::retrieveById(col.id()).referenced()); + + QVERIFY(col.remove()); + } + +}; + +AKTEST_FAKESERVER_MAIN(CollectionReferenceTest) + +#include "collectionreferencetest.moc" diff --git a/autotests/server/createhandlertest.cpp b/autotests/server/createhandlertest.cpp new file mode 100644 index 0000000..517c4c4 --- /dev/null +++ b/autotests/server/createhandlertest.cpp @@ -0,0 +1,186 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 + +#include + +#include "fakeakonadiserver.h" +#include "dbinitializer.h" +#include "aktest.h" +#include "akdebug.h" +#include "entities.h" + +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class CreateHandlerTest : public QObject +{ + Q_OBJECT + +public: + CreateHandlerTest() + { + try { + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + } + + ~CreateHandlerTest() + { + FakeAkonadiServer::instance()->quit(); + } + +private Q_SLOTS: + void testCreate_data() + { + DbInitializer dbInitializer; + + QTest::addColumn("scenarios"); + QTest::addColumn("notification"); + + Akonadi::Protocol::ChangeNotification notificationTemplate; + notificationTemplate.setType(Protocol::ChangeNotification::Collections); + notificationTemplate.setOperation(Protocol::ChangeNotification::Add); + notificationTemplate.setParentCollection(3); + notificationTemplate.setResource("akonadi_fake_resource_0"); + notificationTemplate.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + { + Protocol::CreateCollectionCommand cmd; + cmd.setName(QLatin1String("New Name")); + cmd.setParent(Scope(3)); + cmd.setAttributes({ { "MYRANDOMATTRIBUTE", "" } }); + + Protocol::FetchCollectionsResponse resp(8); + resp.setName(QLatin1String("New Name")); + resp.setParentId(3); + resp.setAttributes({ { "MYRANDOMATTRIBUTE", "" } }); + resp.setResource(QLatin1String("akonadi_fake_resource_0")); + resp.cachePolicy().setLocalParts({ QLatin1String("ALL") }); + resp.setMimeTypes({ QLatin1String("application/octet-stream"), + QLatin1String("inode/directory") }); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateCollectionResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.addEntity(8, QLatin1String(""), QLatin1String("")); + + QTest::newRow("create collection") << scenarios << notification; + } + { + Protocol::CreateCollectionCommand cmd; + cmd.setName(QLatin1String("Name 2")); + cmd.setParent(Scope(3)); + cmd.setEnabled(false); + cmd.setDisplayPref(Tristate::True); + cmd.setSyncPref(Tristate::True); + cmd.setIndexPref(Tristate::True); + + Protocol::FetchCollectionsResponse resp(9); + resp.setName(QLatin1String("Name 2")); + resp.setParentId(3); + resp.setEnabled(false); + resp.setDisplayPref(Tristate::True); + resp.setSyncPref(Tristate::True); + resp.setIndexPref(Tristate::True); + resp.setResource(QLatin1String("akonadi_fake_resource_0")); + resp.cachePolicy().setLocalParts({ QLatin1String("ALL") }); + resp.setMimeTypes({ QLatin1String("application/octet-stream"), + QLatin1String("inode/directory") }); + + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateCollectionResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.addEntity(9, QLatin1String(""), QLatin1String("")); + + QTest::newRow("create collection with local override") << scenarios << notification; + } + + + { + Protocol::CreateCollectionCommand cmd; + cmd.setName(QLatin1String("TopLevel")); + cmd.setParent(Scope(0)); + cmd.setMimeTypes({ QLatin1String("inode/directory") }); + + Protocol::FetchCollectionsResponse resp(10); + resp.setName(QLatin1String("TopLevel")); + resp.setParentId(0); + resp.setEnabled(true); + resp.setMimeTypes({ QLatin1String("inode/directory") }); + resp.cachePolicy().setLocalParts({ QLatin1String("ALL") }); + resp.setResource(QLatin1String("akonadi_fake_resource_0")); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario("akonadi_fake_resource_0") + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateCollectionResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.setSessionId("akonadi_fake_resource_0"); + notification.setParentCollection(0); + notification.addEntity(10, QLatin1String(""), QLatin1String("")); + + QTest::newRow("create top-level collection") << scenarios << notification; + } + + } + + void testCreate() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Protocol::ChangeNotification, notification); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + QSignalSpy *notificationSpy = FakeAkonadiServer::instance()->notificationSpy(); + if (notification.isValid()) { + QCOMPARE(notificationSpy->count(), 1); + const Protocol::ChangeNotification::List notifications = notificationSpy->takeFirst().first().value(); + QCOMPARE(notifications.count(), 1); + QCOMPARE(notifications.first(), notification); + } else { + QVERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + } + } + +}; + +AKTEST_FAKESERVER_MAIN(CreateHandlerTest) + +#include "createhandlertest.moc" diff --git a/autotests/server/dbconfigtest.cpp b/autotests/server/dbconfigtest.cpp new file mode 100644 index 0000000..6ddc18b --- /dev/null +++ b/autotests/server/dbconfigtest.cpp @@ -0,0 +1,64 @@ +/* + Copyright (c) 2011 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 +#include + +#include +#include + +#include + +#define QL1S(x) QStringLiteral(x) + +using namespace Akonadi; +using namespace Akonadi::Server; + +class DbConfigTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDbConfig() + { + // doesn't work, DbConfig has an internal singleton-like cache... + //QFETCH( QString, driverName ); + const QString driverName(QL1S("QMYSQL")); + + // isolated config file to not conflict with a running instance + akTestSetInstanceIdentifier(QL1S("unit-test")); + + { + QSettings s(StandardDirs::serverConfigFile(XdgBaseDirs::WriteOnly)); + s.setValue(QL1S("General/Driver"), driverName); + } + + QScopedPointer cfg(DbConfig::configuredDatabase()); + + QVERIFY(!cfg.isNull()); + QCOMPARE(cfg->driverName(), driverName); + QCOMPARE(cfg->databaseName(), QL1S("akonadi")); + QCOMPARE(cfg->useInternalServer(), true); + QCOMPARE(cfg->sizeThreshold(), 4096ll); + } +}; + +AKTEST_MAIN(DbConfigTest) + +#include "dbconfigtest.moc" diff --git a/autotests/server/dbinitializer.cpp b/autotests/server/dbinitializer.cpp new file mode 100644 index 0000000..fb9221a --- /dev/null +++ b/autotests/server/dbinitializer.cpp @@ -0,0 +1,197 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "dbinitializer.h" + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +DbInitializer::~DbInitializer() +{ + cleanup(); +} + +Resource DbInitializer::createResource(const char *name) +{ + Resource res; + qint64 id = -1; + res.setName(QLatin1String(name)); + const bool ret = res.insert(&id); + Q_ASSERT(ret); + Q_UNUSED(ret); + mResource = res; + return res; +} + +Collection DbInitializer::createCollection(const char *name, const Collection &parent) +{ + Collection col; + if (parent.isValid()) { + col.setParent(parent); + } + col.setName(QLatin1String(name)); + col.setRemoteId(QLatin1String(name)); + col.setResource(mResource); + const bool ret = col.insert(); + Q_ASSERT(ret); + Q_UNUSED(ret); + return col; +} + +PimItem DbInitializer::createItem(const char *name, const Collection &parent) +{ + PimItem item; + MimeType mimeType = MimeType::retrieveByName(QLatin1String("test")); + if (!mimeType.isValid()) { + MimeType mt; + mt.setName(QLatin1String("test")); + mt.insert(); + mimeType = mt; + } + item.setMimeType(mimeType); + item.setCollection(parent); + item.setRemoteId(QLatin1String(name)); + const bool ret = item.insert() ; + Q_ASSERT(ret); + Q_UNUSED(ret); + return item; +} + +QByteArray DbInitializer::toByteArray(bool enabled) +{ + if (enabled) { + return "TRUE"; + } + return "FALSE"; +} + +QByteArray DbInitializer::toByteArray(Akonadi::Tristate tristate) +{ + + switch (tristate) { + case Akonadi::Tristate::True: + return "TRUE"; + case Akonadi::Tristate::False: + return "FALSE"; + case Akonadi::Tristate::Undefined: + default: + break; + } + return "DEFAULT"; +} + +Akonadi::Protocol::FetchCollectionsResponse DbInitializer::listResponse(const Collection &col, + bool ancestors, + bool mimetypes, + const QStringList &ancestorFetchScope) +{ + Akonadi::Protocol::FetchCollectionsResponse resp(col.id()); + resp.setParentId(col.parentId()); + resp.setName(col.name()); + if (mimetypes) { + QStringList mts; + for (const Akonadi::Server::MimeType &mt : col.mimeTypes()) { + mts << mt.name(); + } + resp.setMimeTypes(mts); + } + resp.setRemoteId(col.remoteId()); + resp.setRemoteRevision(col.remoteRevision()); + resp.setResource(col.resource().name()); + resp.setIsVirtual(col.isVirtual()); + Akonadi::Protocol::CachePolicy cp; + cp.setInherit(true); + cp.setLocalParts({ QLatin1String("ALL") }); + resp.setCachePolicy(cp); + if (ancestors) { + QVector ancs; + Collection parent = col.parent(); + while (parent.isValid()) { + Akonadi::Protocol::Ancestor anc; + anc.setId(parent.id()); + anc.setRemoteId(parent.remoteId()); + anc.setName(parent.name()); + if (!ancestorFetchScope.isEmpty()) { + anc.setRemoteId(parent.remoteId()); + Akonadi::Protocol::Attributes attrs; + Q_FOREACH(const CollectionAttribute &attr, parent.attributes()) { + if (ancestorFetchScope.contains(QString::fromLatin1(attr.type()))) { + attrs.insert(attr.type(), attr.value()); + } + } + attrs.insert("ENABLED", parent.enabled() ? "TRUE" : "FALSE"); + anc.setAttributes(attrs); + } + parent = parent.parent(); + ancs.push_back(anc); + } + // Root + ancs.push_back(Akonadi::Protocol::Ancestor(0)); + resp.setAncestors(ancs); + } + resp.setReferenced(col.referenced()); + resp.setEnabled(col.enabled()); + resp.setDisplayPref(col.displayPref()); + resp.setSyncPref(col.syncPref()); + resp.setIndexPref(col.indexPref()); + + Akonadi::Protocol::Attributes attrs; + Q_FOREACH(const CollectionAttribute &attr, col.attributes()) { + attrs.insert(attr.type(), attr.value()); + } + resp.setAttributes(attrs); + return resp; +} + +Collection DbInitializer::collection(const char *name) +{ + return Collection::retrieveByName(QLatin1String(name)); +} + +void DbInitializer::cleanup() +{ + Q_FOREACH (Collection col, mResource.collections()) { + if (!col.isVirtual()) { + col.remove(); + } + } + mResource.remove(); + + if (DataStore::self()->database().isOpen()) { + { + QueryBuilder qb( Relation::tableName(), QueryBuilder::Delete ); + qb.exec(); + } + { + QueryBuilder qb(Tag::tableName(), QueryBuilder::Delete); + qb.exec(); + } + { + QueryBuilder qb(TagType::tableName(), QueryBuilder::Delete); + qb.exec(); + } + } + + Q_FOREACH(PimItem item, PimItem::retrieveAll()) { + item.remove(); + } +} diff --git a/autotests/server/dbinitializer.h b/autotests/server/dbinitializer.h new file mode 100644 index 0000000..b7da20b --- /dev/null +++ b/autotests/server/dbinitializer.h @@ -0,0 +1,47 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 AKTEST_DBINITIALIZER_H +#define AKTEST_DBINITIALIZER_H + +#include "entities.h" +#include + +class DbInitializer +{ +public: + ~DbInitializer(); + Akonadi::Server::Resource createResource(const char *name); + Akonadi::Server::Collection createCollection(const char *name, + const Akonadi::Server::Collection &parent = Akonadi::Server::Collection()); + Akonadi::Server::PimItem createItem(const char *name, const Akonadi::Server::Collection &parent); + QByteArray toByteArray(bool enabled); + QByteArray toByteArray(Akonadi::Tristate tristate); + Akonadi::Protocol::FetchCollectionsResponse listResponse(const Akonadi::Server::Collection &col, + bool ancestors = false, + bool mimetypes = true, + const QStringList &ancestorFetchScope = QStringList()); + Akonadi::Server::Collection collection(const char *name); + + void cleanup(); + +private: + Akonadi::Server::Resource mResource; +}; + +#endif diff --git a/autotests/server/dbinitializertest.cpp b/autotests/server/dbinitializertest.cpp new file mode 100644 index 0000000..9388c2a --- /dev/null +++ b/autotests/server/dbinitializertest.cpp @@ -0,0 +1,183 @@ +/* + Copyright (c) 2010 Tobias Koenig + Copyright (c) 2012 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 "dbinitializertest.h" +#include "unittestschema.h" + +#include +#include + +#include "storage/dbinitializer.h" +#include + +#define QL1S(x) QLatin1String(x) + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(QVector) + +class StatementCollector : public TestInterface +{ +public: + virtual void execStatement(const QString &statement) + { + statements.push_back(statement); + } + + QStringList statements; +}; + +class DbFakeIntrospector : public DbIntrospector +{ +public: + DbFakeIntrospector(const QSqlDatabase &database) + : DbIntrospector(database) + , m_hasTable(false) + , m_hasIndex(false) + , m_tableEmpty(true) + { + } + + virtual bool hasTable(const QString &tableName) + { + Q_UNUSED(tableName); + return m_hasTable; + } + virtual bool hasIndex(const QString &tableName, const QString &indexName) + { + Q_UNUSED(tableName); + Q_UNUSED(indexName); + return m_hasIndex; + } + virtual bool hasColumn(const QString &tableName, const QString &columnName) + { + Q_UNUSED(tableName); + Q_UNUSED(columnName); + return false; + } + virtual bool isTableEmpty(const QString &tableName) + { + Q_UNUSED(tableName); + return m_tableEmpty; + } + virtual QVector< ForeignKey > foreignKeyConstraints(const QString &tableName) + { + Q_UNUSED(tableName); + return m_foreignKeys; + } + + QVector m_foreignKeys; + bool m_hasTable; + bool m_hasIndex; + bool m_tableEmpty; +}; + +void DbInitializerTest::initTestCase() +{ + Q_INIT_RESOURCE(akonadidb); +} + +void DbInitializerTest::testRun_data() +{ + QTest::addColumn("driverName"); + QTest::addColumn("filename"); + QTest::addColumn("hasTable"); + QTest::addColumn >("fks"); + QTest::addColumn("hasFks"); + + QVector fks; + + QTest::newRow("mysql") << "QMYSQL" << ":dbinit_mysql" << false << fks << true; + QTest::newRow("sqlite") << "QSQLITE" << ":dbinit_sqlite" << false << fks << false; + QTest::newRow("psql") << "QPSQL" << ":dbinit_psql" << false << fks << true; + + DbIntrospector::ForeignKey fk; + fk.name = QL1S("myForeignKeyIdentifier"); + fk.column = QL1S("collectionId"); + fk.refTable = QL1S("CollectionTable"); + fk.refColumn = QL1S("id"); + fk.onUpdate = QL1S("RESTRICT"); + fk.onDelete = QL1S("CASCADE"); + fks.push_back(fk); + + QTest::newRow("mysql (incremental)") << "QMYSQL" << ":dbinit_mysql_incremental" << true << fks << true; + QTest::newRow("sqlite (incremental)") << "QSQLITE" << ":dbinit_sqlite_incremental" << true << fks << false; + QTest::newRow("psql (incremental)") << "QPSQL" << ":dbinit_psql_incremental" << true << fks << true; +} + +void DbInitializerTest::testRun() +{ + QFETCH(QString, driverName); + QFETCH(QString, filename); + QFETCH(bool, hasTable); + QFETCH(QVector, fks); + QFETCH(bool, hasFks); + + QFile file(filename); + QVERIFY(file.open(QFile::ReadOnly)); + + if (QSqlDatabase::drivers().contains(driverName)) { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName, driverName); + UnitTestSchema schema; + DbInitializer::Ptr initializer = DbInitializer::createInstance(db, &schema); + QVERIFY(bool(initializer)); + + StatementCollector collector; + initializer->setTestInterface(&collector); + DbFakeIntrospector *introspector = new DbFakeIntrospector(db); + introspector->m_hasTable = hasTable; + introspector->m_hasIndex = hasTable; + introspector->m_tableEmpty = !hasTable; + introspector->m_foreignKeys = fks; + initializer->setIntrospector(DbIntrospector::Ptr(introspector)); + + QVERIFY(initializer->run()); + QVERIFY(initializer->updateIndexesAndConstraints()); + QVERIFY(!collector.statements.isEmpty()); + + Q_FOREACH (const QString &statement, collector.statements) { + const QString expected = readNextStatement(&file).simplified(); + + QString normalized = statement.simplified(); + normalized.replace(QLatin1String(" ,"), QLatin1String(",")); + normalized.replace(QLatin1String(" )"), QLatin1String(")")); + QCOMPARE(normalized, expected); + } + + QVERIFY(initializer->errorMsg().isEmpty()); + QCOMPARE(initializer->hasForeignKeyConstraints(), hasFks); + } +} + +QString DbInitializerTest::readNextStatement(QIODevice *io) +{ + QString statement; + while (!io->atEnd()) { + const QString line = QString::fromUtf8(io->readLine()); + if (line.trimmed().isEmpty() && !statement.isEmpty()) { + return statement; + } + statement += line; + } + + return statement; +} + +AKTEST_MAIN(DbInitializerTest) diff --git a/autotests/server/dbinitializertest.h b/autotests/server/dbinitializertest.h new file mode 100644 index 0000000..3c54116 --- /dev/null +++ b/autotests/server/dbinitializertest.h @@ -0,0 +1,40 @@ +/* + Copyright (c) 2010 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 DBINITIALIZERTEST_H +#define DBINITIALIZERTEST_H + +#include +#include + +class DbInitializerTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void testRun_data(); + void testRun(); + +private: + static QString readNextStatement(QIODevice *io); +}; + +#endif diff --git a/autotests/server/dbintrospectortest.cpp b/autotests/server/dbintrospectortest.cpp new file mode 100644 index 0000000..62f63eb --- /dev/null +++ b/autotests/server/dbintrospectortest.cpp @@ -0,0 +1,94 @@ +/* + Copyright (c) 2012 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 +#include + +#include +#include +#include + +#define QL1S(x) QLatin1String(x) + +using namespace Akonadi::Server; + +class DbIntrospectorTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testHasIndexQuery_data() + { + QTest::addColumn("driverName"); + QTest::addColumn("indexQuery"); + + QTest::newRow("mysql") << "QMYSQL" << "SHOW INDEXES FROM myTable WHERE `Key_name` = 'myIndex'"; + QTest::newRow("sqlite") << "QSQLITE" << "SELECT * FROM sqlite_master WHERE type='index' AND tbl_name='myTable' AND name='myIndex';"; + QTest::newRow("psql") << "QPSQL" << "SELECT indexname FROM pg_catalog.pg_indexes WHERE tablename ilike 'myTable' AND indexname ilike 'myIndex' UNION SELECT conname FROM pg_catalog.pg_constraint WHERE conname ilike 'myIndex'"; + } + + void testHasIndexQuery() + { + QFETCH(QString, driverName); + QFETCH(QString, indexQuery); + + if (QSqlDatabase::drivers().contains(driverName)) { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName, driverName); + DbIntrospector::Ptr introspector = DbIntrospector::createInstance(db); + QVERIFY(introspector); + QCOMPARE(introspector->hasIndexQuery(QL1S("myTable"), QL1S("myIndex")), indexQuery); + } + } + + void testHasIndex_data() + { + QTest::addColumn("driverName"); + QTest::addColumn("shouldThrow"); + + QTest::newRow("mysql") << "QMYSQL" << true; + QTest::newRow("sqlite") << "QSQLITE" << true; + QTest::newRow("psql") << "QPSQL" << true; + } + + void testHasIndex() + { + QFETCH(QString, driverName); + QFETCH(bool, shouldThrow); + + if (QSqlDatabase::drivers().contains(driverName)) { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName, driverName + QL1S("hasIndex")); + DbIntrospector::Ptr introspector = DbIntrospector::createInstance(db); + QVERIFY(introspector); + + bool didThrow = false; + try { + QVERIFY(introspector->hasIndex(QL1S("myTable"), QL1S("myIndex"))); + } catch (const DbException &e) { + didThrow = true; + qDebug() << Q_FUNC_INFO << e.what(); + } + QCOMPARE(didThrow, shouldThrow); + } + } + +}; + +AKTEST_MAIN(DbIntrospectorTest) + +#include "dbintrospectortest.moc" diff --git a/autotests/server/dbpopulator.h b/autotests/server/dbpopulator.h new file mode 100644 index 0000000..57d7b4c --- /dev/null +++ b/autotests/server/dbpopulator.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef AKONADI_SERVER_DBPOPULATOR_H +#define AKONADI_SERVER_DBPOPULATOR_H + +#include + +namespace Akonadi { +namespace Server { + +class DbPopulator +{ +public: + DbPopulator(); + ~DbPopulator(); + + bool run(); + +}; + +} +} + +#endif // AKONADI_SERVER_DBPOPULATOR_H diff --git a/autotests/server/dbpopulator.xsl b/autotests/server/dbpopulator.xsl new file mode 100644 index 0000000..e4d51ba --- /dev/null +++ b/autotests/server/dbpopulator.xsl @@ -0,0 +1,387 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + set + + + + + + + + + + + + + collection.addMimeType(mimeType); + + + + + + + + + + pimItem.addFlag(flag); + + + + + + + + + + pimItem.addTag(tag); + + + + + + + + + + + + + + + + + + + + + + + + ; + + + + + + + + + + + + + + + + + + + + + partType.id() + + + + + mimeType.id() + + + + + + + .id() + + + + NULL + + + + + + + . + ( + + + + + + 0 + + + + + + + + + + Tristate::Undefined + + + + + + + + + + QString() + + + QLatin1String("") + + + + + "" + + + QDateTime::fromString(QLatin1String(""), Qt::ISODate) + + + ); + + + + + + if (!.insert()) { + akError() << "Failed to insert into database"; + akError() << "DB Error:" << FakeDataStore::self()->database().lastError().text(); + return false; + } + akDebug() << " ' + + + + + + + + : + + + + + + + + ' inserted with id" << .id(); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +/* + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#include <shared/akdebug.h> +#include "dbpopulator.h" +#include "entities.h" +#include "fakedatastore.h" + +#include <QtSql/QSqlDatabase> +#include <QtSql/QSqlQuery> +#include <QtSql/QSqlError> + +#include <QtCore/QString> +#include <QtCore/QVariant> + +using namespace Akonadi::Server; + +DbPopulator::DbPopulator() +{ +} + +DbPopulator::~DbPopulator() +{ +} + + + +bool DbPopulator::run() +{ + + + + + + MimeType + mimeType + + + + + Flag + flag + + + + + PartType + partType + + + + + Tag + tag + + + + + + + + + + + + + + + + + + + + akDebug() << "Database successfully populated"; + return true; +} + + + + + diff --git a/autotests/server/dbtest_data/dbdata.xml b/autotests/server/dbtest_data/dbdata.xml new file mode 100644 index 0000000..1c31fa7 --- /dev/null +++ b/autotests/server/dbtest_data/dbdata.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autotests/server/dbtest_data/dbinit_mysql b/autotests/server/dbtest_data/dbinit_mysql new file mode 100644 index 0000000..671878e --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_mysql @@ -0,0 +1,149 @@ +CREATE TABLE SchemaVersionTable (version INTEGER NOT NULL DEFAULT 0) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +INSERT INTO SchemaVersionTable (version) VALUES (22) + +CREATE TABLE ResourceTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARBINARY(255) NOT NULL UNIQUE, + isVirtual BOOL DEFAULT false) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +INSERT INTO ResourceTable (isVirtual,name) VALUES (true,'akonadi_search_resource') + +CREATE TABLE CollectionTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + remoteId VARBINARY(255), + remoteRevision VARBINARY(255), + name VARBINARY(255) NOT NULL, + parentId BIGINT, + resourceId BIGINT NOT NULL, + subscribed BOOL NOT NULL DEFAULT true, + cachePolicyInherit BOOL NOT NULL DEFAULT true, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT false, + cachePolicyLocalParts VARBINARY(255), + queryString VARBINARY(32768), + queryLanguage VARBINARY(255), + FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +INSERT INTO CollectionTable (name,parentId,resourceId) VALUES ('Search',NULL,1) + +CREATE TABLE MimeTypeTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARBINARY(255) NOT NULL UNIQUE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +INSERT INTO MimeTypeTable (name) VALUES ('application/octet-stream') + +INSERT INTO MimeTypeTable (name) VALUES ('message/rfc822') + +INSERT INTO MimeTypeTable (name) VALUES ('text/calendar') + +INSERT INTO MimeTypeTable (name) VALUES ('text/vcard') + +INSERT INTO MimeTypeTable (name) VALUES ('inode/directory') + +CREATE TABLE PimItemTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + rev INTEGER NOT NULL DEFAULT 0, + remoteId VARBINARY(255), + remoteRevision VARBINARY(255), + collectionId BIGINT, + mimeTypeId BIGINT, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP, + dirty BOOL, + size BIGINT NOT NULL DEFAULT 0, + FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE FlagTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARBINARY(255) NOT NULL UNIQUE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +INSERT INTO FlagTable (name) VALUES ('important') + +INSERT INTO FlagTable (name) VALUES ('has_attachment') + +INSERT INTO FlagTable (name) VALUES ('spam') + +INSERT INTO FlagTable (name) VALUES ('\\ANSWERED') + +INSERT INTO FlagTable (name) VALUES ('\\FLAGGED') + +INSERT INTO FlagTable (name) VALUES ('\\DELETED') + +INSERT INTO FlagTable (name) VALUES ('\\SEEN') + +INSERT INTO FlagTable (name) VALUES ('\\DRAFT') + +CREATE TABLE PartTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + pimItemId BIGINT NOT NULL, + name VARBINARY(255) NOT NULL, + data LONGBLOB, + datasize BIGINT NOT NULL, + version INTEGER DEFAULT 0, + external BOOL DEFAULT false, + FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE CollectionAttributeTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + collectionId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE PimItemFlagRelation (PimItem_id BIGINT NOT NULL, + Flag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id), + FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE) COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE CollectionMimeTypeRelation (Collection_id BIGINT NOT NULL, + MimeType_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id), + FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE) COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE CollectionPimItemRelation (Collection_id BIGINT NOT NULL, + PimItem_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id), + FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE) COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE UNIQUE INDEX CollectionTable_parentAndNameIndex ON CollectionTable (parentId,name) + +CREATE INDEX PimItemTable_collectionIndex ON PimItemTable (collectionId) + +CREATE UNIQUE INDEX PartTable_pimItemIdNameIndex ON PartTable (pimItemId,name) + +CREATE INDEX PartTable_pimItemNameIndex ON PartTable (name) + +CREATE INDEX CollectionAttributeTable_collectionIndex ON CollectionAttributeTable (collectionId) + +ALTER TABLE CollectionTable ADD FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionTable ADD FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE PartTable ADD FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionAttributeTable ADD FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + diff --git a/autotests/server/dbtest_data/dbinit_mysql_incremental b/autotests/server/dbtest_data/dbinit_mysql_incremental new file mode 100644 index 0000000..782e6d1 --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_mysql_incremental @@ -0,0 +1,125 @@ +ALTER TABLE SchemaVersionTable ADD COLUMN version INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE ResourceTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE ResourceTable ADD COLUMN name VARBINARY(255) NOT NULL UNIQUE + +ALTER TABLE ResourceTable ADD COLUMN isVirtual BOOL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE CollectionTable ADD COLUMN remoteId VARBINARY(255) + +ALTER TABLE CollectionTable ADD COLUMN remoteRevision VARBINARY(255) + +ALTER TABLE CollectionTable ADD COLUMN name VARBINARY(255) NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN parentId BIGINT + +ALTER TABLE CollectionTable ADD COLUMN resourceId BIGINT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN subscribed BOOL NOT NULL DEFAULT true + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyInherit BOOL NOT NULL DEFAULT true + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicySyncOnDemand BOOL NOT NULL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyLocalParts VARBINARY(255) + +ALTER TABLE CollectionTable ADD COLUMN queryString VARBINARY(32768) + +ALTER TABLE CollectionTable ADD COLUMN queryLanguage VARBINARY(255) + +ALTER TABLE MimeTypeTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE MimeTypeTable ADD COLUMN name VARBINARY(255) NOT NULL UNIQUE + +ALTER TABLE PimItemTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE PimItemTable ADD COLUMN rev INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE PimItemTable ADD COLUMN remoteId VARBINARY(255) + +ALTER TABLE PimItemTable ADD COLUMN remoteRevision VARBINARY(255) + +ALTER TABLE PimItemTable ADD COLUMN collectionId BIGINT + +ALTER TABLE PimItemTable ADD COLUMN mimeTypeId BIGINT + +ALTER TABLE PimItemTable ADD COLUMN datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN atime TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN dirty BOOL + +ALTER TABLE PimItemTable ADD COLUMN size BIGINT NOT NULL DEFAULT 0 + +ALTER TABLE FlagTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE FlagTable ADD COLUMN name VARBINARY(255) NOT NULL UNIQUE + +ALTER TABLE PartTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE PartTable ADD COLUMN pimItemId BIGINT NOT NULL + +ALTER TABLE PartTable ADD COLUMN name VARBINARY(255) NOT NULL + +ALTER TABLE PartTable ADD COLUMN data LONGBLOB + +ALTER TABLE PartTable ADD COLUMN datasize BIGINT NOT NULL + +ALTER TABLE PartTable ADD COLUMN version INTEGER DEFAULT 0 + +ALTER TABLE PartTable ADD COLUMN external BOOL DEFAULT false + +ALTER TABLE CollectionAttributeTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE CollectionAttributeTable ADD COLUMN collectionId BIGINT NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN type LONGBLOB NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN value LONGBLOB + +ALTER TABLE PimItemFlagRelation ADD COLUMN PimItem_id BIGINT NOT NULL + +ALTER TABLE PimItemFlagRelation ADD COLUMN Flag_id BIGINT NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN Collection_id BIGINT NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN MimeType_id BIGINT NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN Collection_id BIGINT NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN PimItem_id BIGINT NOT NULL + +ALTER TABLE PimItemTable DROP FOREIGN KEY myForeignKeyIdentifier + +ALTER TABLE CollectionAttributeTable DROP FOREIGN KEY myForeignKeyIdentifier + +ALTER TABLE CollectionTable ADD FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionTable ADD FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE PartTable ADD FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionAttributeTable ADD FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE diff --git a/autotests/server/dbtest_data/dbinit_psql b/autotests/server/dbtest_data/dbinit_psql new file mode 100644 index 0000000..a278418 --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_psql @@ -0,0 +1,129 @@ +CREATE TABLE SchemaVersionTable (version INTEGER NOT NULL DEFAULT 0) + +INSERT INTO SchemaVersionTable (version) VALUES (22) + +CREATE TABLE ResourceTable (id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + isVirtual BOOL DEFAULT false) + +INSERT INTO ResourceTable (isVirtual,name) VALUES (true,'akonadi_search_resource') + +CREATE TABLE CollectionTable (id SERIAL PRIMARY KEY, + remoteId TEXT, + remoteRevision TEXT, + name TEXT NOT NULL, + parentId int8, + resourceId int8 NOT NULL, + subscribed BOOL NOT NULL DEFAULT true, + cachePolicyInherit BOOL NOT NULL DEFAULT true, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT false, + cachePolicyLocalParts TEXT, + queryString TEXT, + queryLanguage TEXT) + +INSERT INTO CollectionTable (name,parentId,resourceId) VALUES ('Search',NULL,1) + +CREATE TABLE MimeTypeTable (id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL) + +INSERT INTO MimeTypeTable (name) VALUES ('application/octet-stream') + +INSERT INTO MimeTypeTable (name) VALUES ('message/rfc822') + +INSERT INTO MimeTypeTable (name) VALUES ('text/calendar') + +INSERT INTO MimeTypeTable (name) VALUES ('text/vcard') + +INSERT INTO MimeTypeTable (name) VALUES ('inode/directory') + +CREATE TABLE PimItemTable (id SERIAL PRIMARY KEY, + rev INTEGER NOT NULL DEFAULT 0, + remoteId TEXT, + remoteRevision TEXT, + collectionId int8, + mimeTypeId int8, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP, + dirty BOOL, + size int8 NOT NULL DEFAULT 0) + +CREATE TABLE FlagTable (id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL) + +INSERT INTO FlagTable (name) VALUES ('important') + +INSERT INTO FlagTable (name) VALUES ('has_attachment') + +INSERT INTO FlagTable (name) VALUES ('spam') + +INSERT INTO FlagTable (name) VALUES ('\ANSWERED') + +INSERT INTO FlagTable (name) VALUES ('\FLAGGED') + +INSERT INTO FlagTable (name) VALUES ('\DELETED') + +INSERT INTO FlagTable (name) VALUES ('\SEEN') + +INSERT INTO FlagTable (name) VALUES ('\DRAFT') + +CREATE TABLE PartTable (id SERIAL PRIMARY KEY, + pimItemId int8 NOT NULL, + name TEXT NOT NULL, + data BYTEA, + datasize int8 NOT NULL, + version INTEGER DEFAULT 0, + external BOOL DEFAULT false) + +CREATE TABLE CollectionAttributeTable (id SERIAL PRIMARY KEY, + collectionId int8 NOT NULL, + type BYTEA NOT NULL, + value BYTEA) + +CREATE TABLE PimItemFlagRelation (PimItem_id int8 NOT NULL, + Flag_id int8 NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id)) + +CREATE TABLE CollectionMimeTypeRelation (Collection_id int8 NOT NULL, + MimeType_id int8 NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id)) + +CREATE TABLE CollectionPimItemRelation (Collection_id int8 NOT NULL, + PimItem_id int8 NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id)) + +CREATE UNIQUE INDEX CollectionTable_parentAndNameIndex ON CollectionTable (parentId,name) + +CREATE INDEX PimItemTable_collectionIndex ON PimItemTable (collectionId) + +CREATE UNIQUE INDEX PartTable_pimItemIdNameIndex ON PartTable (pimItemId,name) + +CREATE INDEX PartTable_pimItemNameIndex ON PartTable (name) + +CREATE INDEX CollectionAttributeTable_collectionIndex ON CollectionAttributeTable (collectionId) + +ALTER TABLE CollectionTable ADD CONSTRAINT CollectionTableparentId_Collectionid_fk FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionTable ADD CONSTRAINT CollectionTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE PartTable ADD CONSTRAINT PartTablepimItemId_PimItemid_fk FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionAttributeTable ADD CONSTRAINT CollectionAttributeTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD CONSTRAINT PimItemFlagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD CONSTRAINT PimItemFlagRelationFlag_id_Flagid_fk FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD CONSTRAINT CollectionMimeTypeRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD CONSTRAINT CollectionMimeTypeRelationMimeType_id_MimeTypeid_fk FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD CONSTRAINT CollectionPimItemRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD CONSTRAINT CollectionPimItemRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + diff --git a/autotests/server/dbtest_data/dbinit_psql_incremental b/autotests/server/dbtest_data/dbinit_psql_incremental new file mode 100644 index 0000000..befba96 --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_psql_incremental @@ -0,0 +1,126 @@ +ALTER TABLE SchemaVersionTable ADD COLUMN version INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE ResourceTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE ResourceTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE ResourceTable ADD COLUMN isVirtual BOOL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE CollectionTable ADD COLUMN remoteId TEXT + +ALTER TABLE CollectionTable ADD COLUMN remoteRevision TEXT + +ALTER TABLE CollectionTable ADD COLUMN name TEXT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN parentId int8 + +ALTER TABLE CollectionTable ADD COLUMN resourceId int8 NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN subscribed BOOL NOT NULL DEFAULT true + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyInherit BOOL NOT NULL DEFAULT true + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicySyncOnDemand BOOL NOT NULL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyLocalParts TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryString TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryLanguage TEXT + +ALTER TABLE MimeTypeTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE MimeTypeTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE PimItemTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE PimItemTable ADD COLUMN rev INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE PimItemTable ADD COLUMN remoteId TEXT + +ALTER TABLE PimItemTable ADD COLUMN remoteRevision TEXT + +ALTER TABLE PimItemTable ADD COLUMN collectionId int8 + +ALTER TABLE PimItemTable ADD COLUMN mimeTypeId int8 + +ALTER TABLE PimItemTable ADD COLUMN datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN atime TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN dirty BOOL + +ALTER TABLE PimItemTable ADD COLUMN size int8 NOT NULL DEFAULT 0 + +ALTER TABLE FlagTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE FlagTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE PartTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE PartTable ADD COLUMN pimItemId int8 NOT NULL + +ALTER TABLE PartTable ADD COLUMN name TEXT NOT NULL + +ALTER TABLE PartTable ADD COLUMN data BYTEA + +ALTER TABLE PartTable ADD COLUMN datasize int8 NOT NULL + +ALTER TABLE PartTable ADD COLUMN version INTEGER DEFAULT 0 + +ALTER TABLE PartTable ADD COLUMN external BOOL DEFAULT false + +ALTER TABLE CollectionAttributeTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE CollectionAttributeTable ADD COLUMN collectionId int8 NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN type BYTEA NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN value BYTEA + +ALTER TABLE PimItemFlagRelation ADD COLUMN PimItem_id int8 NOT NULL + +ALTER TABLE PimItemFlagRelation ADD COLUMN Flag_id int8 NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN Collection_id int8 NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN MimeType_id int8 NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN Collection_id int8 NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN PimItem_id int8 NOT NULL + +ALTER TABLE PimItemTable DROP CONSTRAINT myForeignKeyIdentifier + +ALTER TABLE CollectionAttributeTable DROP CONSTRAINT myForeignKeyIdentifier + +ALTER TABLE CollectionTable ADD CONSTRAINT CollectionTableparentId_Collectionid_fk FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionTable ADD CONSTRAINT CollectionTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE PartTable ADD CONSTRAINT PartTablepimItemId_PimItemid_fk FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionAttributeTable ADD CONSTRAINT CollectionAttributeTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD CONSTRAINT PimItemFlagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD CONSTRAINT PimItemFlagRelationFlag_id_Flagid_fk FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD CONSTRAINT CollectionMimeTypeRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD CONSTRAINT CollectionMimeTypeRelationMimeType_id_MimeTypeid_fk FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD CONSTRAINT CollectionPimItemRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD CONSTRAINT CollectionPimItemRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + diff --git a/autotests/server/dbtest_data/dbinit_sqlite b/autotests/server/dbtest_data/dbinit_sqlite new file mode 100644 index 0000000..734a0d0 --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_sqlite @@ -0,0 +1,104 @@ +CREATE TABLE SchemaVersionTable (version INTEGER NOT NULL DEFAULT 0) + +INSERT INTO SchemaVersionTable (version) VALUES (22) + +CREATE TABLE ResourceTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL, + isVirtual BOOL DEFAULT 0) + +INSERT INTO ResourceTable (isVirtual,name) VALUES (1,'akonadi_search_resource') + +CREATE TABLE CollectionTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + remoteId TEXT, + remoteRevision TEXT, + name TEXT NOT NULL, + parentId BIGINT, + resourceId BIGINT NOT NULL, + subscribed BOOL NOT NULL DEFAULT 1, + cachePolicyInherit BOOL NOT NULL DEFAULT 1, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT 0, + cachePolicyLocalParts TEXT, + queryString TEXT, + queryLanguage TEXT) + +INSERT INTO CollectionTable (name,parentId,resourceId) VALUES ('Search',NULL,1) + +CREATE TABLE MimeTypeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL) + +INSERT INTO MimeTypeTable (name) VALUES ('application/octet-stream') + +INSERT INTO MimeTypeTable (name) VALUES ('message/rfc822') + +INSERT INTO MimeTypeTable (name) VALUES ('text/calendar') + +INSERT INTO MimeTypeTable (name) VALUES ('text/vcard') + +INSERT INTO MimeTypeTable (name) VALUES ('inode/directory') + +CREATE TABLE PimItemTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + rev INTEGER NOT NULL DEFAULT 0, + remoteId TEXT, + remoteRevision TEXT, + collectionId BIGINT, + mimeTypeId BIGINT, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP, + dirty BOOL, + size BIGINT NOT NULL DEFAULT 0) + +CREATE TABLE FlagTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL) + +INSERT INTO FlagTable (name) VALUES ('important') + +INSERT INTO FlagTable (name) VALUES ('has_attachment') + +INSERT INTO FlagTable (name) VALUES ('spam') + +INSERT INTO FlagTable (name) VALUES ('\ANSWERED') + +INSERT INTO FlagTable (name) VALUES ('\FLAGGED') + +INSERT INTO FlagTable (name) VALUES ('\DELETED') + +INSERT INTO FlagTable (name) VALUES ('\SEEN') + +INSERT INTO FlagTable (name) VALUES ('\DRAFT') + +CREATE TABLE PartTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + pimItemId BIGINT NOT NULL, + name TEXT NOT NULL, + data LONGBLOB, + datasize BIGINT NOT NULL, + version INTEGER DEFAULT 0, + external BOOL DEFAULT 0) + +CREATE TABLE CollectionAttributeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + collectionId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB) + +CREATE TABLE PimItemFlagRelation (PimItem_id BIGINT NOT NULL, + Flag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id)) + +CREATE TABLE CollectionMimeTypeRelation (Collection_id BIGINT NOT NULL, + MimeType_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id)) + +CREATE TABLE CollectionPimItemRelation (Collection_id BIGINT NOT NULL, + PimItem_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id)) + +CREATE UNIQUE INDEX CollectionTable_parentAndNameIndex ON CollectionTable (parentId,name) + +CREATE INDEX PimItemTable_collectionIndex ON PimItemTable (collectionId) + +CREATE UNIQUE INDEX PartTable_pimItemIdNameIndex ON PartTable (pimItemId,name) + +CREATE INDEX PartTable_pimItemNameIndex ON PartTable (name) + +CREATE INDEX CollectionAttributeTable_collectionIndex ON CollectionAttributeTable (collectionId) diff --git a/autotests/server/dbtest_data/dbinit_sqlite_incremental b/autotests/server/dbtest_data/dbinit_sqlite_incremental new file mode 100644 index 0000000..e53a5b5 --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_sqlite_incremental @@ -0,0 +1,97 @@ +ALTER TABLE SchemaVersionTable ADD COLUMN version INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE ResourceTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE ResourceTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE ResourceTable ADD COLUMN isVirtual BOOL DEFAULT 0 + +ALTER TABLE CollectionTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN remoteId TEXT + +ALTER TABLE CollectionTable ADD COLUMN remoteRevision TEXT + +ALTER TABLE CollectionTable ADD COLUMN name TEXT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN parentId BIGINT + +ALTER TABLE CollectionTable ADD COLUMN resourceId BIGINT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN subscribed BOOL NOT NULL DEFAULT 1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyInherit BOOL NOT NULL DEFAULT 1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicySyncOnDemand BOOL NOT NULL DEFAULT 0 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyLocalParts TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryString TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryLanguage TEXT + +ALTER TABLE MimeTypeTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE MimeTypeTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE PimItemTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE PimItemTable ADD COLUMN rev INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE PimItemTable ADD COLUMN remoteId TEXT + +ALTER TABLE PimItemTable ADD COLUMN remoteRevision TEXT + +ALTER TABLE PimItemTable ADD COLUMN collectionId BIGINT + +ALTER TABLE PimItemTable ADD COLUMN mimeTypeId BIGINT + +ALTER TABLE PimItemTable ADD COLUMN datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN atime TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN dirty BOOL + +ALTER TABLE PimItemTable ADD COLUMN size BIGINT NOT NULL DEFAULT 0 + +ALTER TABLE FlagTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE FlagTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE PartTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE PartTable ADD COLUMN pimItemId BIGINT NOT NULL + +ALTER TABLE PartTable ADD COLUMN name TEXT NOT NULL + +ALTER TABLE PartTable ADD COLUMN data LONGBLOB + +ALTER TABLE PartTable ADD COLUMN datasize BIGINT NOT NULL + +ALTER TABLE PartTable ADD COLUMN version INTEGER DEFAULT 0 + +ALTER TABLE PartTable ADD COLUMN external BOOL DEFAULT 0 + +ALTER TABLE CollectionAttributeTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN collectionId BIGINT NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN type LONGBLOB NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN value LONGBLOB + +ALTER TABLE PimItemFlagRelation ADD COLUMN PimItem_id BIGINT NOT NULL + +ALTER TABLE PimItemFlagRelation ADD COLUMN Flag_id BIGINT NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN Collection_id BIGINT NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN MimeType_id BIGINT NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN Collection_id BIGINT NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN PimItem_id BIGINT NOT NULL diff --git a/autotests/server/dbtest_data/dbtest_data.qrc b/autotests/server/dbtest_data/dbtest_data.qrc new file mode 100644 index 0000000..9d4fa2f --- /dev/null +++ b/autotests/server/dbtest_data/dbtest_data.qrc @@ -0,0 +1,14 @@ + + + unittest_dbupdate.xml + unittest_schema.xml + + dbinit_mysql + dbinit_psql + dbinit_sqlite + + dbinit_mysql_incremental + dbinit_psql_incremental + dbinit_sqlite_incremental + + diff --git a/autotests/server/dbtest_data/unittest_dbupdate.xml b/autotests/server/dbtest_data/unittest_dbupdate.xml new file mode 100644 index 0000000..9f2bc88 --- /dev/null +++ b/autotests/server/dbtest_data/unittest_dbupdate.xml @@ -0,0 +1,168 @@ + + + + + + + + + ALTER TABLE LocationTable DROP COLUMN existCount; + ALTER TABLE LocationTable DROP COLUMN recentCount; + ALTER TABLE LocationTable DROP COLUMN unseenCount; + ALTER TABLE LocationTable DROP COLUMN firstUnseen; + + + + UPDATE LocationTable SET subscribed = true; + + + + ALTER TABLE LocationTable DROP COLUMN cachePolicyId; + ALTER TABLE ResourceTable DROP COLUMN cachePolicyId; + DROP TABLE CachePolicyTable; + + + + UPDATE PartTable SET name = 'PLD:ENVELOPE' WHERE name = 'ENVELOPE'; + UPDATE PartTable SET name = 'PLD:RFC822' WHERE name = 'RFC822'; + UPDATE PartTable SET name = 'PLD:HEAD' WHERE name = 'HEAD'; + UPDATE PartTable SET name = concat( 'ATR:', name ) WHERE substr( name, 1, 4 ) != 'PLD:'; + + + + + DROP TABLE CollectionTable; + ALTER TABLE LocationTable RENAME TO CollectionTable; + ALTER TABLE PimItemTable DROP COLUMN collectionId; + ALTER TABLE PimItemTable CHANGE locationId collectionId BIGINT; + DROP TABLE CollectionAttributeTable; + ALTER TABLE LocationAttributeTable CHANGE locationId collectionId BIGINT; + ALTER TABLE LocationAttributeTable RENAME TO CollectionAttributeTable; + DROP TABLE CollectionMimeTypeRelation; + ALTER TABLE LocationMimeTypeRelation CHANGE Location_Id Collection_Id BIGINT NOT NULL DEFAULT '0'; + ALTER TABLE LocationMimeTypeRelation RENAME TO CollectionMimeTypeRelation; + DROP TABLE CollectionPimItemRelation; + ALTER TABLE LocationPimItemRelation CHANGE Location_Id Collection_Id BIGINT NOT NULL DEFAULT '0'; + ALTER TABLE LocationPimItemRelation RENAME TO CollectionPimItemRelation; + + + + ALTER TABLE PartTable CHANGE datasize datasize BIGINT; + + + + UPDATE CollectionTable SET parentId = NULL WHERE parentId = 0; + ALTER TABLE CollectionTable CHANGE parentId parentId BIGINT DEFAULT NULL; + + + + UPDATE ResourceTable SET isVirtual = true WHERE name = 'akonadi_nepomuktag_resource'; + UPDATE ResourceTable SET isVirtual = true WHERE name = 'akonadi_search_resource'; + + + + UPDATE CollectionTable SET queryString = remoteId WHERE resourceId = 1 AND parentId IS NOT NULL; + UPDATE CollectionTable SET queryLanguage = 'SPARQL' WHERE resourceId = 1 AND parentId IS NOT NULL; + + + + ALTER TABLE CollectionAttributeTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionMimeTypeRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionPimItemRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE FlagTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE MimeTypeTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PartTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PimItemFlagRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PimitemTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE ResourceTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE SchemaVersionTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + + + + + ALTER TABLE ResourceTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE CollectionTable CHANGE remoteId remoteId VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE remoteRevision remoteRevision VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE name name VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE cachePolicyLocalParts cachePolicyLocalParts VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE queryString queryString VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE queryLanguage queryLanguage VARCHAR(255) BINARY; + ALTER TABLE MimeTypeTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE PimItemTable CHANGE remoteId remoteId VARCHAR(255) BINARY; + ALTER TABLE PimItemTable CHANGE remoteRevision remoteRevision VARCHAR(255) BINARY; + ALTER TABLE FlagTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE PartTable CHANGE name name VARCHAR(255) BINARY; + + + + + ALTER TABLE ResourceTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE CollectionTable CHANGE remoteId remoteId VARBINARY(255); + ALTER TABLE CollectionTable CHANGE remoteRevision remoteRevision VARBINARY(255); + ALTER TABLE CollectionTable CHANGE name name VARBINARY(255); + ALTER TABLE CollectionTable CHANGE cachePolicyLocalParts cachePolicyLocalParts VARBINARY(255); + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(255); + ALTER TABLE CollectionTable CHANGE queryLanguage queryLanguage VARBINARY(255); + ALTER TABLE MimeTypeTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE PimItemTable CHANGE remoteId remoteId VARBINARY(255); + ALTER TABLE PimItemTable CHANGE remoteRevision remoteRevision VARBINARY(255); + ALTER TABLE FlagTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE PartTable CHANGE name name VARBINARY(255); + + + UPDATE PimItemFlagRelation SET Flag_id=(SELECT id FROM FlagTable WHERE name='\\SEEN') WHERE Flag_id=(SELECT id FROM FlagTable WHERE name='\\Seen'); + DELETE FROM FlagTable WHERE name='\\Seen'; + + + + + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(1024); + + + + + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(32768); + + + + + ALTER TABLE PimItemFlagRelation CHANGE PimItem_id PimItem_id BIGINT NOT NULL + ALTER TABLE PimItemFlagRelation CHANGE Flag_id Flag_id BIGINT NOT NULL + ALTER TABLE CollectionMimeTypeRelation CHANGE Collection_id Collection_id BIGINT NOT NULL + ALTER TABLE CollectionMimeTypeRelation CHANGE MimeType_id MimeType_id BIGINT NOT NULL + ALTER TABLE CollectionPimItemRelation CHANGE Collection_id Collection_id BIGINT NOT NULL + ALTER TABLE CollectionPimItemRelation CHANGE PimItem_id PimItem_id BIGINT NOT NULL + + diff --git a/autotests/server/dbtest_data/unittest_schema.xml b/autotests/server/dbtest_data/unittest_schema.xml new file mode 100644 index 0000000..cfb7cc5 --- /dev/null +++ b/autotests/server/dbtest_data/unittest_schema.xml @@ -0,0 +1,177 @@ + + + + + + + + + Contains the schema version of the database. + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + This meta data is stored inside akonadi to provide fast access. + + + + + + + +
+ + + + + + + + + + create/modified time + + + read access time + + + Indicates that this item has unsaved changes. + + + + +
+ + + This meta data is stored inside akonadi to provide fast access. + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + +
+ + + + + Specifies allowed MimeType for a Collection + + + + Used to associate items with search folders. + +
diff --git a/autotests/server/dbtypetest.cpp b/autotests/server/dbtypetest.cpp new file mode 100644 index 0000000..8ecb965 --- /dev/null +++ b/autotests/server/dbtypetest.cpp @@ -0,0 +1,63 @@ +/* + Copyright (c) 2012 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 + +#include +#include + +#define QL1S(x) QLatin1String(x) + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(DbType::Type) + +class DbTypeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDriverName_data() + { + QTest::addColumn("driverName"); + QTest::addColumn("dbType"); + + QTest::newRow("mysql") << "QMYSQL" << DbType::MySQL; + QTest::newRow("sqlite") << "QSQLITE" << DbType::Sqlite; + QTest::newRow("sqlite3") << "QSQLITE3" << DbType::Sqlite; + QTest::newRow("psql") << "QPSQL" << DbType::PostgreSQL; + } + + void testDriverName() + { + QFETCH(QString, driverName); + QFETCH(DbType::Type, dbType); + + QCOMPARE(DbType::typeForDriverName(driverName), dbType); + + if (QSqlDatabase::drivers().contains(driverName)) { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName, driverName); + QCOMPARE(DbType::type(db), dbType); + } + } +}; + +AKTEST_MAIN(DbTypeTest) + +#include "dbtypetest.moc" diff --git a/autotests/server/dbupdatertest.cpp b/autotests/server/dbupdatertest.cpp new file mode 100644 index 0000000..7a880c9 --- /dev/null +++ b/autotests/server/dbupdatertest.cpp @@ -0,0 +1,132 @@ +/* + Copyright (c) 2010 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. +*/ + +#include "dbupdatertest.h" + +#include +#include +#include +#include + +#include "storage/dbupdater.h" + +using namespace Akonadi::Server; + +void DbUpdaterTest::initTestCase() +{ + Q_INIT_RESOURCE(akonadidb); +} + +void DbUpdaterTest::testMysqlUpdateStatements() +{ + const QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QMYSQL")); + DbUpdater updater(db, QStringLiteral(":unittest_dbupdate.xml")); + + UpdateSet::Map updateSets; + QVERIFY(updater.parseUpdateSets(1, updateSets)); + QVERIFY(updateSets.contains(2)); + QVERIFY(updateSets.contains(3)); + QVERIFY(updateSets.contains(4)); + QVERIFY(updateSets.contains(8)); + QVERIFY(updateSets.contains(10)); + QVERIFY(updateSets.contains(12)); + QVERIFY(updateSets.contains(13)); + QVERIFY(updateSets.contains(14)); + QVERIFY(updateSets.contains(15)); + QVERIFY(updateSets.contains(16)); + QVERIFY(updateSets.contains(17)); + QVERIFY(updateSets.contains(18)); + QVERIFY(updateSets.contains(19)); + QVERIFY(updateSets.contains(22)); + QCOMPARE(updateSets.count(), 16); + + updateSets.clear(); + QVERIFY(updater.parseUpdateSets(13, updateSets)); + QVERIFY(!updateSets.contains(2)); + QVERIFY(!updateSets.contains(3)); + QVERIFY(!updateSets.contains(4)); + QVERIFY(!updateSets.contains(8)); + QVERIFY(!updateSets.contains(10)); + QVERIFY(!updateSets.contains(12)); + QVERIFY(!updateSets.contains(13)); + QVERIFY(updateSets.contains(14)); + QVERIFY(updateSets.contains(15)); + QVERIFY(updateSets.contains(16)); + QVERIFY(updateSets.contains(17)); + QVERIFY(updateSets.contains(18)); + QVERIFY(updateSets.contains(19)); + QVERIFY(updateSets.contains(22)); + QCOMPARE(updateSets.count(), 9); + + QCOMPARE(updateSets.value(14).statements.count(), 2); + QCOMPARE(updateSets.value(16).statements.count(), 11); + QCOMPARE(updateSets.value(22).statements.count(), 6); +} + +void DbUpdaterTest::testPsqlUpdateStatements() +{ + const QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL")); + DbUpdater updater(db, QStringLiteral(":unittest_dbupdate.xml")); + + UpdateSet::Map updateSets; + QVERIFY(updater.parseUpdateSets(1, updateSets)); + QVERIFY(updateSets.contains(2)); + QVERIFY(updateSets.contains(3)); + QVERIFY(updateSets.contains(4)); + QVERIFY(updateSets.contains(8)); + QVERIFY(updateSets.contains(10)); + QVERIFY(updateSets.contains(12)); + QVERIFY(updateSets.contains(13)); + QVERIFY(updateSets.contains(14)); + QVERIFY(updateSets.contains(15)); + QVERIFY(updateSets.contains(16)); + QVERIFY(updateSets.contains(17)); + QVERIFY(updateSets.contains(18)); + QVERIFY(updateSets.contains(19)); + QCOMPARE(updateSets.count(), 16); + + updateSets.clear(); + QVERIFY(updater.parseUpdateSets(13, updateSets)); + QVERIFY(!updateSets.contains(2)); + QVERIFY(!updateSets.contains(3)); + QVERIFY(!updateSets.contains(4)); + QVERIFY(!updateSets.contains(8)); + QVERIFY(!updateSets.contains(10)); + QVERIFY(!updateSets.contains(12)); + QVERIFY(!updateSets.contains(13)); + QVERIFY(updateSets.contains(14)); + QVERIFY(updateSets.contains(15)); + QVERIFY(updateSets.contains(16)); + QVERIFY(updateSets.contains(17)); + QVERIFY(updateSets.contains(18)); + QVERIFY(updateSets.contains(19)); + QCOMPARE(updateSets.count(), 9); + + QCOMPARE(updateSets.value(14).statements.count(), 2); + QCOMPARE(updateSets.value(16).statements.count(), 11); + QCOMPARE(updateSets.value(17).statements.count(), 0); + QCOMPARE(updateSets.value(22).statements.count(), 0); +} + +void DbUpdaterTest::cleanupTestCase() +{ + Q_CLEANUP_RESOURCE(akonadidb); +} + +QTEST_MAIN(DbUpdaterTest) diff --git a/autotests/server/dbupdatertest.h b/autotests/server/dbupdatertest.h new file mode 100644 index 0000000..3da1b2c --- /dev/null +++ b/autotests/server/dbupdatertest.h @@ -0,0 +1,37 @@ +/* + Copyright (c) 2010 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 DBUPDATERTEST_H +#define DBUPDATERTEST_H + +#include +#include + +class DbUpdaterTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testMysqlUpdateStatements(); + void testPsqlUpdateStatements(); + void cleanupTestCase(); +}; + +#endif diff --git a/autotests/server/fakeakonadiserver.cpp b/autotests/server/fakeakonadiserver.cpp new file mode 100644 index 0000000..b3f5375 --- /dev/null +++ b/autotests/server/fakeakonadiserver.cpp @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "fakeakonadiserver.h" +#include "fakeconnection.h" +#include "fakedatastore.h" +#include "fakesearchmanager.h" +#include "fakeclient.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "storage/dbconfig.h" +#include "storage/datastore.h" +#include "preprocessormanager.h" +#include "search/searchmanager.h" +#include "utils.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::NotificationCollector*) + +TestScenario TestScenario::create(qint64 tag, TestScenario::Action action, + const Protocol::Command &response) +{ + TestScenario sc; + sc.action = action; + + QBuffer buffer(&sc.data); + buffer.open(QIODevice::ReadWrite); + { + QDataStream stream(&buffer); + stream << tag; + Protocol::serialize(&buffer, response); + } + + { + buffer.seek(0); + QDataStream os(&buffer); + qint64 cmpTag; + os >> cmpTag; + Q_ASSERT(cmpTag == tag); + Protocol::Command cmpResp = Protocol::deserialize(os.device()); + + bool ok = false; + [cmpTag, tag, cmpResp, response, &ok]() { + QCOMPARE(cmpTag, tag); + QCOMPARE(cmpResp.type(), response.type()); + QCOMPARE(cmpResp.isResponse(), response.isResponse()); + QCOMPARE(cmpResp.debugString(), response.debugString()); + QCOMPARE(cmpResp, response); + ok = true; + }(); + if (!ok) { + sc.data.clear(); + return sc; + } + } + return sc; +} + + + +FakeAkonadiServer *FakeAkonadiServer::instance() +{ + if (!AkonadiServer::s_instance) { + AkonadiServer::s_instance = new FakeAkonadiServer; + } + + Q_ASSERT(qobject_cast(AkonadiServer::s_instance)); + return qobject_cast(AkonadiServer::s_instance); +} + +FakeAkonadiServer::FakeAkonadiServer() + : AkonadiServer() + , mDataStore(0) + , mServerLoop(0) + , mNotificationSpy(0) + , mPopulateDb(true) +{ + qputenv("AKONADI_INSTANCE", qPrintable(instanceName())); + qputenv("XDG_DATA_HOME", qPrintable(QString(basePath() + QLatin1String("/local")))); + qputenv("XDG_CONFIG_HOME", qPrintable(QString(basePath() + QLatin1String("/config")))); + qputenv("HOME", qPrintable(basePath())); + qputenv("KDEHOME", qPrintable(basePath() + QLatin1String("/kdehome"))); + + mClient = new FakeClient; +} + +FakeAkonadiServer::~FakeAkonadiServer() +{ + delete mClient; + delete mConnection; + delete mNotificationSpy; +} + +QString FakeAkonadiServer::basePath() +{ + return QString::fromLatin1("/tmp/akonadiserver-test-%1").arg(QCoreApplication::instance()->applicationPid()); +} + +QString FakeAkonadiServer::socketFile() +{ + return basePath() % QLatin1String("/local/share/akonadi/akonadiserver.socket"); +} + +QString FakeAkonadiServer::instanceName() +{ + return QString::fromLatin1("akonadiserver-test-%1").arg(QCoreApplication::instance()->applicationPid()); +} + +TestScenario::List FakeAkonadiServer::loginScenario(const QByteArray &sessionId) +{ + return { + TestScenario::create(0, TestScenario::ServerCmd, + Protocol::HelloResponse(QStringLiteral("Akonadi"), + QStringLiteral("Not Really IMAP server"), + Protocol::version())), + TestScenario::create(1,TestScenario::ClientCmd, + Protocol::LoginCommand(sessionId.isEmpty() ? instanceName().toLatin1() : sessionId)), + TestScenario::create(1, TestScenario::ServerCmd, + Protocol::LoginResponse()) + }; +} + +TestScenario::List FakeAkonadiServer::selectResourceScenario(const QString &name) +{ + const Resource resource = Resource::retrieveByName(name); + return { + TestScenario::create(3, TestScenario::ClientCmd, + Protocol::SelectResourceCommand(resource.name())), + TestScenario::create(3, TestScenario::ServerCmd, + Protocol::SelectResourceResponse()) + }; +} + +bool FakeAkonadiServer::init() +{ + qDebug() << "==== Fake Akonadi Server starting up ===="; + + qputenv("XDG_DATA_HOME", qPrintable(QString(basePath() + QLatin1String("/local")))); + qputenv("XDG_CONFIG_HOME", qPrintable(QString(basePath() + QLatin1String("/config")))); + qputenv("AKONADI_INSTANCE", qPrintable(instanceName())); + QSettings settings(StandardDirs::serverConfigFile(XdgBaseDirs::WriteOnly), QSettings::IniFormat); + settings.beginGroup(QLatin1String("General")); + settings.setValue(QLatin1String("Driver"), QLatin1String("QSQLITE3")); + settings.endGroup(); + + settings.beginGroup(QLatin1String("QSQLITE3")); + settings.setValue(QLatin1String("Name"), QString(basePath() + QLatin1String("/local/share/akonadi/akonadi.db"))); + settings.endGroup(); + settings.sync(); + + DbConfig *dbConfig = DbConfig::configuredDatabase(); + if (dbConfig->driverName() != QLatin1String("QSQLITE3")) { + throw FakeAkonadiServerException(QLatin1String("Unexpected driver specified. Expected QSQLITE3, got ") + dbConfig->driverName()); + } + + const QLatin1String initCon("initConnection"); + { + QSqlDatabase db = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), initCon); + DbConfig::configuredDatabase()->apply(db); + db.setDatabaseName(DbConfig::configuredDatabase()->databaseName()); + if (!db.isDriverAvailable(DbConfig::configuredDatabase()->driverName())) { + throw FakeAkonadiServerException(QString::fromLatin1("SQL driver %s not available").arg(db.driverName())); + } + if (!db.isValid()) { + throw FakeAkonadiServerException("Got invalid database"); + } + if (db.open()) { + qWarning() << "Database" << dbConfig->configuredDatabase()->databaseName() << "already exists, the test is not running in a clean environment!"; + } + db.close(); + } + + QSqlDatabase::removeDatabase(initCon); + + dbConfig->setup(); + + mDataStore = static_cast(FakeDataStore::self()); + mDataStore->setPopulateDb(mPopulateDb); + if (!mDataStore->init()) { + throw FakeAkonadiServerException("Failed to initialize datastore"); + } + + PreprocessorManager::init(); + PreprocessorManager::instance()->setEnabled(false); + mSearchManager = new FakeSearchManager(); + + const QString socketFile = basePath() + QLatin1String("/local/share/akonadi/akonadiserver.socket"); + + qDebug() << "==== Fake Akonadi Server started ===="; + return true; +} + +bool FakeAkonadiServer::deleteDirectory(const QString &path) +{ + // TODO: Qt 5: Use QDir::removeRecursively + + Q_ASSERT(path.startsWith(basePath())); + QDir dir(path); + dir.setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot | QDir::Hidden); + + const QFileInfoList list = dir.entryInfoList(); + for (int i = 0; i < list.size(); ++i) { + if (list.at(i).isDir() && !list.at(i).isSymLink()) { + deleteDirectory(list.at(i).absoluteFilePath()); + const QDir tmpDir(list.at(i).absoluteDir()); + tmpDir.rmdir(list.at(i).fileName()); + } else { + QFile::remove(list.at(i).absoluteFilePath()); + } + } + dir.cdUp(); + dir.rmdir(path); + return true; +} + +bool FakeAkonadiServer::quit() +{ + qDebug() << "==== Fake Akonadi Server shutting down ===="; + + // Stop listening for connections + close(); + + const QCommandLineParser &args = AkApplicationBase::instance()->commandLineArguments(); + if (!args.isSet(QLatin1String("no-cleanup"))) { + deleteDirectory(basePath()); + qDebug() << "Cleaned up" << basePath(); + } else { + qDebug() << "Skipping clean up of" << basePath(); + } + + PreprocessorManager::done(); + SearchManager::instance(); + + if (mDataStore) { + mDataStore->close(); + } + + qDebug() << "==== Fake Akonadi Server shut down ===="; + return true; +} + +void FakeAkonadiServer::setScenarios(const TestScenario::List &scenarios) +{ + mClient->setScenarios(scenarios); +} + +void FakeAkonadiServer::incomingConnection(quintptr socketDescriptor) +{ + mConnection = new FakeConnection(socketDescriptor); + NotificationCollector *ntfCollector = Q_NULLPTR; + // Connection is it's own thread, so we have to make sure we get collector + // from DataStore of the Connection's thread, not ours + QMetaObject::invokeMethod(mConnection, "notificationCollector", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(Akonadi::Server::NotificationCollector*, ntfCollector)); + Q_ASSERT(ntfCollector); + mNotificationSpy = new QSignalSpy(ntfCollector, + SIGNAL(notify(Akonadi::Protocol::ChangeNotification::List))); + Q_ASSERT(mNotificationSpy->isValid()); +} + +void FakeAkonadiServer::runTest() +{ + QVERIFY(listen(socketFile())); + + mServerLoop = new QEventLoop(this); + connect(mClient, &QThread::finished, mServerLoop, &QEventLoop::quit); + + // Start the client: the client will connect to the server and will + // start playing the scenario + mClient->start(); + + // Wait until the client disconnects, i.e. until the scenario is completed. + mServerLoop->exec(); + + mServerLoop->deleteLater(); + mServerLoop = 0; + + close(); +} + +QSignalSpy *FakeAkonadiServer::notificationSpy() const +{ + return mNotificationSpy; +} + +void FakeAkonadiServer::setPopulateDb(bool populate) +{ + mPopulateDb = populate; +} diff --git a/autotests/server/fakeakonadiserver.h b/autotests/server/fakeakonadiserver.h new file mode 100644 index 0000000..7aa089d --- /dev/null +++ b/autotests/server/fakeakonadiserver.h @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ +#ifndef FAKEAKONADISERVER_H +#define FAKEAKONADISERVER_H + +#include "akonadi.h" +#include "exception.h" + +#include +#include +#include +#include + +#include + +#include + +class QLocalServer; +class QEventLoop; + +namespace Akonadi { +namespace Server { + +class FakeSearchManager; +class FakeDataStore; +class FakeConnection; +class FakeClient; + +class TestScenario { +public: + typedef QList List; + + enum Action { + ServerCmd, + ClientCmd, + Wait, + Quit, + Ignore + }; + + Action action; + QByteArray data; + + static TestScenario create(qint64 tag, Action action, const Protocol::Command &response); + + static TestScenario wait(int timeout) + { + return TestScenario { Wait, QByteArray::number(timeout) }; + } + + static TestScenario quit() + { + return TestScenario { Quit, QByteArray() }; + } + + static TestScenario ignore(int count) + { + return TestScenario { Ignore, QByteArray::number(count) }; + } +}; + +/** + * A fake server used for testing. Loosely based on KIMAP::FakeServer + */ +class FakeAkonadiServer : public AkonadiServer +{ + Q_OBJECT + +public: + static FakeAkonadiServer *instance(); + + ~FakeAkonadiServer(); + + /* Reimpl */ + bool init(); + + /* Reimpl */ + bool quit(); + + static QString basePath(); + static QString socketFile(); + static QString instanceName(); + + static TestScenario::List loginScenario(const QByteArray &sessionId = QByteArray()); + static TestScenario::List selectCollectionScenario(const QString &name); + static TestScenario::List selectResourceScenario(const QString &name); + + void setScenarios(const TestScenario::List &scenarios); + + void runTest(); + + QSignalSpy *notificationSpy() const; + + void setPopulateDb(bool populate); + +protected: + /* Reimpl */ + void incomingConnection(quintptr socketDescriptor); + +private: + explicit FakeAkonadiServer(); + + bool deleteDirectory(const QString &path); + + FakeDataStore *mDataStore; + FakeSearchManager *mSearchManager; + FakeConnection *mConnection; + FakeClient *mClient; + + QEventLoop *mServerLoop; + + QSignalSpy *mNotificationSpy; + + bool mPopulateDb; + + static FakeAkonadiServer *sInstance; +}; + +AKONADI_EXCEPTION_MAKE_INSTANCE(FakeAkonadiServerException); + +} +} + +Q_DECLARE_METATYPE(Akonadi::Server::TestScenario) +Q_DECLARE_METATYPE(Akonadi::Server::TestScenario::List) + +#endif // FAKEAKONADISERVER_H diff --git a/autotests/server/fakeclient.cpp b/autotests/server/fakeclient.cpp new file mode 100644 index 0000000..84741c6 --- /dev/null +++ b/autotests/server/fakeclient.cpp @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "fakeclient.h" +#include "fakeakonadiserver.h" + +#include +#include + +#include +#include +#include + +#define CLIENT_COMPARE(actual, expected)\ +do {\ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) {\ + quit();\ + return;\ + }\ +} while (0) + +#define CLIENT_VERIFY(statement)\ +do {\ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) {\ + quit();\ + return;\ + }\ +} while (0) + +using namespace Akonadi; +using namespace Akonadi::Server; + +FakeClient::FakeClient(QObject *parent) + : QThread(parent) +{ + moveToThread(this); +} + +FakeClient::~FakeClient() +{ +} + +void FakeClient::setScenarios(const TestScenario::List &scenarios) +{ + mScenarios = scenarios; +} + +bool FakeClient::isScenarioDone() const +{ + QMutexLocker locker(&mMutex); + return mScenarios.isEmpty(); +} + +void FakeClient::dataAvailable() +{ + QMutexLocker locker(&mMutex); + + CLIENT_VERIFY(!mScenarios.isEmpty()); + + readServerPart(); + writeClientPart(); +} + +void FakeClient::readServerPart() +{ + while (!mScenarios.isEmpty() && (mScenarios.at(0).action == TestScenario::ServerCmd + || mScenarios.at(0).action == TestScenario::Ignore)) { + TestScenario scenario = mScenarios.takeFirst(); + if (scenario.action == TestScenario::Ignore) { + const int count = scenario.data.toInt(); + + // Read and throw away all "count" responses. Useful for scenarios + // with thousands of responses + qint64 tag; + for (int i = 0; i < count; ++i) { + mStream >> tag; + Protocol::deserialize(mStream.device()); + } + } else { + QDataStream expectedStream(scenario.data); + qint64 expectedTag, actualTag; + Protocol::Command expectedCommand, actualCommand; + + expectedStream >> expectedTag; + + while ((size_t)mSocket->bytesAvailable() < sizeof(qint64)) { + mSocket->waitForReadyRead(); + } + mStream >> actualTag; + CLIENT_COMPARE(actualTag, expectedTag); + + expectedCommand = Protocol::deserialize(expectedStream.device()); + actualCommand = Protocol::deserialize(mStream.device()); + + if (actualCommand.type() != expectedCommand.type()) { + qDebug() << "Actual command: " << actualCommand.debugString(); + qDebug() << "Expected Command:" << expectedCommand.debugString(); + } + CLIENT_COMPARE(actualCommand.type(), expectedCommand.type()); + CLIENT_COMPARE(actualCommand.isResponse(), expectedCommand.isResponse()); + if (actualCommand != expectedCommand) { + qDebug() << "Actual command: " << actualCommand.debugString(); + qDebug() << "Expected Command:" << expectedCommand.debugString(); + } + + CLIENT_COMPARE(actualCommand, expectedCommand); + } + } + + if (!mScenarios.isEmpty()) { + CLIENT_VERIFY(mScenarios.at(0).action == TestScenario::ClientCmd + || mScenarios.at(0).action == TestScenario::Wait + || mScenarios.at(0).action ==TestScenario::Quit); + } else { + // Server replied and there's nothing else to send, then quit + quit(); + } +} + +void FakeClient::writeClientPart() +{ + while (!mScenarios.isEmpty() && (mScenarios.at(0).action == TestScenario::ClientCmd + || mScenarios.at(0).action == TestScenario::Wait)) { + const TestScenario rule = mScenarios.takeFirst(); + + if (rule.action == TestScenario::ClientCmd) { + mSocket->write(rule.data); + } else { + const int timeout = rule.data.toInt(); + QTest::qWait(timeout); + } + } + + if (!mScenarios.isEmpty() && mScenarios.at(0).action == TestScenario::Quit) { + mSocket->close(); + } + + if (!mScenarios.isEmpty()) { + CLIENT_VERIFY(mScenarios.at(0).action == TestScenario::ServerCmd + || mScenarios.at(0).action == TestScenario::Ignore); + } +} + +void FakeClient::run() +{ + mSocket = new QLocalSocket(); + mSocket->connectToServer(FakeAkonadiServer::socketFile()); + connect(mSocket, &QIODevice::readyRead, this, &FakeClient::dataAvailable); + connect(mSocket, &QLocalSocket::disconnected, this, &FakeClient::connectionLost); + if (!mSocket->waitForConnected()) { + akFatal() << "Failed to connect to FakeAkonadiServer"; + } + mStream.setDevice(mSocket); + + exec(); + + mStream.setDevice(Q_NULLPTR); + mSocket->close(); + delete mSocket; + mSocket = 0; +} + +void FakeClient::connectionLost() +{ + // Otherwise this is an error on server-side, we expected more talking + CLIENT_VERIFY(isScenarioDone()); + + quit(); +} diff --git a/autotests/server/fakeclient.h b/autotests/server/fakeclient.h new file mode 100644 index 0000000..7fcc6ff --- /dev/null +++ b/autotests/server/fakeclient.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef AKONADI_SERVER_FAKECLIENT_H +#define AKONADI_SERVER_FAKECLIENT_H + +#include +#include +#include + +#include "fakeakonadiserver.h" + +class QLocalSocket; + +namespace Akonadi { +namespace Server { + +class FakeClient : public QThread +{ + Q_OBJECT + +public: + + FakeClient(QObject *parent = 0); + ~FakeClient(); + + void setScenarios(const TestScenario::List &scenarios); + + bool isScenarioDone() const; + +protected: + void run(); + +private Q_SLOTS: + void dataAvailable(); + void readServerPart(); + void writeClientPart(); + void connectionLost(); + +private: + mutable QMutex mMutex; + + TestScenario::List mScenarios; + QLocalSocket *mSocket; + QDataStream mStream; +}; +} +} + + +#endif // AKONADI_SERVER_FAKECLIENT_H diff --git a/autotests/server/fakeconnection.cpp b/autotests/server/fakeconnection.cpp new file mode 100644 index 0000000..c0dde57 --- /dev/null +++ b/autotests/server/fakeconnection.cpp @@ -0,0 +1,56 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "fakeconnection.h" + +#include "fakedatastore.h" +#include "fakeakonadiserver.h" + +#include + +using namespace Akonadi::Server; + +FakeConnection::FakeConnection(quintptr socketDescriptor, QObject *parent) + : Connection(socketDescriptor, parent) +{ +} + +FakeConnection::FakeConnection(QObject *parent) + : Connection(parent) +{ +} + +FakeConnection::~FakeConnection() +{ + +} + +DataStore *FakeConnection::storageBackend() +{ + if (!m_backend) { + m_backend = static_cast(FakeDataStore::self()); + } + + return m_backend; +} + +NotificationCollector *FakeConnection::notificationCollector() +{ + return storageBackend()->notificationCollector(); +} diff --git a/autotests/server/fakeconnection.h b/autotests/server/fakeconnection.h new file mode 100644 index 0000000..4a39021 --- /dev/null +++ b/autotests/server/fakeconnection.h @@ -0,0 +1,49 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_FAKECONNECTION +#define AKONADI_FAKECONNECTION + +#include "connection.h" + +namespace Akonadi { +namespace Server { + +class NotificationCollector; + +class FakeConnection : public Connection +{ + Q_OBJECT + +public: + FakeConnection(quintptr socketDescriptor, QObject *parent = 0); + FakeConnection(QObject *parent = 0); + virtual ~FakeConnection(); + + DataStore *storageBackend(); + +public Q_SLOTS: + Akonadi::Server::NotificationCollector *notificationCollector(); + +}; + +} +} + +#endif diff --git a/autotests/server/fakedatastore.cpp b/autotests/server/fakedatastore.cpp new file mode 100644 index 0000000..4b16ca1 --- /dev/null +++ b/autotests/server/fakedatastore.cpp @@ -0,0 +1,322 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "fakedatastore.h" +#include "dbpopulator.h" +#include "storage/dbconfig.h" +#include "storage/notificationcollector.h" +#include "akonadischema.h" +#include "storage/dbinitializer.h" + +#include +#include +#include + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(PimItem) +Q_DECLARE_METATYPE(PimItem::List) +Q_DECLARE_METATYPE(Collection) +Q_DECLARE_METATYPE(Flag) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(Tag) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(MimeType) +Q_DECLARE_METATYPE(QList) + +Akonadi::Server::FakeDataStore::FakeDataStore() + : DataStore() + , mPopulateDb(true) +{ + notificationCollector(); +} + +FakeDataStore::~FakeDataStore() +{ +} + +NotificationCollector *FakeDataStore::notificationCollector() +{ + if (!mNotificationCollector) { + mNotificationCollector = new NotificationCollector(this); + } + return mNotificationCollector; +} + +DataStore *FakeDataStore::self() +{ + if (!sInstances.hasLocalData()) { + sInstances.setLocalData(new FakeDataStore()); + } + + return sInstances.localData(); +} + +bool FakeDataStore::init() +{ + if (!DataStore::init()) { + return false; + } + + if (mPopulateDb) { + DbPopulator dbPopulator; + if (!dbPopulator.run()) { + akError() << "Failed to populate database"; + return false; + } + } + + return true; +} + +bool FakeDataStore::setItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged, + const Collection &col, + bool silent) +{ + mChanges.insert(QStringLiteral("setItemsFlags"), + QVariantList() << QVariant::fromValue(items) + << QVariant::fromValue(flags) + << QVariant::fromValue(col) + << silent); + return DataStore::setItemsFlags(items, flags, flagsChanged, col, silent); +} + +bool FakeDataStore::appendItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged, + bool checkIfExists, + const Collection &col, + bool silent) +{ + mChanges.insert(QStringLiteral("appendItemsFlags"), + QVariantList() << QVariant::fromValue(items) + << QVariant::fromValue(flags) + << checkIfExists + << QVariant::fromValue(col) + << silent); + return DataStore::appendItemsFlags(items, flags, flagsChanged, checkIfExists, col, silent); +} + +bool FakeDataStore::removeItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged, + const Collection &col, + bool silent) +{ + mChanges.insert(QStringLiteral("removeItemsFlags"), + QVariantList() << QVariant::fromValue(items) + << QVariant::fromValue(flags) + << QVariant::fromValue(col) + << silent); + return DataStore::removeItemsFlags(items, flags, flagsChanged, col, silent); +} + +bool FakeDataStore::setItemsTags(const PimItem::List &items, + const Tag::List &tags, + bool *tagsChanged, + bool silent) +{ + mChanges.insert(QStringLiteral("setItemsTags"), + QVariantList() << QVariant::fromValue(items) + << QVariant::fromValue(tags) + << silent); + return DataStore::setItemsTags(items, tags, tagsChanged, silent); +} + +bool FakeDataStore::appendItemsTags(const PimItem::List &items, + const Tag::List &tags, + bool *tagsChanged, + bool checkIfExists, + const Collection &col, + bool silent) +{ + mChanges.insert(QStringLiteral("appendItemsTags"), + QVariantList() << QVariant::fromValue(items) + << QVariant::fromValue(tags) + << checkIfExists + << QVariant::fromValue(col) + << silent); + return DataStore::appendItemsTags(items, tags, tagsChanged, + checkIfExists, col, silent); +} + +bool FakeDataStore::removeItemsTags(const PimItem::List &items, + const Tag::List &tags, + bool *tagsChanged, + bool silent) +{ + mChanges.insert(QStringLiteral("removeItemsTags"), + QVariantList() << QVariant::fromValue(items) + << QVariant::fromValue(tags) + << silent); + return DataStore::removeItemsTags(items, tags, tagsChanged, silent); +} + +bool FakeDataStore::removeItemParts(const PimItem &item, + const QSet &parts) +{ + mChanges.insert(QStringLiteral("remoteItemParts"), + QVariantList() << QVariant::fromValue(item) + << QVariant::fromValue(parts)); + return DataStore::removeItemParts(item, parts); +} + +bool FakeDataStore::invalidateItemCache(const PimItem &item) +{ + mChanges.insert(QStringLiteral("invalidateItemCache"), + QVariantList() << QVariant::fromValue(item)); + return DataStore::invalidateItemCache(item); +} + +bool FakeDataStore::appendCollection(Collection &collection) +{ + mChanges.insert(QStringLiteral("appendCollection"), + QVariantList() << QVariant::fromValue(collection)); + return DataStore::appendCollection(collection); +} + +bool FakeDataStore::cleanupCollection(Collection &collection) +{ + mChanges.insert(QStringLiteral("cleanupCollection"), + QVariantList() << QVariant::fromValue(collection)); + return DataStore::cleanupCollection(collection); +} + +bool FakeDataStore::cleanupCollection_slow(Collection &collection) +{ + mChanges.insert(QStringLiteral("cleanupCollection_slow"), + QVariantList() << QVariant::fromValue(collection)); + return DataStore::cleanupCollection_slow(collection); +} + +bool FakeDataStore::moveCollection(Collection &collection, + const Collection &newParent) +{ + mChanges.insert(QStringLiteral("moveCollection"), + QVariantList() << QVariant::fromValue(collection) + << QVariant::fromValue(newParent)); + return DataStore::moveCollection(collection, newParent); +} + +bool FakeDataStore::appendMimeTypeForCollection(qint64 collectionId, + const QStringList &mimeTypes) +{ + mChanges.insert(QStringLiteral("appendMimeTypeForCollection"), + QVariantList() << collectionId + << QVariant::fromValue(mimeTypes)); + return DataStore::appendMimeTypeForCollection(collectionId, mimeTypes); +} + +void FakeDataStore::activeCachePolicy(Collection &col) +{ + mChanges.insert(QStringLiteral("activeCachePolicy"), + QVariantList() << QVariant::fromValue(col)); + return DataStore::activeCachePolicy(col); +} + +bool FakeDataStore::appendMimeType(const QString &mimetype, + qint64 *insertId) +{ + mChanges.insert(QStringLiteral("appendMimeType"), + QVariantList() << mimetype); + return DataStore::appendMimeType(mimetype, insertId); +} + +bool FakeDataStore::appendPimItem(QVector &parts, + const MimeType &mimetype, + const Collection &collection, + const QDateTime &dateTime, + const QString &remote_id, + const QString &remoteRevision, + const QString &gid, + PimItem &pimItem) +{ + mChanges.insert(QStringLiteral("appendPimItem"), + QVariantList() << QVariant::fromValue(mimetype) + << QVariant::fromValue(collection) + << dateTime + << remote_id + << remoteRevision + << gid); + return DataStore::appendPimItem(parts, mimetype, collection, dateTime, + remote_id, remoteRevision, gid, pimItem); +} + +bool FakeDataStore::cleanupPimItems(const PimItem::List &items) +{ + mChanges.insert(QStringLiteral("cleanupPimItems"), + QVariantList() << QVariant::fromValue(items)); + return DataStore::cleanupPimItems(items); +} + +bool FakeDataStore::unhidePimItem(PimItem &pimItem) +{ + mChanges.insert(QStringLiteral("unhidePimItem"), + QVariantList() << QVariant::fromValue(pimItem)); + return DataStore::unhidePimItem(pimItem); +} + +bool FakeDataStore::unhideAllPimItems() +{ + mChanges.insert(QStringLiteral("unhideAllPimItems"), QVariantList()); + return DataStore::unhideAllPimItems(); +} + +bool FakeDataStore::addCollectionAttribute(const Collection &col, + const QByteArray &key, + const QByteArray &value) +{ + mChanges.insert(QStringLiteral("addCollectionAttribute"), + QVariantList() << QVariant::fromValue(col) + << key << value); + return DataStore::addCollectionAttribute(col, key, value); +} + +bool FakeDataStore::removeCollectionAttribute(const Collection &col, + const QByteArray &key) +{ + mChanges.insert(QStringLiteral("removeCollectionAttribute"), + QVariantList() << QVariant::fromValue(col) << key); + return DataStore::removeCollectionAttribute(col, key); +} + +bool FakeDataStore::beginTransaction() +{ + mChanges.insert(QStringLiteral("beginTransaction"), QVariantList()); + return DataStore::beginTransaction(); +} + +bool FakeDataStore::commitTransaction() +{ + mChanges.insert(QStringLiteral("commitTransaction"), QVariantList()); + return DataStore::commitTransaction(); +} + +bool FakeDataStore::rollbackTransaction() +{ + mChanges.insert(QStringLiteral("rollbackTransaction"), QVariantList()); + return DataStore::rollbackTransaction(); +} + +void FakeDataStore::setPopulateDb(bool populate) +{ + mPopulateDb = populate; +} diff --git a/autotests/server/fakedatastore.h b/autotests/server/fakedatastore.h new file mode 100644 index 0000000..4595769 --- /dev/null +++ b/autotests/server/fakedatastore.h @@ -0,0 +1,137 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_SERVER_FAKEDATASTORE_H +#define AKONADI_SERVER_FAKEDATASTORE_H + +#include "storage/datastore.h" + +namespace Akonadi { +namespace Server { + +class FakeDataStore : public DataStore +{ + Q_OBJECT + +public: + virtual ~FakeDataStore(); + static DataStore *self(); + + bool init() Q_DECL_OVERRIDE; + + QMap changes() const + { + return mChanges; + } + + virtual bool setItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged = 0, + const Collection &col = Collection(), + bool silent = false) Q_DECL_OVERRIDE; + virtual bool appendItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged = 0, + bool checkIfExists = true, + const Collection &col = Collection(), + bool silent = false) Q_DECL_OVERRIDE; + virtual bool removeItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged = 0, + const Collection &col = Collection(), + bool silent = false) Q_DECL_OVERRIDE; + + virtual bool setItemsTags(const PimItem::List &items, + const Tag::List &tags, + bool *tagsChanged = 0, + bool silent = false) Q_DECL_OVERRIDE; + virtual bool appendItemsTags(const PimItem::List &items, + const Tag::List &tags, + bool *tagsChanged = 0, + bool checkIfExists = true, + const Collection &col = Collection(), + bool silent = false) Q_DECL_OVERRIDE; + virtual bool removeItemsTags(const PimItem::List &items, + const Tag::List &tags, + bool *tagsChanged = 0, + bool silent = false) Q_DECL_OVERRIDE; + + virtual bool removeItemParts(const PimItem &item, + const QSet &parts) Q_DECL_OVERRIDE; + + virtual bool invalidateItemCache(const PimItem &item) Q_DECL_OVERRIDE; + + virtual bool appendCollection(Collection &collection) Q_DECL_OVERRIDE; + + virtual bool cleanupCollection(Collection &collection) Q_DECL_OVERRIDE; + virtual bool cleanupCollection_slow(Collection &collection) Q_DECL_OVERRIDE; + + virtual bool moveCollection(Collection &collection, + const Collection &newParent) Q_DECL_OVERRIDE; + + virtual bool appendMimeTypeForCollection(qint64 collectionId, + const QStringList &mimeTypes) Q_DECL_OVERRIDE; + + virtual void activeCachePolicy(Collection &col) Q_DECL_OVERRIDE; + + virtual bool appendMimeType(const QString &mimetype, + qint64 *insertId = 0) Q_DECL_OVERRIDE; + virtual bool appendPimItem(QVector &parts, + const MimeType &mimetype, + const Collection &collection, + const QDateTime &dateTime, + const QString &remote_id, + const QString &remoteRevision, + const QString &gid, + PimItem &pimItem) Q_DECL_OVERRIDE; + + virtual bool cleanupPimItems(const PimItem::List &items) Q_DECL_OVERRIDE; + + virtual bool unhidePimItem(PimItem &pimItem) Q_DECL_OVERRIDE; + virtual bool unhideAllPimItems() Q_DECL_OVERRIDE; + + virtual bool addCollectionAttribute(const Collection &col, + const QByteArray &key, + const QByteArray &value) Q_DECL_OVERRIDE; + virtual bool removeCollectionAttribute(const Collection &col, + const QByteArray &key) Q_DECL_OVERRIDE; + + virtual bool beginTransaction() Q_DECL_OVERRIDE; + virtual bool rollbackTransaction() Q_DECL_OVERRIDE; + virtual bool commitTransaction() Q_DECL_OVERRIDE; + + virtual NotificationCollector *notificationCollector() Q_DECL_OVERRIDE; + + void setPopulateDb(bool populate); + +protected: + FakeDataStore(); + + QMap mChanges; + +private: + bool populateDatabase(); + bool mPopulateDb; + +}; + +} +} + +#endif // AKONADI_SERVER_FAKEDATASTORE_H diff --git a/autotests/server/fakeentities.h b/autotests/server/fakeentities.h new file mode 100644 index 0000000..e394163 --- /dev/null +++ b/autotests/server/fakeentities.h @@ -0,0 +1,88 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 FAKEENTITIES_H +#define FAKEENTITIES_H + +#include "entities.h" + +namespace Akonadi { +namespace Server { + +class FakePart : public Part +{ +public: + FakePart() + : Part() + { + } + + void setPartType(const PartType &partType) + { + m_partType = partType; + Part::setPartType(partType); + } + + PartType partType() const + { + return m_partType; + } + +private: + PartType m_partType; +}; + +class FakeTag : public Tag +{ +public: + FakeTag() + : Tag() + { + } + + void setTagType(const TagType &tagType) + { + m_tagType = tagType; + Tag::setTagType(tagType); + } + + TagType tagType() const + { + return m_tagType; + } + + void setRemoteId(const QString &remoteId) + { + m_remoteId = remoteId; + } + + QString remoteId() const + { + return m_remoteId; + } + +private: + TagType m_tagType; + QString m_remoteId; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // FAKEENTITIES_H diff --git a/autotests/server/fakesearchmanager.cpp b/autotests/server/fakesearchmanager.cpp new file mode 100644 index 0000000..4276522 --- /dev/null +++ b/autotests/server/fakesearchmanager.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "fakesearchmanager.h" + +#include "entities.h" + +using namespace Akonadi::Server; + +FakeSearchManager::FakeSearchManager(QObject *parent) + : SearchManager(QStringList(), parent) +{ +} + +FakeSearchManager::~FakeSearchManager() +{ +} + +void FakeSearchManager::registerInstance(const QString &id) +{ + Q_UNUSED(id); +} + +void FakeSearchManager::unregisterInstance(const QString &id) +{ + Q_UNUSED(id); +} + +void FakeSearchManager::updateSearch(const Collection &collection) +{ + Q_UNUSED(collection); +} + +void FakeSearchManager::updateSearchAsync(const Collection &collection) +{ + Q_UNUSED(collection); +} + +QVector FakeSearchManager::searchPlugins() const +{ + return QVector(); +} + +void FakeSearchManager::scheduleSearchUpdate() +{ +} diff --git a/autotests/server/fakesearchmanager.h b/autotests/server/fakesearchmanager.h new file mode 100644 index 0000000..61f9a9f --- /dev/null +++ b/autotests/server/fakesearchmanager.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef AKONADI_SERVER_FAKESEARCHMANAGER_H +#define AKONADI_SERVER_FAKESEARCHMANAGER_H + +#include + +namespace Akonadi { +namespace Server { + +/** + * Subclass of SearchManager that does nothing. + */ +class FakeSearchManager : public SearchManager +{ + Q_OBJECT + +public: + explicit FakeSearchManager(QObject *parent = 0); + virtual ~FakeSearchManager(); + + void registerInstance(const QString &id); + void unregisterInstance(const QString &id); + void updateSearch(const Collection &collection); + void updateSearchAsync(const Collection &collection); + QVector searchPlugins() const; + + void scheduleSearchUpdate(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SERVER_FAKESEARCHMANAGER_H diff --git a/autotests/server/fetchhandlertest.cpp b/autotests/server/fetchhandlertest.cpp new file mode 100644 index 0000000..bbc93a2 --- /dev/null +++ b/autotests/server/fetchhandlertest.cpp @@ -0,0 +1,271 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 +#include + +#include "fakeakonadiserver.h" +#include "aktest.h" +#include "akdebug.h" +#include "entities.h" +#include "dbinitializer.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::Tag::List) +Q_DECLARE_METATYPE(Akonadi::Server::Tag) + + +class FetchHandlerTest : public QObject +{ + Q_OBJECT + +public: + FetchHandlerTest() + : QObject() + { + qRegisterMetaType(); + + try { + FakeAkonadiServer::instance()->setPopulateDb(false); + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + } + + ~FetchHandlerTest() + { + FakeAkonadiServer::instance()->quit(); + } + + Protocol::FetchItemsCommand createCommand(const Scope &scope, const Protocol::ScopeContext &ctx = Protocol::ScopeContext()) + { + Protocol::FetchItemsCommand cmd(scope, ctx); + cmd.fetchScope().setFetch(Protocol::FetchScope::IgnoreErrors); + return cmd;; + } + + Protocol::FetchItemsResponse createResponse(const PimItem &item) + { + Protocol::FetchItemsResponse resp(item.id()); + resp.setMimeType(item.mimeType().name()); + resp.setParentId(item.collectionId()); + + return resp; + } + + QScopedPointer initializer; + +private Q_SLOTS: + void testFetch_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col = initializer->createCollection("root"); + PimItem item1 = initializer->createItem("item1", col); + PimItem item2 = initializer->createItem("item2", col); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item1.id())) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponse()); + + QTest::newRow("basic fetch") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Collection, col.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item2)) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponse()); + QTest::newRow("collection context") << scenarios; + } + } + + void testFetch() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + + void testFetchByTag_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col = initializer->createCollection("root"); + PimItem item1 = initializer->createItem("item1", col); + PimItem item2 = initializer->createItem("item2", col); + + Tag tag; + TagType type; + type.setName(QStringLiteral("PLAIN")); + type.insert(); + tag.setTagType(type); + tag.setGid(QStringLiteral("gid")); + tag.insert(); + + item1.addTag(tag); + item1.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Tag, tag.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponse()); + + QTest::newRow("fetch by tag") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << FakeAkonadiServer::selectResourceScenario(QStringLiteral("testresource")) + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Tag, tag.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponse()); + + QTest::newRow("uid fetch by tag from resource") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << FakeAkonadiServer::selectResourceScenario(QStringLiteral("testresource")) + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Collection, col.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item2)) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponse()); + + QTest::newRow("fetch collection") << scenarios; + } + } + + void testFetchByTag() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + + void testFetchCommandContext_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + PimItem item1 = initializer->createItem("item1", col1); + Collection col2 = initializer->createCollection("col2"); + + Tag tag; + TagType type; + type.setName(QStringLiteral("PLAIN")); + type.insert(); + tag.setTagType(type); + tag.setGid(QStringLiteral("gid")); + tag.insert(); + + item1.addTag(tag); + item1.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << FakeAkonadiServer::selectResourceScenario(QStringLiteral("testresource")) + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Collection, col2.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponse()) + << TestScenario::create(6, TestScenario::ClientCmd, createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Tag, tag.id()))) + << TestScenario::create(6, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(6, TestScenario::ServerCmd, Protocol::FetchItemsResponse()); + + //Special case that used to be broken due to persistent command context + QTest::newRow("fetch by tag after collection") << scenarios; + } + } + + void testFetchCommandContext() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + + void testList_data() + { + QElapsedTimer timer; + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + + timer.start(); + QList items; + for (int i = 0; i < 1000; i++) { + items.append(initializer->createItem(QString::number(i).toLatin1().constData(),col1)); + } + qDebug() << timer.nsecsElapsed()/1.0e6 << "ms"; + timer.start(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Collection, col1.id()))); + while (!items.isEmpty()) { + const PimItem &item = items.takeLast(); + scenarios << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item)); + } + scenarios << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponse()); + QTest::newRow("complete list") << scenarios; + } + qDebug() << timer.nsecsElapsed()/1.0e6 << "ms"; + } + + void testList() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + //StorageDebugger::instance()->enableSQLDebugging(true); + //StorageDebugger::instance()->writeToFile(QStringLiteral("sqllog.txt")); + FakeAkonadiServer::instance()->runTest(); + } + +}; + +AKTEST_FAKESERVER_MAIN(FetchHandlerTest) + +#include "fetchhandlertest.moc" diff --git a/autotests/server/handlertest.cpp b/autotests/server/handlertest.cpp new file mode 100644 index 0000000..06a57a8 --- /dev/null +++ b/autotests/server/handlertest.cpp @@ -0,0 +1,181 @@ +/* + Copyright (c) 2011 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 + +#include +#include + +#include "handler.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +#define MAKE_CMD_ROW( command, class ) QTest::newRow(#command) << command << "Akonadi::Server::" #class; + +class HandlerTest : public QObject +{ + Q_OBJECT +private: + void setupTestData() + { + QTest::addColumn("command"); + QTest::addColumn("className"); + } + + void addAuthCommands() + { + MAKE_CMD_ROW(Protocol::Command::CreateCollection, Create) + MAKE_CMD_ROW(Protocol::Command::FetchCollections, List) + MAKE_CMD_ROW(Protocol::Command::StoreSearch, SearchPersistent) + MAKE_CMD_ROW(Protocol::Command::Search, Search) + MAKE_CMD_ROW(Protocol::Command::FetchItems, Fetch) + MAKE_CMD_ROW(Protocol::Command::ModifyItems, Store) + MAKE_CMD_ROW(Protocol::Command::FetchCollectionStats, Status) + MAKE_CMD_ROW(Protocol::Command::DeleteCollection, Delete) + MAKE_CMD_ROW(Protocol::Command::ModifyCollection, Modify) + MAKE_CMD_ROW(Protocol::Command::Transaction, TransactionHandler) + MAKE_CMD_ROW(Protocol::Command::CreateItem, AkAppend) + MAKE_CMD_ROW(Protocol::Command::CopyItems, Copy) + MAKE_CMD_ROW(Protocol::Command::CopyCollection, ColCopy) + MAKE_CMD_ROW(Protocol::Command::LinkItems, Link) + MAKE_CMD_ROW(Protocol::Command::SelectResource, ResourceSelect) + MAKE_CMD_ROW(Protocol::Command::DeleteItems, Remove) + MAKE_CMD_ROW(Protocol::Command::MoveItems, Move) + MAKE_CMD_ROW(Protocol::Command::MoveCollection, ColMove) + } + + void addNonAuthCommands() + { + MAKE_CMD_ROW(Protocol::Command::Login, Login) + } + + void addAlwaysCommands() + { + MAKE_CMD_ROW(Protocol::Command::Logout, Logout) + } + + void addInvalidCommands() + { + //MAKE_CMD_ROW(Protocol::Command::Invalid, UnknownCommandHandler) + } +private Q_SLOTS: + void testFindAuthenticatedCommand_data() + { + setupTestData(); + addAuthCommands(); + } + + void testFindAuthenticatedCommand() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QString, className); + QScopedPointer handler(Handler::findHandlerForCommandAuthenticated(command)); + QVERIFY(!handler.isNull()); + QCOMPARE(handler->metaObject()->className(), className.toLatin1().constData()); + } + + void testFindAuthenticatedCommandNegative_data() + { + setupTestData(); + addNonAuthCommands(); + addAlwaysCommands(); + addInvalidCommands(); + } + + void testFindAuthenticatedCommandNegative() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QString, className); + + QScopedPointer handler(Handler::findHandlerForCommandAuthenticated(command)); + QVERIFY(handler.isNull()); + } + + void testFindNonAutenticatedCommand_data() + { + setupTestData(); + addNonAuthCommands(); + } + + void testFindNonAutenticatedCommand() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QString, className); + + QScopedPointer handler(Handler::findHandlerForCommandNonAuthenticated(command)); + QVERIFY(!handler.isNull()); + QCOMPARE(handler->metaObject()->className(), className.toLatin1().constData()); + } + + void testFindNonAutenticatedCommandNegative_data() + { + setupTestData(); + addAuthCommands(); + addAlwaysCommands(); + addInvalidCommands(); + } + + void testFindNonAutenticatedCommandNegative() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QString, className); + + QScopedPointer handler(Handler::findHandlerForCommandNonAuthenticated(command)); + QVERIFY(handler.isNull()); + } + + void testFindAlwaysCommand_data() + { + setupTestData(); + addAlwaysCommands(); + } + + void testFindAlwaysCommand() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QString, className); + + QScopedPointer handler(Handler::findHandlerForCommandAlwaysAllowed(command)); + QVERIFY(!handler.isNull()); + QCOMPARE(handler->metaObject()->className(), className.toLatin1().constData()); + } + + void testFindAlwaysCommandNegative_data() + { + setupTestData(); + addAuthCommands(); + addNonAuthCommands(); + addInvalidCommands(); + } + + void testFindAlwaysCommandNegative() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QString, className); + + QScopedPointer handler(Handler::findHandlerForCommandAlwaysAllowed(command)); + QVERIFY(handler.isNull()); + } +}; + +AKTEST_MAIN(HandlerTest) + +#include "handlertest.moc" diff --git a/autotests/server/itemretrievertest.cpp b/autotests/server/itemretrievertest.cpp new file mode 100644 index 0000000..2a73e7a --- /dev/null +++ b/autotests/server/itemretrievertest.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2013 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 +#include + +#include "storage/itemretriever.h" + +using namespace Akonadi::Server; + +class ItemRetrieverTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testFullPayload() + { + ItemRetriever r1(0); + r1.setRetrieveFullPayload(true); + QCOMPARE(r1.retrieveParts().size(), 1); + QCOMPARE(r1.retrieveParts().at(0), { "PLD:RFC822" }); + r1.setRetrieveParts({ "PLD:FOO" }); + QCOMPARE(r1.retrieveParts().size(), 2); + } +}; + +QTEST_MAIN(ItemRetrieverTest) + +#include "itemretrievertest.moc" diff --git a/autotests/server/linkhandlertest.cpp b/autotests/server/linkhandlertest.cpp new file mode 100644 index 0000000..02d2110 --- /dev/null +++ b/autotests/server/linkhandlertest.cpp @@ -0,0 +1,281 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 + +#include "fakeakonadiserver.h" +#include +#include +#include "entities.h" + +#include +#include + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class LinkHandlerTest : public QObject +{ + Q_OBJECT + +public: + LinkHandlerTest() + { + try { + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + } + + ~LinkHandlerTest() + { + FakeAkonadiServer::instance()->quit(); + } + + Protocol::LinkItemsResponse createError(const QString &error) + { + Protocol::LinkItemsResponse resp; + resp.setError(1, error); + return resp; + } + +private Q_SLOTS: + void testLink_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn("notification"); + QTest::addColumn("expectFail"); + + TestScenario::List scenarios; + Protocol::ChangeNotification notification; + + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommand(Protocol::LinkItemsCommand::Link, ImapInterval(1, 3), 3)) + << TestScenario::create(5, TestScenario::ServerCmd, createError(QLatin1String("Can't link items to non-virtual collections"))); + QTest::newRow("non-virtual collection") << scenarios << Protocol::ChangeNotification() << true; + + notification.setType(Protocol::ChangeNotification::Items); + notification.setOperation(Protocol::ChangeNotification::Link); + notification.addEntity(1, QLatin1String("A"), QString(), QLatin1String("application/octet-stream")); + notification.addEntity(2, QLatin1String("B"), QString(), QLatin1String("application/octet-stream")); + notification.addEntity(3, QLatin1String("C"), QString(), QLatin1String("application/octet-stream")); + notification.setParentCollection(6); + notification.setResource("akonadi_fake_resource_with_virtual_collections_0"); + notification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommand(Protocol::LinkItemsCommand::Link, ImapInterval(1, 3), 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponse()); + QTest::newRow("normal") << scenarios << notification << false; + + notification.clearEntities(); + notification.addEntity(4, QLatin1String("D"), QString(), QLatin1String("application/octet-stream")); + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommand(Protocol::LinkItemsCommand::Link, QVector{ 4, 123456 }, 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponse()); + QTest::newRow("existent and non-existent item") << scenarios << notification << false; + + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommand(Protocol::LinkItemsCommand::Link, 4, 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponse()); + QTest::newRow("non-existent item only") << scenarios << Protocol::ChangeNotification() << false; + + //FIXME: All RID related operations are currently broken because we reset the collection context before every command, + //and LINK still relies on SELECT to set the collection context. + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectCollectionScenario(QLatin1String("Collection B")) + // << "C: 3 UID LINK 6 RID (\"F\" \"G\")\n" + // << "S: 3 OK LINK complete"; + // notification.clearEntities(); + // notification.clearEntities(); + // notification.addEntity(6, QLatin1String("F"), QString(), QLatin1String("application/octet-stream")); + // notification.addEntity(7, QLatin1String("G"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("RID items") << scenario << notification << false; + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_with_virtual_collections_0")) + // << "C: 4 HRID LINK ((-1, \"virtual2\") (-1, \"virtual\") (-1, \"\")) UID 5" + // << "S: 4 OK LINK complete"; + // notification.setParentCollection(7); + // notification.clearEntities(); + // notification.addEntity(5, QLatin1String("E"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("HRID collection") << scenario << notification << false; + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_with_virtual_collections_0")) + // << FakeAkonadiServer::selectCollectionScenario(QLatin1String("Collection B")) + // << "C: 4 HRID LINK ((-1, \"virtual2\") (-1, \"virtual\") (-1, \"\")) RID \"H\"" + // << "S: 4 OK LINK complete"; + // notification.clearEntities(); + // notification.addEntity(8, QLatin1String("H"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("HRID collection, RID items") << scenario << notification << false; + } + + void testLink() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Protocol::ChangeNotification, notification); + QFETCH(bool, expectFail); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + QSignalSpy *notificationSpy = FakeAkonadiServer::instance()->notificationSpy(); + if (notification.isValid()) { + QCOMPARE(notificationSpy->count(), 1); + const Protocol::ChangeNotification::List notifications = notificationSpy->takeFirst().first().value(); + QCOMPARE(notifications.count(), 1); + QCOMPARE(notifications.first(), notification); + } else { + QVERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + } + + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, notification.entities()) { + if (expectFail) { + QVERIFY(!Collection::relatesToPimItem(notification.parentCollection(), entity.id)); + } else { + QVERIFY(Collection::relatesToPimItem(notification.parentCollection(), entity.id)); + } + } + } + + void testUnlink_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn("notification"); + QTest::addColumn("expectFail"); + + TestScenario::List scenarios; + Protocol::ChangeNotification notification; + + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommand(Protocol::LinkItemsCommand::Unlink, ImapInterval(1, 3), 3)) + << TestScenario::create(5, TestScenario::ServerCmd, createError(QLatin1String("Can't link items to non-virtual collections"))); + QTest::newRow("non-virtual collection") << scenarios << Protocol::ChangeNotification() << true; + + notification.setType(Protocol::ChangeNotification::Items); + notification.setOperation(Protocol::ChangeNotification::Unlink); + notification.addEntity(1, QLatin1String("A"), QString(), QLatin1String("application/octet-stream")); + notification.addEntity(2, QLatin1String("B"), QString(), QLatin1String("application/octet-stream")); + notification.addEntity(3, QLatin1String("C"), QString(), QLatin1String("application/octet-stream")); + notification.setParentCollection(6); + notification.setResource("akonadi_fake_resource_with_virtual_collections_0"); + notification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommand(Protocol::LinkItemsCommand::Unlink, ImapInterval(1, 3), 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponse()); + QTest::newRow("normal") << scenarios << notification << false; + + notification.clearEntities(); + notification.addEntity(4, QLatin1String("D"), QString(), QLatin1String("application/octet-stream")); + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommand(Protocol::LinkItemsCommand::Unlink, QVector{ 4, 2048 }, 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponse()); + QTest::newRow("existent and non-existent item") << scenarios << notification << false; + + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommand(Protocol::LinkItemsCommand::Unlink, 4096, 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponse()); + QTest::newRow("non-existent item only") << scenarios << Protocol::ChangeNotification() << false; + + //FIXME: All RID related operations are currently broken because we reset the collection context before every command, + //and LINK still relies on SELECT to set the collection context. + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectCollectionScenario(QLatin1String("Collection B")) + // << "C: 4 UID UNLINK 6 RID (\"F\" \"G\")" + // << "S: 4 OK LINK complete"; + // notification.clearEntities(); + // notification.clearEntities(); + // notification.addEntity(6, QLatin1String("F"), QString(), QLatin1String("application/octet-stream")); + // notification.addEntity(7, QLatin1String("G"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("RID items") << scenario << notification << false; + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_with_virtual_collections_0")) + // << "C: 4 HRID UNLINK ((-1, \"virtual2\") (-1, \"virtual\") (-1, \"\")) UID 5" + // << "S: 4 OK LINK complete"; + // notification.setParentCollection(7); + // notification.clearEntities(); + // notification.addEntity(5, QLatin1String("E"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("HRID collection") << scenario << notification << false; + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectCollectionScenario(QLatin1String("Collection B")) + // << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_with_virtual_collections_0")) + // << "C: 4 HRID UNLINK ((-1, \"virtual2\") (-1, \"virtual\") (-1, \"\")) RID \"H\"" + // << "S: 4 OK LINK complete"; + // notification.clearEntities(); + // notification.addEntity(8, QLatin1String("H"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("HRID collection, RID items") << scenario << notification << false; + } + + void testUnlink() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Protocol::ChangeNotification, notification); + QFETCH(bool, expectFail); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + QSignalSpy *notificationSpy = FakeAkonadiServer::instance()->notificationSpy(); + if (notification.isValid()) { + QCOMPARE(notificationSpy->count(), 1); + const Protocol::ChangeNotification::List notifications = notificationSpy->takeFirst().first().value(); + QCOMPARE(notifications.count(), 1); + QCOMPARE(notifications.first(), notification); + } else { + QVERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + } + + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, notification.entities()) { + if (expectFail) { + QVERIFY(Collection::relatesToPimItem(notification.parentCollection(), entity.id)); + } else { + QVERIFY(!Collection::relatesToPimItem(notification.parentCollection(), entity.id)); + } + } + } + +}; + +AKTEST_FAKESERVER_MAIN(LinkHandlerTest) + +#include "linkhandlertest.moc" diff --git a/autotests/server/listhandlertest.cpp b/autotests/server/listhandlertest.cpp new file mode 100644 index 0000000..5f87fa2 --- /dev/null +++ b/autotests/server/listhandlertest.cpp @@ -0,0 +1,592 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 + + +#include + +#include "fakeakonadiserver.h" +#include "aktest.h" +#include "akdebug.h" +#include "entities.h" +#include "dbinitializer.h" +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class ListHandlerTest : public QObject +{ + Q_OBJECT + +public: + ListHandlerTest() + : QObject() + { + try { + FakeAkonadiServer::instance()->setPopulateDb(false); + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + { + MimeType mt(QLatin1String("mimetype1")); + mt.insert(); + } + { + MimeType mt(QLatin1String("mimetype2")); + mt.insert(); + } + { + MimeType mt(QLatin1String("mimetype3")); + mt.insert(); + } + { + MimeType mt(QLatin1String("mimetype4")); + mt.insert(); + } + } + + ~ListHandlerTest() + { + FakeAkonadiServer::instance()->quit(); + } + + Protocol::FetchCollectionsCommand createCommand(const Scope &scope, + Protocol::FetchCollectionsCommand::Depth depth = Akonadi::Protocol::FetchCollectionsCommand::BaseCollection, + Protocol::Ancestor::Depth ancDepth = Protocol::Ancestor::NoAncestor, + const QStringList &mimeTypes = QStringList(), + const QString &resource = QString()) + { + Protocol::FetchCollectionsCommand cmd(scope); + cmd.setDepth(depth); + cmd.setAncestorsDepth(ancDepth); + cmd.setMimeTypes(mimeTypes); + cmd.setResource(resource); + return cmd; + } + + QScopedPointer initializer; +private Q_SLOTS: + + void testList_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + Collection col2 = initializer->createCollection("col2", col1); + Collection col3 = initializer->createCollection("col3", col2); + Collection col4 = initializer->createCollection("col4"); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col4)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive list") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id())) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("base list") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id(), Protocol::FetchCollectionsCommand::ParentCollection)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("first level list") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id(), Protocol::FetchCollectionsCommand::AllCollections)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive list that filters collection") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col2.id(), Protocol::FetchCollectionsCommand::BaseCollection, + Protocol::Ancestor::AllAncestors)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("base ancestors") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col2.id(), Protocol::FetchCollectionsCommand::BaseCollection, + Protocol::Ancestor::AllAncestors)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("first level ancestors") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id(), Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::AllAncestors)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, true)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive ancestors") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(Scope(), Protocol::FetchCollectionsCommand::ParentCollection)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col4)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("first level root list") << scenarios; + } + } + + void testList() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + + void testListFiltered_data() + { + initializer.reset(new DbInitializer); + + MimeType mtCalendar(QLatin1String("text/calendar")); + mtCalendar.insert(); + + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + col1.update(); + Collection col2 = initializer->createCollection("col2", col1); + col2.addMimeType(mtCalendar); + col2.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::NoAncestor, + { QLatin1String("text/calendar") })) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1, false, false)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive list to display including local override") << scenarios; + } + } + + void testListFiltered() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + + void testListFilterByResource() + { + initializer.reset(new DbInitializer); + + Resource res2; + res2.setName(QLatin1String("testresource2")); + QVERIFY(res2.insert()); + + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + + Collection col2; + col2.setName(QLatin1String("col2")); + col2.setRemoteId(QLatin1String("col2")); + col2.setResource(res2); + QVERIFY(col2.insert()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(Scope(), + Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::NoAncestor, {}, QLatin1String("testresource"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + col2.remove(); + res2.remove(); + } + + void testListEnabled_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + Collection col2 = initializer->createCollection("col2", col1); + col2.setEnabled(false); + col2.setSyncPref(Tristate::True); + col2.setDisplayPref(Tristate::True); + col2.setIndexPref(Tristate::True); + col2.update(); + Collection col3 = initializer->createCollection("col3", col2); + col3.setEnabled(true); + col3.setSyncPref(Tristate::False); + col3.setDisplayPref(Tristate::False); + col3.setIndexPref(Tristate::False); + col3.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + + auto cmd = createCommand(col3.id()); + cmd.setDisplayPref(true); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + //Listing a disabled collection should still work for base listing + QTest::newRow("list base of disabled collection") << scenarios; + } + { + TestScenario::List scenarios; + + auto cmd = createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections); + cmd.setDisplayPref(true); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive list to display including local override") << scenarios; + } + { + TestScenario::List scenarios; + + auto cmd = createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections); + cmd.setSyncPref(true); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive list to sync including local override") << scenarios; + } + { + TestScenario::List scenarios; + + auto cmd = createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections); + cmd.setIndexPref(true); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive list to index including local override") << scenarios; + } + { + TestScenario::List scenarios; + + auto cmd = createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections); + cmd.setEnabled(true); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive list of enabled") << scenarios; + } + } + + void testListEnabled() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + + void testListAttribute_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + Collection col2 = initializer->createCollection("col2"); + + CollectionAttribute attr1; + attr1.setType("type"); + attr1.setValue("value"); + attr1.setCollection(col1); + attr1.insert(); + + CollectionAttribute attr2; + attr2.setType("type"); + attr2.setValue(QString::fromUtf8("Umlautäöü").toUtf8()); + attr2.setCollection(col2); + attr2.insert(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id())) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1, false, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("list attribute") << scenarios; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col2.id())) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, false, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("list attribute") << scenarios; + } + } + + void testListAttribute() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + + void testListAncestorAttributes_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + + CollectionAttribute attr1; + attr1.setType("type"); + attr1.setValue("value"); + attr1.setCollection(col1); + attr1.insert(); + + Collection col2 = initializer->createCollection("col2", col1); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + + auto cmd = createCommand(col2.id(), Protocol::FetchCollectionsCommand::BaseCollection, Protocol::Ancestor::AllAncestors); + cmd.setAncestorsAttributes({ "type" }); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, true, true, { QLatin1String("type") })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("list ancestor attribute with fetch scope") << scenarios; + } + } + + void testListAncestorAttributes() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + + void testIncludeAncestors_data() + { + //The collection we are quering contains a load of disabled collections (typical scenario with many shared folders) + //The collection we are NOT querying contains a reasonable amount of enabled collections (to test the performance impact of the manually filtering by tree) + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + + MimeType mtDirectory = MimeType::retrieveByName(QLatin1String("mimetype1")); + + Collection col1 = initializer->createCollection("col1"); + col1.addMimeType(mtDirectory); + col1.update(); + Collection col2 = initializer->createCollection("col2", col1); + Collection col3 = initializer->createCollection("col3", col2); + Collection col4 = initializer->createCollection("col4", col3); + col4.addMimeType(mtDirectory); + col4.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::NoAncestor, { QLatin1String("mimetype1") })) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col4)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("ensure filtered grandparent is included") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id(), Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::NoAncestor, { QLatin1String("mimetype1") })) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col4)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + //This also ensures col1 is excluded although it matches the mimetype filter + QTest::newRow("ensure filtered grandparent is included with specified parent") << scenarios; + } + } + + void testIncludeAncestors() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + +//No point in running the benchmark everytime +#if 0 + + void testListEnabledBenchmark_data() + { + //The collection we are quering contains a load of disabled collections (typical scenario with many shared folders) + //The collection we are NOT querying contains a reasonable amount of enabled collections (to test the performance impact of the manually filtering by tree) + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + + Collection toplevel = initializer->createCollection("toplevel"); + + Collection col1 = initializer->createCollection("col1", toplevel); + Collection col2 = initializer->createCollection("col2", col1); + Collection col3 = initializer->createCollection("col3", col2); + + Collection col4 = initializer->createCollection("col4", toplevel); + Collection col5 = initializer->createCollection("col5", col4); + col5.setEnabled(false); + col5.update(); + Collection col6 = initializer->createCollection("col6", col5); + col5.setEnabled(false); + col5.update(); + + MimeType mt1 = MimeType::retrieveByName(QLatin1String("mimetype1")); + MimeType mt2 = MimeType::retrieveByName(QLatin1String("mimetype2")); + MimeType mt3 = MimeType::retrieveByName(QLatin1String("mimetype3")); + MimeType mt4 = MimeType::retrieveByName(QLatin1String("mimetype4")); + + QTime t; + t.start(); + for (int i = 0; i < 100000; i++) { + QByteArray name = QString::fromLatin1("col%1").arg(i+4).toLatin1(); + Collection col = initializer->createCollection(name.data(), col3); + col.setEnabled(false); + col.addMimeType(mt1); + col.addMimeType(mt2); + col.addMimeType(mt3); + col.addMimeType(mt4); + col.update(); + } + for (int i = 0; i < 1000; i++) { + QByteArray name = QString::fromLatin1("col%1").arg(i+100004).toLatin1(); + Collection col = initializer->createCollection(name.data(), col5); + col.addMimeType(mt1); + col.addMimeType(mt2); + col.update(); + } + + qDebug() << "Created 100000 collections in" << t.elapsed() << "msecs"; + + QTest::addColumn("scenarios"); + + // { + // QList scenario; + // scenario << FakeAkonadiServer::defaultScenario() + // << "C: 2 LIST " + QByteArray::number(toplevel.id()) + " INF (ENABLED ) ()" + // << "S: IGNORE 1006" + // << "S: 2 OK List completed"; + // QTest::newRow("recursive list of enabled") << scenario; + // } + // { + // QList scenario; + // scenario << FakeAkonadiServer::defaultScenario() + // << "C: 2 LIST " + QByteArray::number(toplevel.id()) + " INF (MIMETYPE (mimetype1) RESOURCE \"testresource\") ()" + // // << "C: 2 LIST " + QByteArray::number(0) + " INF (RESOURCE \"testresource\") ()" + // << "S: IGNORE 101005" + // << "S: 2 OK List completed"; + // QTest::newRow("recursive list filtered by mimetype") << scenario; + // } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(toplevel.id(), Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::AllAncestors, { QLatin1String("mimetype1") }, QLatin1String("testresource"))) + << TestScenario::ignore(101005) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive list filtered by mimetype with ancestors") << scenarios; + } + } + + void testListEnabledBenchmark() + { + QFETCH(TestScenario::List, scenarios); + // StorageDebugger::instance()->enableSQLDebugging(true); + // StorageDebugger::instance()->writeToFile(QLatin1String("sqllog.txt")); + + QBENCHMARK { + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + } + +#endif + +}; + +AKTEST_FAKESERVER_MAIN(ListHandlerTest) + +#include "listhandlertest.moc" diff --git a/autotests/server/mockobjects.h b/autotests/server/mockobjects.h new file mode 100644 index 0000000..dbd269d --- /dev/null +++ b/autotests/server/mockobjects.h @@ -0,0 +1,61 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef MOCKOBJECTS_H +#define MOCKOBJECTS_H + +#include "akonadiconnection.h" +#include "teststoragebackend.h" + +using namespace Akonadi; + +static AkonadiConnection *s_connection = 0; +static DataStore *s_backend = 0; + +class MockConnection : public AkonadiConnection +{ +public: + MockConnection() + { + } + DataStore *storageBackend() + { + if (!s_backend) { + s_backend = new MockBackend(); + } + return s_backend; + } +}; + +class MockObjects +{ +public: + MockObjects(); + ~MockObjects(); + + static AkonadiConnection *mockConnection() + { + if (!s_connection) { + s_connection = new MockConnection(); + } + return s_connection; + } +}; // End of class MockObjects + +#endif // MOCKOBJECTS_H diff --git a/autotests/server/modifyhandlertest.cpp b/autotests/server/modifyhandlertest.cpp new file mode 100644 index 0000000..579e9fd --- /dev/null +++ b/autotests/server/modifyhandlertest.cpp @@ -0,0 +1,186 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 + +#include + +#include "fakeakonadiserver.h" +#include "aktest.h" +#include "akdebug.h" +#include "entities.h" + +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(QList) + +class ModifyHandlerTest : public QObject +{ + Q_OBJECT + +public: + ModifyHandlerTest() + { + try { + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + } + + ~ModifyHandlerTest() + { + FakeAkonadiServer::instance()->quit(); + } + +private Q_SLOTS: + void testModify_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn >("expectedNotifications"); + QTest::addColumn("newValue"); + + Akonadi::Protocol::ChangeNotification notificationTemplate; + notificationTemplate.setType(Protocol::ChangeNotification::Collections); + notificationTemplate.setOperation(Protocol::ChangeNotification::Modify); + notificationTemplate.addEntity(5, QStringLiteral("ColD"), QStringLiteral("")); + notificationTemplate.setParentCollection(4); + notificationTemplate.setResource("akonadi_fake_resource_0"); + notificationTemplate.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + { + Protocol::ModifyCollectionCommand cmd(5); + cmd.setName(QStringLiteral("New Name")); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.setItemParts(QSet() << "NAME"); + + QTest::newRow("modify collection") << scenarios << (QList() << notification) << QVariant::fromValue(QString::fromLatin1("New Name")); + } + { + Protocol::ModifyCollectionCommand cmd(5); + cmd.setEnabled(false); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.setItemParts(QSet() << "ENABLED"); + Akonadi::Protocol::ChangeNotification unsubscribeNotification = notificationTemplate; + unsubscribeNotification.setOperation(Protocol::ChangeNotification::Unsubscribe); + + QTest::newRow("disable collection") << scenarios << (QList() << notification << unsubscribeNotification) << QVariant::fromValue(false); + } + { + Protocol::ModifyCollectionCommand cmd(5); + cmd.setEnabled(true); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.setItemParts(QSet() << "ENABLED"); + Akonadi::Protocol::ChangeNotification subscribeNotification = notificationTemplate; + subscribeNotification.setOperation(Protocol::ChangeNotification::Subscribe); + + QTest::newRow("enable collection") << scenarios << (QList() << notification << subscribeNotification) << QVariant::fromValue(true); + } + { + Protocol::ModifyCollectionCommand cmd(5); + cmd.setEnabled(false); + cmd.setSyncPref(Tristate::True); + cmd.setDisplayPref(Tristate::True); + cmd.setIndexPref(Tristate::True); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponse()); + + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + notification.setItemParts(QSet() << "ENABLED" << "SYNC" << "DISPLAY" << "INDEX"); + Akonadi::Protocol::ChangeNotification unsubscribeNotification = notificationTemplate; + unsubscribeNotification.setOperation(Protocol::ChangeNotification::Unsubscribe); + + QTest::newRow("local override enable") << scenarios << (QList() << notification << unsubscribeNotification) << QVariant::fromValue(true); + } + } + + void testModify() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(QList, expectedNotifications); + QFETCH(QVariant, newValue); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + QSignalSpy *notificationSpy = FakeAkonadiServer::instance()->notificationSpy(); + if (expectedNotifications.isEmpty()) { + QVERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + return; + } + QCOMPARE(notificationSpy->count(), 1); + //Only one notify call + QCOMPARE(notificationSpy->first().count(), 1); + const Protocol::ChangeNotification::List receivedNotifications = notificationSpy->first().first().value(); + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + + for (int i = 0; i < expectedNotifications.size(); i++) { + QCOMPARE(receivedNotifications.at(i), expectedNotifications.at(i)); + Protocol::ChangeNotification notification = receivedNotifications.at(i); + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, notification.entities()) { + if (notification.itemParts().contains("NAME")) { + Collection col = Collection::retrieveById(entity.id); + QCOMPARE(col.name(), newValue.toString()); + } + if (notification.itemParts().contains("ENABLED") || notification.itemParts().contains("SYNC") || notification.itemParts().contains("DISPLAY") || notification.itemParts().contains("INDEX")) { + Collection col = Collection::retrieveById(entity.id); + const bool sync = col.syncPref() == Tristate::Undefined ? col.enabled() : col.syncPref() == Tristate::True; + QCOMPARE(sync, newValue.toBool()); + const bool display = col.displayPref() == Tristate::Undefined ? col.enabled() : col.displayPref() == Tristate::True; + QCOMPARE(display, newValue.toBool()); + const bool index = col.indexPref() == Tristate::Undefined ? col.enabled() : col.indexPref() == Tristate::True; + QCOMPARE(index, newValue.toBool()); + } + } + } + } + +}; + +AKTEST_FAKESERVER_MAIN(ModifyHandlerTest) + +#include "modifyhandlertest.moc" diff --git a/autotests/server/notificationmanagertest.cpp b/autotests/server/notificationmanagertest.cpp new file mode 100644 index 0000000..7fed294 --- /dev/null +++ b/autotests/server/notificationmanagertest.cpp @@ -0,0 +1,308 @@ +/* + Copyright (c) 2013 Daniel Vrátil + + 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 "entities.h" +#include "notificationmanager.h" +#include "notificationsource.h" + +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(QVector) + +class NotificationManagerTest : public QObject +{ + Q_OBJECT + + typedef QList NSList; + +private Q_SLOTS: + void testSourceFilter_data() + { + qRegisterMetaType(); + + QTest::addColumn("allMonitored"); + QTest::addColumn >("monitoredCollections"); + QTest::addColumn >("monitoredItems"); + QTest::addColumn >("monitoredResources"); + QTest::addColumn >("monitoredMimeTypes"); + QTest::addColumn >("ignoredSessions"); + QTest::addColumn("notification"); + QTest::addColumn("accepted"); + + Protocol::ChangeNotification msg; + +#define EmptyList(T) (QVector()) +#define List(T,x) (QVector() << x) + + msg = Protocol::ChangeNotification(); + msg.setType(Protocol::ChangeNotification::Items); + msg.setOperation(Protocol::ChangeNotification::Add); + msg.setParentCollection(1); + QTest::newRow("monitorAll vs notification without items") + << true + << EmptyList(Entity::Id) + << EmptyList(Entity::Id) + << EmptyList(QByteArray) + << EmptyList(QString) + << EmptyList(QByteArray) + << msg + << false; + + msg.addEntity(1, QString(), QString(), QStringLiteral("message/rfc822")); + QTest::newRow("monitorAll vs notification with one item") + << true + << EmptyList(Entity::Id) + << EmptyList(Entity::Id) + << EmptyList(QByteArray) + << EmptyList(QString) + << EmptyList(QByteArray) + << msg + << true; + + QTest::newRow("item monitored but different mimetype") + << false + << EmptyList(Entity::Id) + << List(Entity::Id, 1 << 2) + << EmptyList(QByteArray) + << List(QString, QStringLiteral("random/mimetype")) + << EmptyList(QByteArray) + << msg + << false; + + QTest::newRow("item not monitored, but mimetype matches") + << false + << EmptyList(Entity::Id) + << EmptyList(Entity::Id) + << EmptyList(QByteArray) + << List(QString, QStringLiteral("message/rfc822")) + << EmptyList(QByteArray) + << msg + << true; + + msg.setSessionId("testSession"); + QTest::newRow("item monitored but session ignored") + << false + << EmptyList(Entity::Id) + << List(Entity::Id, 1) + << EmptyList(QByteArray) + << EmptyList(QString) + << List(QByteArray, "testSession") + << msg + << false; + + // Simulate adding a new resource + msg = Protocol::ChangeNotification(); + msg.setType(Protocol::ChangeNotification::Collections); + msg.setOperation(Protocol::ChangeNotification::Add); + msg.addEntity(1, QStringLiteral("imap://user@some.domain/")); + msg.setParentCollection(0); + msg.setSessionId("akonadi_imap_resource_0"); + msg.setResource("akonadi_imap_resource_0"); + QTest::newRow("new root collection in non-monitored resource") + << false + << List(Entity::Id, 0) + << EmptyList(Entity::Id) + << List(QByteArray, "akonadi_search_resource") + << List(QString, QStringLiteral("message/rfc822")) + << EmptyList(QByteArray) + << msg + << true; + + msg = Protocol::ChangeNotification(); + msg.setType(Protocol::ChangeNotification::Items); + msg.setOperation(Protocol::ChangeNotification::Move); + msg.setResource("akonadi_resource_1"); + msg.setDestinationResource("akonadi_resource_2"); + msg.setParentCollection(1); + msg.setParentDestCollection(2); + msg.setSessionId("kmail"); + msg.addEntity(10, QStringLiteral("123"), QStringLiteral("1"), QStringLiteral("message/rfc822")); + QTest::newRow("inter-resource move, source source") + << false + << EmptyList(Entity::Id) + << EmptyList(Entity::Id) + << List(QByteArray, "akonadi_resource_1") + << List(QString, QStringLiteral("message/rfc822")) + << List(QByteArray, "akonadi_resource_1") + << msg + << true; + + QTest::newRow("inter-resource move, destination source") + << false + << EmptyList(Entity::Id) + << EmptyList(Entity::Id) + << List(QByteArray, "akonadi_resource_2") + << List(QString, QStringLiteral("message/rfc822")) + << List(QByteArray, "akonadi_resource_2") + << msg + << true; + + QTest::newRow("inter-resource move, uninterested party") + << false + << List(Entity::Id, 12) + << EmptyList(Entity::Id) + << EmptyList(QByteArray) + << List(QString, QStringLiteral("inode/directory")) + << EmptyList(QByteArray) + << msg + << false; + + msg = Protocol::ChangeNotification(); + msg.setType(Protocol::ChangeNotification::Collections); + msg.setOperation(Protocol::ChangeNotification::Add); + msg.setSessionId("kmail"); + msg.setResource("akonadi_resource_1"); + msg.setParentCollection(1); + QTest::newRow("new subfolder") + << false + << List(Entity::Id, 0) + << EmptyList(Entity::Id) + << EmptyList(QByteArray) + << List(QString, QStringLiteral("message/rfc822")) + << EmptyList(QByteArray) + << msg + << false; + + msg = Protocol::ChangeNotification(); + msg.setType(Protocol::ChangeNotification::Items); + msg.setOperation(Protocol::ChangeNotification::Add); + msg.setSessionId("randomSession"); + msg.setResource("randomResource"); + msg.setParentCollection(1); + msg.addEntity(10, QString(), QString(), QStringLiteral("message/rfc822")); + QTest::newRow("new mail for mailfilter or maildispatcher") + << false + << List(Entity::Id, 0) + << EmptyList(Entity::Id) + << EmptyList(QByteArray) + << List(QString, QStringLiteral("message/rfc822")) + << EmptyList(QByteArray) + << msg + << true; + + msg = Protocol::ChangeNotification(); + msg.setType( Protocol::ChangeNotification::Tags ); + msg.setOperation( Protocol::ChangeNotification::Remove ); + msg.setSessionId( "randomSession" ); + msg.setResource( "akonadi_random_resource_0" ); + msg.addEntity( 1, QStringLiteral("TAG") ); + QTest::newRow( "Tag removal - resource notification - matching resource source") + << false + << EmptyList( Entity::Id ) + << EmptyList( Entity::Id ) + << EmptyList( QByteArray ) + << EmptyList( QString ) + << List( QByteArray, "akonadi_random_resource_0" ) + << msg + << true; + + QTest::newRow( "Tag removal - resource notification - wrong resource source" ) + << false + << EmptyList( Entity::Id ) + << EmptyList( Entity::Id ) + << EmptyList( QByteArray ) + << EmptyList( QString ) + << List( QByteArray, "akonadi_another_resource_1" ) + << msg + << false; + + msg = Protocol::ChangeNotification(); + msg.setType( Protocol::ChangeNotification::Tags ); + msg.setOperation( Protocol::ChangeNotification::Remove ); + msg.setSessionId( "randomSession" ); + msg.addEntity( 1, QStringLiteral("TAG") ); + QTest::newRow( "Tag removal - client notification - client source" ) + << false + << EmptyList( Entity::Id ) + << EmptyList( Entity::Id ) + << EmptyList( QByteArray ) + << EmptyList( QString ) + << EmptyList( QByteArray ) + << msg + << true; + + QTest::newRow( "Tag removal - client notification - resource source" ) + << false + << EmptyList( Entity::Id ) + << EmptyList( Entity::Id ) + << EmptyList( QByteArray ) + << EmptyList( QString ) + << List( QByteArray, "akonadi_some_resource_0" ) + << msg + << false; + } + + void testSourceFilter() + { + QFETCH(bool, allMonitored); + QFETCH(QVector, monitoredCollections); + QFETCH(QVector, monitoredItems); + QFETCH(QVector, monitoredResources); + QFETCH(QVector, monitoredMimeTypes); + QFETCH(QVector, ignoredSessions); + QFETCH(Protocol::ChangeNotification, notification); + QFETCH(bool, accepted); + + NotificationManager mgr; + NotificationSource source(QStringLiteral("testSource"), QString(), &mgr); + mgr.registerSource(&source); + + source.setAllMonitored(allMonitored); + Q_FOREACH (Entity::Id id, monitoredCollections) { + source.setMonitoredCollection(id, true); + } + Q_FOREACH (Entity::Id id, monitoredItems) { + source.setMonitoredItem(id, true); + } + Q_FOREACH (const QByteArray &res, monitoredResources) { + source.setMonitoredResource(res, true); + } + Q_FOREACH (const QString &mimeType, monitoredMimeTypes) { + source.setMonitoredMimeType(mimeType, true); + } + Q_FOREACH (const QByteArray &session, ignoredSessions) { + source.setIgnoredSession(session, true); + } + + QSignalSpy spy(&source, SIGNAL(notify(Akonadi::Protocol::Command))); + Protocol::ChangeNotification::List list; + list << notification; + mgr.slotNotify(list); + mgr.emitPendingNotifications(); + + QCOMPARE(spy.count(), accepted ? 1 : 0); + + if (accepted) { + Protocol::ChangeNotification ntf = spy.at(0).at(0).value(); + QVERIFY(ntf.isValid()); + } + } +}; + +AKTEST_MAIN(NotificationManagerTest) + +#include "notificationmanagertest.moc" diff --git a/autotests/server/parthelpertest.cpp b/autotests/server/parthelpertest.cpp new file mode 100644 index 0000000..097fecb --- /dev/null +++ b/autotests/server/parthelpertest.cpp @@ -0,0 +1,128 @@ +/* + Copyright (c) 2012 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 "entities.h" +#include "storage/parthelper.h" + +#include +#include +#include +#include + +#define QL1S(x) QString::fromLatin1(x) + +using namespace Akonadi::Server; + +class PartHelperTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: +#if 0 + void testFileName() + { + akTestSetInstanceIdentifier(QString()); + + Part p; + p.setId(42); + + QString fileName = PartHelper::fileNameForPart(&p); + QVERIFY(fileName.endsWith(QL1S("42"))); + } +#endif + + void testRemoveFile_data() + { + QTest::addColumn("instance"); + QTest::newRow("main") << QString(); + QTest::newRow("multi-instance") << QL1S("foo"); + } + +#if 0 + void testRemoveFile() + { + QFETCH(QString, instance); + akTestSetInstanceIdentifier(instance); + + Part p; + p.setId(23); + const QString validFileName = PartHelper::storagePath() + QDir::separator() + PartHelper::fileNameForPart(&p); + PartHelper::removeFile(validFileName); // no throw + } +#endif + +#if 0 + void testInvalidRemoveFile_data() + { + QTest::addColumn("fileName"); + QTest::newRow("empty") << QString(); + QTest::newRow("relative") << QL1S("foo"); + QTest::newRow("absolute") << QL1S("/foo"); + + akTestSetInstanceIdentifier(QL1S("foo")); + Part p; + p.setId(23); + QTest::newRow("wrong instance") << PartHelper::fileNameForPart(&p); + } +#endif + +#if 0 + void testInvalidRemoveFile() + { + QFETCH(QString, fileName); + akTestSetInstanceIdentifier(QString()); + try { + PartHelper::removeFile(fileName); + } catch (const PartHelperException &e) { + return; // all good + } + QVERIFY(false); // didn't throw + } +#endif + +#if 0 + void testStorageLocation() + { + akTestSetInstanceIdentifier(QString()); + const QString mainLocation = PartHelper::storagePath(); + QVERIFY(mainLocation.endsWith(QDir::separator())); + QVERIFY(mainLocation.startsWith(QDir::separator())); + + akTestSetInstanceIdentifier(QL1S("foo")); + QVERIFY(PartHelper::storagePath().endsWith(QDir::separator())); + QVERIFY(PartHelper::storagePath().startsWith(QDir::separator())); + QVERIFY(mainLocation != PartHelper::storagePath()); + } +#endif + +#if 0 + void testResolveAbsolutePath() + { +#ifndef Q_OS_WIN + QVERIFY(PartHelper::resolveAbsolutePath("foo").startsWith(QLatin1Char('/'))); + QCOMPARE(PartHelper::resolveAbsolutePath("/foo"), QString::fromLatin1("/foo")); + QVERIFY(!PartHelper::resolveAbsolutePath("foo").contains(QL1S("//"))); // no double separator +#endif + } +#endif +}; + +AKTEST_MAIN(PartHelperTest) + +#include "parthelpertest.moc" diff --git a/autotests/server/partstreamertest.cpp b/autotests/server/partstreamertest.cpp new file mode 100644 index 0000000..6a649a2 --- /dev/null +++ b/autotests/server/partstreamertest.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include + +#include "fakeakonadiserver.h" +#include "fakeconnection.h" +#include "akdebug.h" +#include "aktest.h" +#include "entities.h" + +#include +#include + +#include "storage/partstreamer.h" +#include + +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::PimItem) + +class PartStreamerTest : public QObject +{ + Q_OBJECT + +public: + PartStreamerTest() + { + // Set a very small treshold for easier testing + const QString serverConfigFile = StandardDirs::serverConfigFile(XdgBaseDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + settings.setValue(QLatin1String("General/SizeThreshold"), 5); + + try { + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + } + + ~PartStreamerTest() + { + FakeAkonadiServer::instance()->quit(); + } + + Protocol::ModifyItemsCommand createCommand(const PimItem &item) + { + Protocol::ModifyItemsCommand cmd(item.id()); + cmd.setParts({ "PLD:DATA" }); + return cmd; + } + +private Q_SLOTS: + void testStreamer_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn("expectedPartName"); + QTest::addColumn("expectedPartData"); + QTest::addColumn("expectedFileData"); + QTest::addColumn("expectedPartSize"); + QTest::addColumn("expectedChanged"); + QTest::addColumn("isExternal"); + QTest::addColumn("pimItem"); + + PimItem item; + item.setCollectionId(Collection::retrieveByName(QLatin1String("Col A")).id()); + item.setMimeType(MimeType::retrieveByName(QLatin1String("application/octet-stream"))); + item.setSize(1); // this will not match reality during the test, but that does not matter, as + // that's not the subject of this test + QVERIFY(item.insert()); + + qint64 partId = -1; + Part::List parts = Part::retrieveAll(); + if (parts.isEmpty()) { + partId = 0; + } else { + partId = parts.last().id() + 1; + } + + // Order of these tests matters! + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 3))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", "123")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponse(item.id(), 1)); + + QTest::newRow("item 1, internal") << scenarios << QByteArray("PLD:DATA") << QByteArray("123") << QByteArray() << 3ll << true << false << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 9))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data, QString::fromLatin1("%1_r0").arg(partId))) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponse(item.id(), 2)); + + QTest::newRow("item 1, change to external") << scenarios << QByteArray("PLD:DATA") << QByteArray("15_r0") << QByteArray("123456789") << 9ll << true << true << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 9))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data, QString::fromLatin1("%1_r1").arg(partId))) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponse(item.id(), 3)); + + QTest::newRow("item 1, update external") << scenarios << QByteArray("PLD:DATA") << QByteArray("15_r1") << QByteArray("987654321") << 9ll << true << true << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 9))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data, QString::fromLatin1("%1_r2").arg(partId))) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponse(item.id(), 4)); + + QTest::newRow("item 1, external, no change") << scenarios << QByteArray("PLD:DATA") << QByteArray("15_r2") << QByteArray("987654321") << 9ll << false << true << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 4))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", "1234")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponse(item.id(), 5)); + + QTest::newRow("item 1, change to internal") << scenarios << QByteArray("PLD:DATA") << QByteArray("1234") << QByteArray() << 4ll << true << false << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 4))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommand("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponse("PLD:DATA", "1234")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponse(item.id(), 6)); + + QTest::newRow("item 1, internal, no change") << scenarios << QByteArray("PLD:DATA") << QByteArray("1234") << QByteArray() << 4ll << false << false << item; + } + } + + void testStreamer() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(QByteArray, expectedPartName); + QFETCH(QByteArray, expectedPartData); + QFETCH(QByteArray, expectedFileData); + QFETCH(qint64, expectedPartSize); + QFETCH(bool, isExternal); + QFETCH(PimItem, pimItem); + + if (isExternal) { + // Create the payload file now, since don't have means to react + // directly to the streaming command + QFile file(ExternalPartStorage::resolveAbsolutePath(expectedPartData)); + file.open(QIODevice::WriteOnly); + file.write(expectedFileData); + file.close(); + } + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + PimItem item = PimItem::retrieveById(pimItem.id()); + const QVector parts = item.parts(); + QVERIFY(parts.count() == 1); + const Part part = parts[0]; + QCOMPARE(part.datasize(), expectedPartSize); + QCOMPARE(part.external(), isExternal); + const QByteArray data = part.data(); + + if (isExternal) { + QCOMPARE(data, expectedPartData); + QFile file(ExternalPartStorage::resolveAbsolutePath(data)); + QVERIFY(file.exists()); + QCOMPARE(file.size(), expectedPartSize); + QVERIFY(file.open(QIODevice::ReadOnly)); + + const QByteArray fileData = file.readAll(); + QCOMPARE(fileData, expectedFileData); + + // Make sure no previous versions are left behind in file_db_data + for (int i = 0; i < part.version(); ++i) { + const QByteArray fileName = QByteArray::number(part.id()) + "_r" + QByteArray::number(part.version()); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(fileName); + QVERIFY(!QFile::exists(filePath)); + } + } else { + QCOMPARE(data, expectedPartData); + + // Make sure nothing is left behind in file_db_data + for (int i = 0; i <= part.version(); ++i) { + const QByteArray fileName = QByteArray::number(part.id()) + "_r" + QByteArray::number(part.version()); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(fileName); + QVERIFY(!QFile::exists(filePath)); + } + } + } + +}; + +AKTEST_FAKESERVER_MAIN(PartStreamerTest) + +#include "partstreamertest.moc" diff --git a/autotests/server/parttypehelpertest.cpp b/autotests/server/parttypehelpertest.cpp new file mode 100644 index 0000000..912a1dc --- /dev/null +++ b/autotests/server/parttypehelpertest.cpp @@ -0,0 +1,89 @@ +/* + Copyright (c) 2011 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 + +#include +#include + +#define QL1S(x) QStringLiteral(x) + +using namespace Akonadi::Server; + +class PartTypeHelperTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testParseFqName_data() + { + QTest::addColumn("fqName"); + QTest::addColumn("ns"); + QTest::addColumn("name"); + QTest::addColumn("shouldThrow"); + + QTest::newRow("empty") << QString() << QString() << QString() << true; + QTest::newRow("valid") << "PLD:RFC822" << "PLD" << "RFC822" << false; + QTest::newRow("no separator") << "ABC" << QString() << QString() << true; + QTest::newRow("no ns") << ":RFC822" << QString() << QString() << true; + QTest::newRow("no name") << "PLD:" << QString() << QString() << true; + QTest::newRow("too many separators") << "A:B:C" << QString() << QString() << true; + } + + void testParseFqName() + { + QFETCH(QString, fqName); + QFETCH(QString, ns); + QFETCH(QString, name); + QFETCH(bool, shouldThrow); + + QPair p; + bool didThrow = false; + try { + p = PartTypeHelper::parseFqName(fqName); + } catch (const PartTypeException &e) { + didThrow = true; + } + + QCOMPARE(didThrow, shouldThrow); + QCOMPARE(p.first, ns); + QCOMPARE(p.second, name); + } + + void testConditionFromName() + { + Query::Condition c = PartTypeHelper::conditionFromFqName(QL1S("PLD:RFC822")); + QVERIFY(!c.isEmpty()); + QCOMPARE(c.subConditions().size(), 2); + } + + void testConditionFromNames() + { + Query::Condition c = PartTypeHelper::conditionFromFqNames(QStringList() << QL1S("PLD:RFC822") << QL1S("PLD:HEAD") << QL1S("PLD:ENVELOPE")); + QVERIFY(!c.isEmpty()); + QCOMPARE(c.subConditions().size(), 3); + Q_FOREACH (const Query::Condition &subC, c.subConditions()) { + QCOMPARE(subC.subConditions().size(), 2); + } + } +}; + +AKTEST_MAIN(PartTypeHelperTest) + +#include "parttypehelpertest.moc" diff --git a/autotests/server/querybuildertest.cpp b/autotests/server/querybuildertest.cpp new file mode 100644 index 0000000..7df5cc9 --- /dev/null +++ b/autotests/server/querybuildertest.cpp @@ -0,0 +1,347 @@ +/* + 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 "querybuildertest.h" +#include "moc_querybuildertest.cpp" + +#define QUERYBUILDER_UNITTEST + +#include "storage/query.cpp" +#include "storage/querybuilder.cpp" + +#include + +QTEST_MAIN(QueryBuilderTest) + +Q_DECLARE_METATYPE(QVector) + +using namespace Akonadi::Server; + +void QueryBuilderTest::testQueryBuilder_data() +{ + qRegisterMetaType >(); + mBuilders.clear(); + QTest::addColumn("qbId"); + QTest::addColumn("sql"); + QTest::addColumn >("bindValues"); + + QueryBuilder qb("table", QueryBuilder::Select); + qb.addColumn("col1"); + mBuilders << qb; + QTest::newRow("simple select") << mBuilders.count() << QString("SELECT col1 FROM table") << QVector(); + + qb.addColumn("col2"); + mBuilders << qb; + QTest::newRow("simple select 2") << mBuilders.count() << QString("SELECT col1, col2 FROM table") << QVector(); + + qb.addValueCondition("col1", Query::Equals, QVariant(5)); + QVector bindVals; + bindVals << QVariant(5); + mBuilders << qb; + QTest::newRow("single where") << mBuilders.count() << QString("SELECT col1, col2 FROM table WHERE ( col1 = :0 )") << bindVals; + + qb.addColumnCondition("col1", Query::LessOrEqual, "col2"); + mBuilders << qb; + QTest::newRow("flat where") << mBuilders.count() << QString("SELECT col1, col2 FROM table WHERE ( col1 = :0 AND col1 <= col2 )") << bindVals; + + qb.setSubQueryMode(Query::Or); + mBuilders << qb; + QTest::newRow("flat where 2") << mBuilders.count() << QString("SELECT col1, col2 FROM table WHERE ( col1 = :0 OR col1 <= col2 )") << bindVals; + + Condition subCon; + subCon.addColumnCondition("col1", Query::Greater, "col2"); + subCon.addValueCondition("col1", Query::NotEquals, QVariant()); + qb.addCondition(subCon); + mBuilders << qb; + QTest::newRow("hierarchical where") << mBuilders.count() << QString("SELECT col1, col2 FROM table WHERE ( col1 = :0 OR col1 <= col2 OR ( col1 > col2 AND col1 <> NULL ) )") << bindVals; + + qb = QueryBuilder("table"); + qb.addAggregation("col1", "count"); + mBuilders << qb; + QTest::newRow("single aggregation") << mBuilders.count() << QString("SELECT count(col1) FROM table") << QVector(); + + qb = QueryBuilder("table"); + qb.addColumn("col1"); + qb.addSortColumn("col1"); + mBuilders << qb; + QTest::newRow("single order by") << mBuilders.count() << QString("SELECT col1 FROM table ORDER BY col1 ASC") << QVector(); + + qb.addSortColumn("col2", Query::Descending); + mBuilders << qb; + QTest::newRow("multiple order by") << mBuilders.count() << QString("SELECT col1 FROM table ORDER BY col1 ASC, col2 DESC") << QVector(); + + qb = QueryBuilder("table"); + qb.addColumn("col1"); + QStringList vals; + vals << "a" << "b" << "c"; + qb.addValueCondition("col1", Query::In, vals); + bindVals.clear(); + bindVals << QString("a") << QString("b") << QString("c"); + mBuilders << qb; + QTest::newRow("where in") << mBuilders.count() << QString("SELECT col1 FROM table WHERE ( col1 IN ( :0, :1, :2 ) )") << bindVals; + + qb = QueryBuilder("table", QueryBuilder::Select); + qb.setDatabaseType(DbType::MySQL); + qb.addColumn("col1"); + qb.setLimit(1); + mBuilders << qb; + QTest::newRow("SELECT with LIMIT") << mBuilders.count() << QString("SELECT col1 FROM table LIMIT 1") << QVector(); + + qb = QueryBuilder("table", QueryBuilder::Update); + qb.setColumnValue("col1", QString("bla")); + bindVals.clear(); + bindVals << QString("bla"); + mBuilders << qb; + QTest::newRow("update") << mBuilders.count() << QString("UPDATE table SET col1 = :0") << bindVals; + + qb = QueryBuilder("table1", QueryBuilder::Update); + qb.setDatabaseType(DbType::MySQL); + qb.addJoin(QueryBuilder::InnerJoin, "table2", "table1.id", "table2.id"); + qb.addJoin(QueryBuilder::InnerJoin, "table3", "table1.id", "table3.id"); + qb.setColumnValue("col1", QString("bla")); + bindVals.clear(); + bindVals << QString("bla"); + mBuilders << qb; + QTest::newRow("update multi table MYSQL") << mBuilders.count() << QString("UPDATE table1, table2, table3 SET col1 = :0 WHERE ( ( table1.id = table2.id ) AND ( table1.id = table3.id ) )") + << bindVals; + + qb = QueryBuilder("table1", QueryBuilder::Update); + qb.setDatabaseType(DbType::PostgreSQL); + qb.addJoin(QueryBuilder::InnerJoin, "table2", "table1.id", "table2.id"); + qb.addJoin(QueryBuilder::InnerJoin, "table3", "table1.id", "table3.id"); + qb.setColumnValue("col1", QString("bla")); + mBuilders << qb; + QTest::newRow("update multi table PSQL") << mBuilders.count() << QString("UPDATE table1 SET col1 = :0 FROM table2 JOIN table3 WHERE ( ( table1.id = table2.id ) AND ( table1.id = table3.id ) )") + << bindVals; + ///TODO: test for subquery in SQLite case + + qb = QueryBuilder("table", QueryBuilder::Insert); + qb.setColumnValue("col1", QString("bla")); + mBuilders << qb; + QTest::newRow("insert single column") << mBuilders.count() << QString("INSERT INTO table (col1) VALUES (:0)") << bindVals; + + qb = QueryBuilder("table", QueryBuilder::Insert); + qb.setColumnValue("col1", QString("bla")); + qb.setColumnValue("col2", 5); + bindVals << 5; + mBuilders << qb; + QTest::newRow("insert multi column") << mBuilders.count() << QString("INSERT INTO table (col1, col2) VALUES (:0, :1)") << bindVals; + + qb = QueryBuilder("table", QueryBuilder::Insert); + qb.setDatabaseType(DbType::PostgreSQL); + qb.setColumnValue("col1", QString("bla")); + qb.setColumnValue("col2", 5); + mBuilders << qb; + QTest::newRow("insert multi column PSQL") << mBuilders.count() << QString("INSERT INTO table (col1, col2) VALUES (:0, :1) RETURNING id") << bindVals; + + qb.setIdentificationColumn(QString()); + mBuilders << qb; + QTest::newRow("insert multi column PSQL without id") << mBuilders.count() << QString("INSERT INTO table (col1, col2) VALUES (:0, :1)") << bindVals; + + // test GROUP BY foo + bindVals.clear(); + qb = QueryBuilder("table", QueryBuilder::Select); + qb.addColumn("foo"); + qb.addGroupColumn("id1"); + mBuilders << qb; + QTest::newRow("select group by single column") << mBuilders.count() << QString("SELECT foo FROM table GROUP BY id1") << bindVals; + // test GROUP BY foo, bar + qb.addGroupColumn("id2"); + mBuilders << qb; + QTest::newRow("select group by two columns") << mBuilders.count() << QString("SELECT foo FROM table GROUP BY id1, id2") << bindVals; + // test: HAVING .addValueCondition() + qb.addValueCondition("bar", Equals, 1, QueryBuilder::HavingCondition); + mBuilders << qb; + bindVals << 1; + QTest::newRow("select with having valueCond") << mBuilders.count() << QString("SELECT foo FROM table GROUP BY id1, id2 HAVING ( bar = :0 )") << bindVals; + // test: HAVING .addColumnCondition() + qb.addColumnCondition("asdf", Equals, "yxcv", QueryBuilder::HavingCondition); + mBuilders << qb; + QTest::newRow("select with having columnCond") << mBuilders.count() << QString("SELECT foo FROM table GROUP BY id1, id2 HAVING ( bar = :0 AND asdf = yxcv )") << bindVals; + // test: HAVING .addCondition() + qb.addCondition(subCon, QueryBuilder::HavingCondition); + mBuilders << qb; + QTest::newRow("select with having condition") << mBuilders.count() << QString("SELECT foo FROM table GROUP BY id1, id2 HAVING ( bar = :0 AND asdf = yxcv AND ( col1 > col2 AND col1 <> NULL ) )") << bindVals; + // test: HAVING and WHERE + qb.addValueCondition("bla", Equals, 2, QueryBuilder::WhereCondition); + mBuilders << qb; + bindVals.clear(); + bindVals << 2 << 1; + QTest::newRow("select with having and where") << mBuilders.count() << QString("SELECT foo FROM table WHERE ( bla = :0 ) GROUP BY id1, id2 HAVING ( bar = :1 AND asdf = yxcv AND ( col1 > col2 AND col1 <> NULL ) )") << bindVals; + + { + /// SELECT with JOINS + QueryBuilder qbTpl = QueryBuilder("table1", QueryBuilder::Select); + qbTpl.setDatabaseType(DbType::MySQL); + qbTpl.addColumn("col"); + bindVals.clear(); + + QueryBuilder qb = qbTpl; + qb.addJoin(QueryBuilder::InnerJoin, "table2", "table2.t1_id", "table1.id"); + qb.addJoin(QueryBuilder::LeftJoin, "table3", "table1.id", "table3.t1_id"); + mBuilders << qb; + QTest::newRow("select left join and inner join (different tables)") << mBuilders.count() + << QString("SELECT col FROM table1 INNER JOIN table2 ON ( table2.t1_id = table1.id ) LEFT JOIN table3 ON ( table1.id = table3.t1_id )") << bindVals; + + qb = qbTpl; + qb.addJoin(QueryBuilder::InnerJoin, "table2", "table2.t1_id", "table1.id"); + qb.addJoin(QueryBuilder::LeftJoin, "table2", "table2.t1_id", "table1.id"); + mBuilders << qb; + // join-condition too verbose but should not have any impact on speed + QTest::newRow("select left join and inner join (same table)") << mBuilders.count() + << QString("SELECT col FROM table1 INNER JOIN table2 ON ( table2.t1_id = table1.id AND ( table2.t1_id = table1.id ) )") << bindVals; + + // order of joins in the query should be the same as we add the joins in code + qb = qbTpl; + qb.addJoin(QueryBuilder::InnerJoin, "b_table", "b_table.t1_id", "table1.id"); + qb.addJoin(QueryBuilder::InnerJoin, "a_table", "a_table.b_id", "b_table.id"); + mBuilders << qb; + QTest::newRow("select join order") << mBuilders.count() + << QString("SELECT col FROM table1 INNER JOIN b_table ON ( b_table.t1_id = table1.id ) INNER JOIN a_table ON ( a_table.b_id = b_table.id )") << bindVals; + } + + { + /// SELECT with CASE + QueryBuilder qbTpl = QueryBuilder("table1", QueryBuilder::Select); + qbTpl.setDatabaseType(DbType::MySQL); + + QueryBuilder qb = qbTpl; + qb.addColumn("col"); + qb.addColumn(Query::Case("col1", Query::Greater, 42, "1", "0")); + bindVals.clear(); + bindVals << 42; + mBuilders << qb; + QTest::newRow("select case simple") << mBuilders.count() + << QString("SELECT col, CASE WHEN ( col1 > :0 ) THEN 1 ELSE 0 END FROM table1") << bindVals; + + qb = qbTpl; + qb.addAggregation("table1.col1", "sum"); + qb.addAggregation("table1.col2", "count"); + Query::Condition cond(Query::Or); + cond.addValueCondition("table3.col2", Query::Equals, "value1"); + cond.addValueCondition("table3.col2", Query::Equals, "value2"); + Query::Case caseStmt(cond, "1", "0"); + qb.addAggregation(caseStmt, "sum"); + qb.addJoin(QueryBuilder::LeftJoin, "table2", "table1.col3", "table2.col1"); + qb.addJoin(QueryBuilder::LeftJoin, "table3", "table2.col2", "table3.col1"); + bindVals.clear(); + bindVals << QString("value1") << QString("value2"); + mBuilders < :1 AND ( table2.t1_id = table1.id ) )") << bindVals; + + qb = qbTpl; + qb.setDatabaseType(DbType::PostgreSQL); + mBuilders << qb; + QTest::newRow("update inner join PSQL") << mBuilders.count() + << QString("UPDATE table1 SET col = :0 FROM table2 WHERE ( table2.answer <> :1 AND ( table2.t1_id = table1.id ) )") << bindVals; + + qb = qbTpl; + qb.setDatabaseType(DbType::Sqlite); + mBuilders << qb; + QTest::newRow("update inner join SQLite") << mBuilders.count() + << QString("UPDATE table1 SET col = :0 WHERE ( ( SELECT table2.answer FROM table2 WHERE ( ( table2.t1_id = table1.id ) ) ) <> :1 )") << bindVals; + + qb = qbTpl; + qb.setDatabaseType(DbType::Sqlite); + Query::Condition condition; + condition.addValueCondition("table2.col2", Query::Equals, 666); + condition.addValueCondition("table1.col3", Query::Equals, "text"); + qb.addCondition(condition); + qb.addValueCondition("table1.id", Query::Equals, 10); + mBuilders << qb; + bindVals << 666 << "text" << 10; + QTest::newRow("update inner join SQLite with subcondition") << mBuilders.count() + << QString("UPDATE table1 SET col = :0 WHERE ( ( SELECT table2.answer FROM table2 WHERE " + "( ( table2.t1_id = table1.id ) ) ) <> :1 AND " + "( ( SELECT table2.col2 FROM table2 WHERE ( ( table2.t1_id = table1.id ) ) ) = :2 AND table1.col3 = :3 ) AND " + "table1.id = :4 )") << bindVals; + + } +} + +void QueryBuilderTest::testQueryBuilder() +{ + QFETCH(int, qbId); + QFETCH(QString, sql); + QFETCH(QVector, bindValues); + + --qbId; + + QVERIFY(mBuilders[qbId].exec()); + QCOMPARE(mBuilders[qbId].mStatement, sql); + QCOMPARE(mBuilders[qbId].mBindValues, bindValues); +} + +void QueryBuilderTest::benchQueryBuilder() +{ + const QString table1 = QStringLiteral("Table1"); + const QString table2 = QStringLiteral("Table2"); + const QString table3 = QStringLiteral("Table3"); + const QString table1_id = QStringLiteral("Table1.id"); + const QString table2_id = QStringLiteral("Table2.id"); + const QString table3_id = QStringLiteral("Table3.id"); + const QString aggregate = QStringLiteral("COUNT"); + const QVariant value = QVariant::fromValue(QString("asdf")); + + const QStringList columns = QStringList() + << QStringLiteral("Table1.id") + << QStringLiteral("Table1.fooAsdf") + << QStringLiteral("Table2.barLala") + << QStringLiteral("Table3.xyzFsd"); + + bool executed = true; + + QBENCHMARK { + QueryBuilder builder(table1, QueryBuilder::Select); + builder.setDatabaseType(DbType::MySQL); + builder.addColumns(columns); + builder.addJoin(QueryBuilder::InnerJoin, table2, table2_id, table1_id); + builder.addJoin(QueryBuilder::LeftJoin, table3, table1_id, table3_id); + builder.addAggregation(columns.first(), aggregate); + builder.addColumnCondition(columns.at(1), Query::LessOrEqual, columns.last()); + builder.addValueCondition(columns.at(3), Query::Equals, value); + builder.addSortColumn(columns.at(2)); + builder.setLimit(10); + builder.addGroupColumn(columns.at(3)); + executed = executed && builder.exec(); + } + + QVERIFY(executed); +} diff --git a/autotests/server/querybuildertest.h b/autotests/server/querybuildertest.h new file mode 100644 index 0000000..b377198 --- /dev/null +++ b/autotests/server/querybuildertest.h @@ -0,0 +1,45 @@ +/* + 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_QUERYBUILDERTEST_H +#define AKONADI_QUERYBUILDERTEST_H + +#undef QT_NO_CAST_FROM_ASCII + +#include + +namespace Akonadi { +namespace Server { +class QueryBuilder; +} +} + +class QueryBuilderTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testQueryBuilder_data(); + void testQueryBuilder(); + void benchQueryBuilder(); + + private: + QList mBuilders; +}; + +#endif diff --git a/autotests/server/relationhandlertest.cpp b/autotests/server/relationhandlertest.cpp new file mode 100644 index 0000000..0497a04 --- /dev/null +++ b/autotests/server/relationhandlertest.cpp @@ -0,0 +1,414 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 + +#include "fakeakonadiserver.h" +#include "aktest.h" +#include "akdebug.h" +#include "entities.h" +#include "dbinitializer.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::Relation::List) +Q_DECLARE_METATYPE(Akonadi::Server::Relation) + +static Protocol::ChangeNotification::List extractNotifications(QSignalSpy *notificationSpy) +{ + Protocol::ChangeNotification::List receivedNotifications; + for (int q = 0; q < notificationSpy->size(); q++) { + //Only one notify call + if (notificationSpy->at(q).count() != 1) { + qWarning() << "Error: We're assuming only one notify call."; + return Protocol::ChangeNotification::List(); + } + const Protocol::ChangeNotification::List n = notificationSpy->at(q).first().value(); + for (int i = 0; i < n.size(); i++) { + // qDebug() << n.at(i); + receivedNotifications.append(n.at(i)); + } + } + return receivedNotifications; +} + +class RelationHandlerTest : public QObject +{ + Q_OBJECT + +public: + RelationHandlerTest() + : QObject() + { + qRegisterMetaType(); + + try { + FakeAkonadiServer::instance()->setPopulateDb(false); + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + + RelationType type; + type.setName(QLatin1String("type")); + type.insert(); + + RelationType type2; + type2.setName(QLatin1String("type2")); + type2.insert(); + } + + ~RelationHandlerTest() + { + FakeAkonadiServer::instance()->quit(); + } + + QScopedPointer initializer; + + Akonadi::Protocol::ChangeNotification relationNotification(const Akonadi::Protocol::ChangeNotification ¬ificationTemplate, const PimItem &item1, const PimItem &item2, const QByteArray &rid, const QByteArray &type = QByteArray("type")) + { + Akonadi::Protocol::ChangeNotification notification = notificationTemplate; + QSet itemParts; + itemParts << "LEFT " + QByteArray::number(item1.id()); + itemParts << "RIGHT " + QByteArray::number(item2.id()); + itemParts << "TYPE " + type; + itemParts << "RID " + rid; + notification.setItemParts(itemParts); + return notification; + } + +private Q_SLOTS: + void testStoreRelation_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + PimItem item1 = initializer->createItem("item1", col1); + PimItem item2 = initializer->createItem("item2", col1); + + QTest::addColumn("scenarios"); + QTest::addColumn("expectedRelations"); + QTest::addColumn("expectedNotifications"); + + Akonadi::Protocol::ChangeNotification notificationTemplate; + notificationTemplate.setType(Protocol::ChangeNotification::Relations); + notificationTemplate.setOperation(Protocol::ChangeNotification::Add); + notificationTemplate.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::ModifyRelationCommand(item1.id(), item2.id(), "type")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyRelationResponse()); + + Relation rel; + rel.setLeftId(item1.id()); + rel.setRightId(item2.id()); + RelationType type; + type.setName(QLatin1String("type")); + rel.setRelationType(type); + + Akonadi::Protocol::ChangeNotification itemNotification; + itemNotification.setType(Protocol::ChangeNotification::Items); + itemNotification.setOperation(Protocol::ChangeNotification::ModifyRelations); + itemNotification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + itemNotification.setResource("testresource"); + itemNotification.setParentCollection(col1.id()); + itemNotification.addEntity(item1.id(), item1.remoteId(), QString(), item1.mimeType().name()); + itemNotification.addEntity(item2.id(), item2.remoteId(), QString(), item2.mimeType().name()); + itemNotification.setAddedFlags(QSet() << "RELATION type " + QByteArray::number(item1.id()) + " " + QByteArray::number(item2.id())); + + Akonadi::Protocol::ChangeNotification notification = relationNotification(notificationTemplate, item1, item2, rel.remoteId().toLatin1()); + + QTest::newRow("uid create relation") << scenarios << (Relation::List() << rel) << (Protocol::ChangeNotification::List() << notification << itemNotification); + } + } + + void testStoreRelation() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Relation::List, expectedRelations); + QFETCH(Protocol::ChangeNotification::List, expectedNotifications); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + const Protocol::ChangeNotification::List receivedNotifications = extractNotifications(FakeAkonadiServer::instance()->notificationSpy()); + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < expectedNotifications.size(); i++) { + QCOMPARE(receivedNotifications.at(i), expectedNotifications.at(i)); + } + + const Relation::List relations = Relation::retrieveAll(); + // Q_FOREACH (const Relation &rel, relations) { + // akDebug() << rel.leftId() << rel.rightId(); + // } + QCOMPARE(relations.size(), expectedRelations.size()); + for (int i = 0; i < relations.size(); i++) { + QCOMPARE(relations.at(i).leftId(), expectedRelations.at(i).leftId()); + QCOMPARE(relations.at(i).rightId(), expectedRelations.at(i).rightId()); + // QCOMPARE(relations.at(i).typeId(), expectedRelations.at(i).typeId()); + QCOMPARE(relations.at(i).remoteId(), expectedRelations.at(i).remoteId()); + } + QueryBuilder qb(Relation::tableName(), QueryBuilder::Delete); + qb.exec(); + } + + void testRemoveRelation_data() + { + initializer.reset(new DbInitializer); + QCOMPARE(Relation::retrieveAll().size(), 0); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + PimItem item1 = initializer->createItem("item1", col1); + PimItem item2 = initializer->createItem("item2", col1); + + Relation rel; + rel.setLeftId(item1.id()); + rel.setRightId(item2.id()); + rel.setRelationType(RelationType::retrieveByName(QLatin1String("type"))); + QVERIFY(rel.insert()); + + Relation rel2; + rel2.setLeftId(item1.id()); + rel2.setRightId(item2.id()); + rel2.setRelationType(RelationType::retrieveByName(QLatin1String("type2"))); + QVERIFY(rel2.insert()); + + Akonadi::Protocol::ChangeNotification notificationTemplate; + notificationTemplate.setType(Protocol::ChangeNotification::Relations); + notificationTemplate.setOperation(Protocol::ChangeNotification::Remove); + notificationTemplate.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + QTest::addColumn("scenarios"); + QTest::addColumn("expectedRelations"); + QTest::addColumn("expectedNotifications"); + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::RemoveRelationsCommand(item1.id(), item2.id(), "type")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::RemoveRelationsResponse()); + + Akonadi::Protocol::ChangeNotification itemNotification; + itemNotification.setType(Protocol::ChangeNotification::Items); + itemNotification.setOperation(Protocol::ChangeNotification::ModifyRelations); + itemNotification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + itemNotification.setResource("testresource"); + itemNotification.setParentCollection(col1.id()); + itemNotification.addEntity(item1.id(), item1.remoteId(), QString(), item1.mimeType().name()); + itemNotification.addEntity(item2.id(), item2.remoteId(), QString(), item2.mimeType().name()); + itemNotification.setRemovedFlags(QSet() << "RELATION type " + QByteArray::number(item1.id()) + " " + QByteArray::number(item2.id())); + + Akonadi::Protocol::ChangeNotification notification = relationNotification(notificationTemplate, item1, item2, rel.remoteId().toLatin1()); + + QTest::newRow("uid remove relation") << scenarios << (Relation::List() << rel2) << (Protocol::ChangeNotification::List() << notification << itemNotification); + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::RemoveRelationsCommand(item1.id(), item2.id())) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::RemoveRelationsResponse()); + + Akonadi::Protocol::ChangeNotification itemNotification; + itemNotification.setType(Protocol::ChangeNotification::Items); + itemNotification.setOperation(Protocol::ChangeNotification::ModifyRelations); + itemNotification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + itemNotification.setResource("testresource"); + itemNotification.setParentCollection(col1.id()); + itemNotification.addEntity(item1.id(), item1.remoteId(), QString(), item1.mimeType().name()); + itemNotification.addEntity(item2.id(), item2.remoteId(), QString(), item2.mimeType().name()); + itemNotification.setRemovedFlags(QSet() << "RELATION type2 " + QByteArray::number(item1.id()) + " " + QByteArray::number(item2.id())); + + Akonadi::Protocol::ChangeNotification notification = relationNotification(notificationTemplate, item1, item2, rel.remoteId().toLatin1(), "type2"); + + QTest::newRow("uid remove relation without type") << scenarios << Relation::List() << (Protocol::ChangeNotification::List() << notification << itemNotification); + } + + } + + void testRemoveRelation() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Relation::List, expectedRelations); + QFETCH(Protocol::ChangeNotification::List, expectedNotifications); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + const Protocol::ChangeNotification::List receivedNotifications = extractNotifications(FakeAkonadiServer::instance()->notificationSpy()); + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < expectedNotifications.size(); i++) { + QCOMPARE(receivedNotifications.at(i), expectedNotifications.at(i)); + } + + const Relation::List relations = Relation::retrieveAll(); + // Q_FOREACH (const Relation &rel, relations) { + // akDebug() << rel.leftId() << rel.rightId() << rel.relationType().name() << rel.remoteId(); + // } + QCOMPARE(relations.size(), expectedRelations.size()); + for (int i = 0; i < relations.size(); i++) { + QCOMPARE(relations.at(i).leftId(), expectedRelations.at(i).leftId()); + QCOMPARE(relations.at(i).rightId(), expectedRelations.at(i).rightId()); + QCOMPARE(relations.at(i).typeId(), expectedRelations.at(i).typeId()); + QCOMPARE(relations.at(i).remoteId(), expectedRelations.at(i).remoteId()); + } + } + + void testListRelation_data() + { + QueryBuilder qb(Relation::tableName(), QueryBuilder::Delete); + qb.exec(); + + initializer.reset(new DbInitializer); + QCOMPARE(Relation::retrieveAll().size(), 0); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + PimItem item1 = initializer->createItem("item1", col1); + PimItem item2 = initializer->createItem("item2", col1); + PimItem item3 = initializer->createItem("item3", col1); + PimItem item4 = initializer->createItem("item4", col1); + + Relation rel; + rel.setLeftId(item1.id()); + rel.setRightId(item2.id()); + rel.setRelationType(RelationType::retrieveByName(QLatin1String("type"))); + rel.setRemoteId(QLatin1String("foobar1")); + QVERIFY(rel.insert()); + + Relation rel2; + rel2.setLeftId(item1.id()); + rel2.setRightId(item2.id()); + rel2.setRelationType(RelationType::retrieveByName(QLatin1String("type2"))); + rel2.setRemoteId(QLatin1String("foobar2")); + QVERIFY(rel2.insert()); + + Relation rel3; + rel3.setLeftId(item3.id()); + rel3.setRightId(item4.id()); + rel3.setRelationType(RelationType::retrieveByName(QLatin1String("type"))); + rel3.setRemoteId(QLatin1String("foobar3")); + QVERIFY(rel3.insert()); + + Relation rel4; + rel4.setLeftId(item4.id()); + rel4.setRightId(item3.id()); + rel4.setRelationType(RelationType::retrieveByName(QLatin1String("type"))); + rel4.setRemoteId(QLatin1String("foobar4")); + QVERIFY(rel4.insert()); + + QTest::addColumn("scenarios"); + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommand(-1, { "type" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item1.id(), item1.mimeType().name().toUtf8(), item2.id(), item2.mimeType().name().toUtf8(), { "type" }, "foobar1")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item3.id(), item3.mimeType().name().toUtf8(), item4.id(), item4.mimeType().name().toUtf8(), { "type" }, "foobar3")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item4.id(), item4.mimeType().name().toUtf8(), item3.id(), item3.mimeType().name().toUtf8(), { "type" }, "foobar4")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse()); + + QTest::newRow("filter by type") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommand()) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item1.id(), item1.mimeType().name().toUtf8(), item2.id(), item2.mimeType().name().toUtf8(), { "type" }, "foobar1")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item1.id(), item1.mimeType().name().toUtf8(), item2.id(), item2.mimeType().name().toUtf8(), { "type2" }, "foobar2")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item3.id(), item3.mimeType().name().toUtf8(), item4.id(), item4.mimeType().name().toUtf8(), { "type" }, "foobar3")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item4.id(), item4.mimeType().name().toUtf8(), item3.id(), item3.mimeType().name().toUtf8(), { "type" }, "foobar4")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse()); + + QTest::newRow("no filter") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommand(-1, {}, QLatin1String("testresource"))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item1.id(), item1.mimeType().name().toUtf8(), item2.id(), item2.mimeType().name().toUtf8(), { "type" }, "foobar1")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item1.id(), item1.mimeType().name().toUtf8(), item2.id(), item2.mimeType().name().toUtf8(), { "type2" }, "foobar2")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item3.id(), item3.mimeType().name().toUtf8(), item4.id(), item4.mimeType().name().toUtf8(), { "type" }, "foobar3")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item4.id(), item4.mimeType().name().toUtf8(), item3.id(), item3.mimeType().name().toUtf8(), { "type" }, "foobar4")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse()); + + QTest::newRow("filter by resource with matching resource") << scenarios; + } + { + + Resource res; + res.setName(QLatin1String("testresource2")); + res.insert(); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommand(-1, {}, QLatin1String("testresource2"))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse()); + + QTest::newRow("filter by resource with nonmatching resource") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommand(item1.id(), -1, { "type" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item1.id(), item1.mimeType().name().toUtf8(), item2.id(), item2.mimeType().name().toUtf8(), "type", "foobar1")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse()); + + QTest::newRow("filter by left and type") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommand(-1, item2.id(), { "type" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item1.id(), item1.mimeType().name().toUtf8(), item2.id(), item2.mimeType().name().toUtf8(), "type", "foobar1")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse()); + + QTest::newRow("filter by right and type") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommand(item3.id(), { "type" })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item3.id(), item3.mimeType().name().toUtf8(), item4.id(), item4.mimeType().name().toUtf8(), "type", "foobar3")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse(item4.id(), item4.mimeType().name().toUtf8(), item3.id(), item3.mimeType().name().toUtf8(), "type", "foobar4")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponse()); + + QTest::newRow("fetch by side with typefilter") << scenarios; + } + } + + void testListRelation() + { + QFETCH(TestScenario::List, scenarios); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + } + +}; + +AKTEST_FAKESERVER_MAIN(RelationHandlerTest) + +#include "relationhandlertest.moc" diff --git a/autotests/server/searchtest.cpp b/autotests/server/searchtest.cpp new file mode 100644 index 0000000..6bba52e --- /dev/null +++ b/autotests/server/searchtest.cpp @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "fakeakonadiserver.h" +#include "handler/searchhelper.h" +#include "akdebug.h" +#include "aktest.h" + +#include + +#include + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QList) + +class SearchTest : public QObject +{ + Q_OBJECT + +public: + SearchTest() + : QObject() + { + try { + FakeAkonadiServer::instance()->setPopulateDb(false); + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + } + + ~SearchTest() + { + FakeAkonadiServer::instance()->quit(); + } + + Collection createCollection(const Resource &res, const QString &name, const Collection &parent, const QStringList &mimetypes) + { + Collection col; + col.setName(name); + col.setResource(res); + col.setParentId(parent.isValid() ? parent.id() : 0); + col.insert(); + Q_FOREACH (const QString &mimeType, mimetypes) { + MimeType mt = MimeType::retrieveByName(mimeType); + if (!mt.isValid()) { + mt = MimeType(mimeType); + mt.insert(); + } + col.addMimeType(mt); + } + return col; + } + +private Q_SLOTS: + void testSearchHelperCollectionListing_data() + { + /* + Fake Resource + |- Col 1 (inode/directory) + | |- Col 2 (inode/direcotry, application/octet-stream) + | | |- Col 3(application/octet-stream) + | |- Col 4 (text/plain) + |- Col 5 (inode/directory, text/plain) + |- Col 6 (inode/directory, application/octet-stream) + |- Col 7 (inode/directory, text/plain) + |- Col 8 (inode/directory, application/octet-stream) + |- Col 9 (unique/mime-type) + */ + + Resource res(QLatin1String("Test Resource"), false); + res.insert(); + + Collection col1 = createCollection(res, QLatin1String("Col 1"), Collection(), + QStringList() << QLatin1String("inode/directory")); + Collection col2 = createCollection(res, QLatin1String("Col 2"), col1, + QStringList() << QLatin1String("inode/directory") + << QLatin1String("application/octet-stream")); + Collection col3 = createCollection(res, QLatin1String("Col 3"), col2, + QStringList() << QLatin1String("application/octet-stream")); + Collection col4 = createCollection(res, QLatin1String("Col 4"), col2, + QStringList() << QLatin1String("text/plain")); + Collection col5 = createCollection(res, QLatin1String("Col 5"), Collection(), + QStringList() << QLatin1String("inode/directory") + << QLatin1String("text/plain")); + Collection col6 = createCollection(res, QLatin1String("Col 6"), col5, + QStringList() << QLatin1String("inode/directory") + << QLatin1String("application/octet-stream")); + Collection col7 = createCollection(res, QLatin1String("Col 7"), col5, + QStringList() << QLatin1String("inode/directory") + << QLatin1String("text/plain")); + Collection col8 = createCollection(res, QLatin1String("Col 8"), col7, + QStringList() << QLatin1String("text/directory") + << QLatin1String("application/octet-stream")); + Collection col9 = createCollection(res, QLatin1String("Col 9"), col8, + QStringList() << QLatin1String("unique/mime-type")); + + QTest::addColumn>("ancestors"); + QTest::addColumn("mimetypes"); + QTest::addColumn>("expectedResults"); + + QTest::newRow("") << (QVector() << 0) + << (QStringList() << QLatin1String("text/plain")) + << (QVector() << col4.id() << col5.id() << col7.id()); + QTest::newRow("") << (QVector() << 0) + << (QStringList() << QLatin1String("application/octet-stream")) + << (QVector() << col2.id() << col3.id() << col6.id() << col8.id()); + QTest::newRow("") << (QVector() << col1.id()) + << (QStringList() << QLatin1String("text/plain")) + << (QVector() << col4.id()); + QTest::newRow("") << (QVector() << col1.id()) + << (QStringList() << QLatin1String("unique/mime-type")) + << QVector(); + QTest::newRow("") << (QVector() << col2.id() << col7.id()) + << (QStringList() << QLatin1String("application/octet-stream")) + << (QVector() << col3.id() << col8.id()); + } + + void testSearchHelperCollectionListing() + { + QFETCH(QVector, ancestors); + QFETCH(QStringList, mimetypes); + QFETCH(QVector, expectedResults); + + QVector results = SearchHelper::matchSubcollectionsByMimeType(ancestors, mimetypes); + + qSort(expectedResults); + qSort(results); + + QCOMPARE(results.size(), expectedResults.size()); + QCOMPARE(results, expectedResults); + } + +}; + +AKTEST_FAKESERVER_MAIN(SearchTest) + +#include "searchtest.moc" diff --git a/autotests/server/taghandlertest.cpp b/autotests/server/taghandlertest.cpp new file mode 100644 index 0000000..c051c18 --- /dev/null +++ b/autotests/server/taghandlertest.cpp @@ -0,0 +1,472 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 + +#include "fakeakonadiserver.h" +#include "aktest.h" +#include "akdebug.h" +#include "entities.h" +#include "dbinitializer.h" + +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +typedef QPair TagTagAttributeListPair; + +Q_DECLARE_METATYPE(Akonadi::Server::Tag::List) +Q_DECLARE_METATYPE(Akonadi::Server::Tag) +Q_DECLARE_METATYPE(QVector) + +static Protocol::ChangeNotification::List extractNotifications(QSignalSpy *notificationSpy) +{ + Protocol::ChangeNotification::List receivedNotifications; + for (int q = 0; q < notificationSpy->size(); q++) { + //Only one notify call + if (notificationSpy->at(q).count() != 1) { + qWarning() << "Error: We're assuming only one notify call."; + return Protocol::ChangeNotification::List(); + } + const Protocol::ChangeNotification::List n = notificationSpy->at(q).first().value(); + for (int i = 0; i < n.size(); i++) { + // qDebug() << n.at(i); + receivedNotifications.append(n.at(i)); + } + } + return receivedNotifications; +} + +class TagHandlerTest : public QObject +{ + Q_OBJECT + +public: + TagHandlerTest() + : QObject() + { + qRegisterMetaType(); + + try { + FakeAkonadiServer::instance()->setPopulateDb(false); + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + akError() << "Server exception: " << e.what(); + akFatal() << "Fake Akonadi Server failed to start up, aborting test"; + } + } + + ~TagHandlerTest() + { + FakeAkonadiServer::instance()->quit(); + } + + Protocol::FetchTagsResponse createResponse(const Tag &tag, const QByteArray &remoteId = QByteArray(), + const Protocol::Attributes &attrs = Protocol::Attributes()) + { + Protocol::FetchTagsResponse resp(tag.id()); + resp.setGid(tag.gid().toUtf8()); + resp.setParentId(tag.parentId()); + resp.setType(tag.tagType().name().toUtf8()); + resp.setRemoteId(remoteId); + resp.setAttributes(attrs); + return resp; + } + + QScopedPointer initializer; + +private Q_SLOTS: + void testStoreTag_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + + // Make sure the type exists + TagType type = type.retrieveByName(QStringLiteral("PLAIN")); + if (!type.isValid()) { + type.setName(QStringLiteral("PLAIN")); + type.insert(); + } + + QTest::addColumn("scenarios"); + QTest::addColumn>>("expectedTags"); + QTest::addColumn("expectedNotifications"); + + { + Protocol::CreateTagCommand cmd; + cmd.setGid("tag"); + cmd.setParentId(0); + cmd.setType("PLAIN"); + cmd.setAttributes({ { "TAG", "(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")" } }); + + Protocol::FetchTagsResponse resp(1); + resp.setGid(cmd.gid()); + resp.setParentId(cmd.parentId()); + resp.setType(cmd.type()); + resp.setAttributes(cmd.attributes()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateTagResponse()); + + Tag tag; + tag.setId(1); + tag.setTagType(type); + tag.setParentId(0); + + TagAttribute attribute; + attribute.setTagId(1); + attribute.setType("TAG"); + attribute.setValue("(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")"); + + Akonadi::Protocol::ChangeNotification notification; + notification.setType(Protocol::ChangeNotification::Tags); + notification.setOperation(Protocol::ChangeNotification::Add); + notification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + notification.addEntity(1); + + QTest::newRow("uid create relation") << scenarios + << QVector{ { tag, { attribute } } } + << Protocol::ChangeNotification::List{ notification }; + } + + { + Protocol::CreateTagCommand cmd; + cmd.setGid("tag2"); + cmd.setParentId(1); + cmd.setType("PLAIN"); + cmd.setAttributes({ { "TAG", "(\\\"tag3\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")" } }); + + Protocol::FetchTagsResponse resp(2); + resp.setGid(cmd.gid()); + resp.setParentId(cmd.parentId()); + resp.setType(cmd.type()); + resp.setAttributes(cmd.attributes()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateTagResponse()); + + Tag tag; + tag.setId(2); + tag.setTagType(type); + tag.setParentId(1); + + TagAttribute attribute; + attribute.setTagId(2); + attribute.setType("TAG"); + attribute.setValue("(\\\"tag3\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")"); + + Akonadi::Protocol::ChangeNotification notification; + notification.setType(Protocol::ChangeNotification::Tags); + notification.setOperation(Protocol::ChangeNotification::Add); + notification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + notification.addEntity(2); + + QTest::newRow("create child tag") << scenarios + << QVector{ { tag, { attribute } } } + << Protocol::ChangeNotification::List{ notification }; + } + } + + void testStoreTag() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(QVector, expectedTags); + QFETCH(Protocol::ChangeNotification::List, expectedNotifications); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + const Protocol::ChangeNotification::List receivedNotifications = extractNotifications(FakeAkonadiServer::instance()->notificationSpy()); + + QVariantList ids; + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < expectedNotifications.size(); i++) { + QCOMPARE(receivedNotifications.at(i), expectedNotifications.at(i)); + Q_FOREACH (qint64 id, receivedNotifications.at(i).entities().keys()) { + ids << id; + } + } + + SelectQueryBuilder qb; + qb.addValueCondition(Tag::idColumn(), Query::In, ids); + QVERIFY(qb.exec()); + const Tag::List tags = qb.result(); + QCOMPARE(tags.size(), expectedTags.size()); + for (int i = 0; i < tags.size(); i++) { + const Tag actual = tags.at(i); + const Tag expected = expectedTags.at(i).first; + const TagAttribute::List expectedAttrs = expectedTags.at(i).second; + + QCOMPARE(actual.id(), expected.id()); + QCOMPARE(actual.typeId(), expected.typeId()); + QCOMPARE(actual.parentId(), expected.parentId()); + + TagAttribute::List attributes = TagAttribute::retrieveFiltered( + TagAttribute::tagIdColumn(), tags.at(i).id()); + QCOMPARE(attributes.size(), expectedAttrs.size()); + for (int j = 0; j < attributes.size(); ++j) { + const TagAttribute actualAttr = attributes.at(i); + const TagAttribute expectedAttr = expectedAttrs.at(i); + + QCOMPARE(actualAttr.tagId(), expectedAttr.tagId()); + QCOMPARE(actualAttr.type(), expectedAttr.type()); + QCOMPARE(actualAttr.value(), expectedAttr.value()); + } + } + } + + void testModifyTag_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Resource res2 = initializer->createResource("testresource2"); + Collection col = initializer->createCollection("Col 1"); + PimItem pimItem = initializer->createItem("Item 1", col); + + Tag tag; + TagType type; + type.setName(QStringLiteral("PLAIN")); + type.insert(); + tag.setTagType(type); + tag.setGid(QStringLiteral("gid")); + tag.insert(); + + pimItem.addTag(tag); + + TagRemoteIdResourceRelation rel; + rel.setRemoteId(QStringLiteral("TAG1RES2RID")); + rel.setResource(res2); + rel.setTag(tag); + rel.insert(); + + QTest::addColumn("scenarios"); + QTest::addColumn("expectedTags"); + QTest::addColumn("expectedNotifications"); + { + Protocol::ModifyTagCommand cmd(tag.id()); + cmd.setAttributes({ { "TAG", "(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")" } }); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(tag, QByteArray(), cmd.attributes())) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyTagResponse()); + + Akonadi::Protocol::ChangeNotification notification; + notification.setType(Protocol::ChangeNotification::Tags); + notification.setOperation(Protocol::ChangeNotification::Modify); + notification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + notification.addEntity(tag.id()); + + QTest::newRow("uid store name") << scenarios << (Tag::List() << tag) << (Protocol::ChangeNotification::List() << notification); + } + + { + Protocol::ModifyTagCommand cmd(tag.id()); + cmd.setRemoteId("remote1"); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << FakeAkonadiServer::selectResourceScenario(QStringLiteral("testresource")) + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(tag, "remote1", + { { "TAG", "(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")" } })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyTagResponse()); + + // RID-only changes don't emit notifications + /* + Akonadi::Protocol::ChangeNotification notification; + notification.setType(Protocol::ChangeNotification::Tags); + notification.setOperation(Protocol::ChangeNotification::Modify); + notification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + notification.addEntity(tag.id()); + */ + + QTest::newRow("uid store rid") << scenarios << (Tag::List() << tag) << Protocol::ChangeNotification::List(); + } + + { + Protocol::ModifyTagCommand cmd(tag.id()); + cmd.setRemoteId(QByteArray()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << FakeAkonadiServer::selectResourceScenario(res.name()) + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(tag, QByteArray(), + { { "TAG", "(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")" } })) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyTagResponse()); + + // RID-only changes don't emit notifications + /* + Akonadi::Protocol::ChangeNotification tagChangeNtf; + tagChangeNtf.setType(Protocol::ChangeNotification::Tags); + tagChangeNtf.setOperation(Protocol::ChangeNotification::Modify); + tagChangeNtf.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + tagChangeNtf.addEntity(tag.id()); + */ + + QTest::newRow("uid store unset one rid") << scenarios << (Tag::List() << tag) << Protocol::ChangeNotification::List(); + } + + { + Protocol::ModifyTagCommand cmd(tag.id()); + cmd.setRemoteId(QByteArray()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << FakeAkonadiServer::selectResourceScenario(res2.name()) + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::DeleteTagResponse()) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyTagResponse()); + + Akonadi::Protocol::ChangeNotification itemUntaggedNtf; + itemUntaggedNtf.setType(Protocol::ChangeNotification::Items); + itemUntaggedNtf.setOperation(Protocol::ChangeNotification::ModifyTags); + itemUntaggedNtf.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + itemUntaggedNtf.addEntity(pimItem.id(), pimItem.remoteId(), QString(), pimItem.mimeType().name()); + itemUntaggedNtf.setResource(res2.name().toLatin1()); + itemUntaggedNtf.setParentCollection(col.id()); + itemUntaggedNtf.setRemovedTags(QSet() << tag.id()); + + Akonadi::Protocol::ChangeNotification tagRemoveNtf; + tagRemoveNtf.setType(Protocol::ChangeNotification::Tags); + tagRemoveNtf.setOperation(Protocol::ChangeNotification::Remove); + tagRemoveNtf.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + tagRemoveNtf.addEntity(tag.id()); + + QTest::newRow("uid store unset last rid") << scenarios << Tag::List() << (Protocol::ChangeNotification::List() << itemUntaggedNtf << tagRemoveNtf); + } + } + + void testModifyTag() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Tag::List, expectedTags); + QFETCH(Protocol::ChangeNotification::List, expectedNotifications); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + const Protocol::ChangeNotification::List receivedNotifications = extractNotifications(FakeAkonadiServer::instance()->notificationSpy()); + + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < receivedNotifications.size(); i++) { + QCOMPARE(receivedNotifications.at(i), expectedNotifications.at(i)); + } + + const Tag::List tags = Tag::retrieveAll(); + QCOMPARE(tags.size(), expectedTags.size()); + for (int i = 0; i < tags.size(); i++) { + QCOMPARE(tags.at(i).id(), expectedTags.at(i).id()); + QCOMPARE(tags.at(i).tagType().name(), expectedTags.at(i).tagType().name()); + } + } + + void testRemoveTag_data() + { + initializer.reset(new DbInitializer); + Resource res1 = initializer->createResource("testresource3"); + Resource res2 = initializer->createResource("testresource4"); + + Tag tag; + TagType type; + type.setName(QStringLiteral("PLAIN")); + type.insert(); + tag.setTagType(type); + tag.setGid(QStringLiteral("gid2")); + tag.insert(); + + TagRemoteIdResourceRelation rel1; + rel1.setRemoteId(QStringLiteral("TAG2RES1RID")); + rel1.setResource(res1); + rel1.setTag(tag); + rel1.insert(); + + TagRemoteIdResourceRelation rel2; + rel2.setRemoteId(QStringLiteral("TAG2RES2RID")); + rel2.setResource(res2); + rel2.setTag(tag); + rel2.insert(); + + QTest::addColumn("scenarios"); + QTest::addColumn("expectedTags"); + QTest::addColumn("expectedNotifications"); + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::DeleteTagCommand(tag.id())) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::DeleteTagResponse()); + + Akonadi::Protocol::ChangeNotification ntf; + ntf.setType(Protocol::ChangeNotification::Tags); + ntf.setOperation(Protocol::ChangeNotification::Remove); + ntf.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + Akonadi::Protocol::ChangeNotification res1Ntf = ntf; + res1Ntf.addEntity(tag.id(), rel1.remoteId()); + res1Ntf.setResource(res1.name().toLatin1()); + + Akonadi::Protocol::ChangeNotification res2Ntf = ntf; + res2Ntf.addEntity(tag.id(), rel2.remoteId()); + res2Ntf.setResource(res2.name().toLatin1()); + + Akonadi::Protocol::ChangeNotification clientNtf = ntf; + clientNtf.addEntity(tag.id()); + + QTest::newRow("uid remove") << scenarios << Tag::List() << (Protocol::ChangeNotification::List() << res1Ntf << res2Ntf << clientNtf); + } + } + + void testRemoveTag() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Tag::List, expectedTags); + QFETCH(Akonadi::Protocol::ChangeNotification::List, expectedNotifications); + + FakeAkonadiServer::instance()->setScenarios(scenarios); + FakeAkonadiServer::instance()->runTest(); + + const Protocol::ChangeNotification::List receivedNotifications = extractNotifications(FakeAkonadiServer::instance()->notificationSpy()); + + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < receivedNotifications.size(); i++) { + QCOMPARE(receivedNotifications.at(i), expectedNotifications.at(i)); + } + + const Tag::List tags = Tag::retrieveAll(); + QCOMPARE(tags.size(), 0); + } +}; + +AKTEST_FAKESERVER_MAIN(TagHandlerTest) + +#include "taghandlertest.moc" diff --git a/cmake/modules/AkonadiMacros.cmake b/cmake/modules/AkonadiMacros.cmake new file mode 100644 index 0000000..247fede --- /dev/null +++ b/cmake/modules/AkonadiMacros.cmake @@ -0,0 +1,25 @@ + +macro(akonadi_generate_schema _schemaXml _className _fileBaseName) +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_fileBaseName}.h + ${CMAKE_CURRENT_BINARY_DIR}/${_fileBaseName}.cpp + COMMAND ${XSLTPROC_EXECUTABLE} + --output ${CMAKE_CURRENT_BINARY_DIR}/${_fileBaseName}.h + --stringparam code header + --stringparam className ${_className} + --stringparam fileName ${_fileBaseName} + ${Akonadi_SOURCE_DIR}/src/server/storage/schema.xsl + ${_schemaXml} + COMMAND ${XSLTPROC_EXECUTABLE} + --output ${CMAKE_CURRENT_BINARY_DIR}/${_fileBaseName}.cpp + --stringparam code source + --stringparam className ${_className} + --stringparam fileName ${_fileBaseName} + ${Akonadi_SOURCE_DIR}/src/server/storage/schema.xsl + ${_schemaXml} + DEPENDS ${Akonadi_SOURCE_DIR}/src/server/storage/schema.xsl + ${Akonadi_SOURCE_DIR}/src/server/storage/schema-header.xsl + ${Akonadi_SOURCE_DIR}/src/server/storage/schema-source.xsl + ${_schemaXml} +) +endmacro() diff --git a/cmake/modules/COPYING-CMAKE-SCRIPTS b/cmake/modules/COPYING-CMAKE-SCRIPTS new file mode 100644 index 0000000..4b41776 --- /dev/null +++ b/cmake/modules/COPYING-CMAKE-SCRIPTS @@ -0,0 +1,22 @@ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmake/modules/FindASan.cmake b/cmake/modules/FindASan.cmake new file mode 100644 index 0000000..bf608d4 --- /dev/null +++ b/cmake/modules/FindASan.cmake @@ -0,0 +1,67 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2013 Matthew Arsenault +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +include(CheckCCompilerFlag) + +# Set -Werror to catch "argument unused during compilation" warnings +set(CMAKE_REQUIRED_FLAGS "-Werror -faddress-sanitizer") # Also needs to be a link flag for test to pass +check_c_compiler_flag("-faddress-sanitizer" HAVE_FLAG_ADDRESS_SANITIZER) + +set(CMAKE_REQUIRED_FLAGS "-Werror -fsanitize=address") # Also needs to be a link flag for test to pass +check_c_compiler_flag("-fsanitize=address" HAVE_FLAG_SANITIZE_ADDRESS) + +unset(CMAKE_REQUIRED_FLAGS) + +if(HAVE_FLAG_SANITIZE_ADDRESS) + # Clang 3.2+ use this version + set(ADDRESS_SANITIZER_FLAG "-fsanitize=address") +elseif(HAVE_FLAG_ADDRESS_SANITIZER) + # Older deprecated flag for ASan + set(ADDRESS_SANITIZER_FLAG "-faddress-sanitizer") +endif() + + +if(NOT ADDRESS_SANITIZER_FLAG) + return() +endif() + +message(STATUS "Detected ASan (${ADDRESS_SANITIZER_FLAG})") + +set(CMAKE_C_FLAGS_ASAN "-O1 -g ${ADDRESS_SANITIZER_FLAG} -fno-omit-frame-pointer -fno-optimize-sibling-calls" + CACHE STRING "Flags used by the C compiler during ASan builds." + FORCE) +set(CMAKE_CXX_FLAGS_ASAN "-O1 -g ${ADDRESS_SANITIZER_FLAG} -fno-omit-frame-pointer -fno-optimize-sibling-calls" + CACHE STRING "Flags used by the C++ compiler during ASan builds." + FORCE) +set(CMAKE_EXE_LINKER_FLAGS_ASAN "${ADDRESS_SANITIZER_FLAG}" + CACHE STRING "Flags used for linking binaries during ASan builds." + FORCE) +set(CMAKE_SHARED_LINKER_FLAGS_ASAN "${ADDRESS_SANITIZER_FLAG}" + CACHE STRING "Flags used by the shared libraries linker during ASan builds." + FORCE) +mark_as_advanced(CMAKE_C_FLAGS_ASAN + CMAKE_CXX_FLAGS_ASAN + CMAKE_EXE_LINKER_FLAGS_ASAN + CMAKE_SHARED_LINKER_FLAGS_ASAN) +set(ASAN_FOUND TRUE) diff --git a/cmake/modules/FindBacktrace.cmake b/cmake/modules/FindBacktrace.cmake new file mode 100644 index 0000000..0af8d1b --- /dev/null +++ b/cmake/modules/FindBacktrace.cmake @@ -0,0 +1,84 @@ +# - Find provider for backtrace(3) +# Checks if OS supports backtrace(3) via either libc or custom library. +# This module defines the following variables: +# Backtrace_HEADER - The header file needed for backtrace(3). Cached. +# Could be forcibly set by user. +# Backtrace_INCLUDE_DIRS - The include directories needed to use backtrace(3) header. +# Backtrace_LIBRARIES - The libraries (linker flags) needed to use backtrace(3), if any. +# Backtrace_FOUND - Is set if and only if backtrace(3) support detected. +# +# The following cache variables are also available to set or use: +# Backtrace_LIBRARY - The external library providing backtrace, if any. +# Backtrace_INCLUDE_DIR - The directory holding the backtrace(3) header. +# +# Typical usage is to generate of header file using configure_file() with the +# contents like the following: +# #cmakedefine01 Backtrace_FOUND +# #if Backtrace_FOUND +# # include <${Backtrace_HEADER}> +# #endif +# And then reference that generated header file in actual source. + +#============================================================================= +# Copyright (c) 2013, Vadim Zhukov +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +include(CMakePushCheckState) +include(CheckSymbolExists) +include(FindPackageHandleStandardArgs) + +# List of variables to be provided to find_package_handle_standard_args() +set(_Backtrace_STD_ARGS Backtrace_INCLUDE_DIR) + +if(Backtrace_HEADER) + set(_Backtrace_HEADER_TRY "${Backtrace_HEADER}") +else(Backtrace_HEADER) + set(_Backtrace_HEADER_TRY "execinfo.h") +endif(Backtrace_HEADER) + +find_path(Backtrace_INCLUDE_DIR "${_Backtrace_HEADER_TRY}") +set(Backtrace_INCLUDE_DIRS ${Backtrace_INCLUDE_DIR}) + +# First, check if we already have backtrace(), e.g., in libc +cmake_push_check_state(RESET) +set(CMAKE_REQUIRED_INCLUDES ${Backtrace_INCLUDE_DIRS}) +check_symbol_exists("backtrace" "${_Backtrace_HEADER_TRY}" _Backtrace_SYM_FOUND) +cmake_pop_check_state() + +if(_Backtrace_SYM_FOUND) + set(Backtrace_LIBRARY) + if(NOT Backtrace_FIND_QUIETLY) + message(STATUS "backtrace facility detected in default set of libraries") + endif() +else() + # Check for external library, for non-glibc systems + if(Backtrace_INCLUDE_DIR) + # OpenBSD has libbacktrace renamed to libexecinfo + find_library(Backtrace_LIBRARY "execinfo") + elseif() # respect user wishes + set(_Backtrace_HEADER_TRY "backtrace.h") + find_path(Backtrace_INCLUDE_DIR ${_Backtrace_HEADER_TRY}) + find_library(Backtrace_LIBRARY "backtrace") + endif() + + # Prepend list with library path as it's more common practice + set(_Backtrace_STD_ARGS Backtrace_LIBRARY ${_Backtrace_STD_ARGS}) +endif() + +set(Backtrace_LIBRARIES ${Backtrace_LIBRARY}) +set(Backtrace_HEADER "${_Backtrace_HEADER_TRY}" CACHE STRING "Header providing backtrace(3) facility") + +find_package_handle_standard_args(Backtrace FOUND_VAR Backtrace_FOUND REQUIRED_VARS ${_Backtrace_STD_ARGS}) +mark_as_advanced(Backtrace_HEADER Backtrace_INCLUDE_DIR Backtrace_LIBRARY) diff --git a/cmake/modules/FindSqlite.cmake b/cmake/modules/FindSqlite.cmake new file mode 100644 index 0000000..c43a7b5 --- /dev/null +++ b/cmake/modules/FindSqlite.cmake @@ -0,0 +1,113 @@ +# - Try to find Sqlite +# Once done this will define +# +# SQLITE_FOUND - system has Sqlite +# SQLITE_INCLUDE_DIR - the Sqlite include directory +# SQLITE_LIBRARIES - Link these to use Sqlite +# SQLITE_MIN_VERSION - The minimum SQLite version +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. +# +# Copyright (c) 2008, Gilles Caulier, +# Copyright (c) 2010, Christophe Giboudeaux, +# Copyright (c) 2014, Daniel Vrátil +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +if(NOT SQLITE_MIN_VERSION) + set(SQLITE_MIN_VERSION "3.6.16") +endif(NOT SQLITE_MIN_VERSION) + +if ( SQLITE_INCLUDE_DIR AND SQLITE_LIBRARIES ) + # in cache already + SET(Sqlite_FIND_QUIETLY TRUE) +endif ( SQLITE_INCLUDE_DIR AND SQLITE_LIBRARIES ) + +# use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +if( NOT WIN32 ) + find_package(PkgConfig) + + pkg_check_modules(PC_SQLITE sqlite3) + + set(SQLITE_DEFINITIONS ${PC_SQLITE_CFLAGS_OTHER}) +endif( NOT WIN32 ) + +if(PC_SQLITE_FOUND) + find_path(SQLITE_INCLUDE_DIR + NAMES sqlite3.h + PATHS ${PC_SQLITE_INCLUDEDIR} + NO_DEFAULT_PATH + ) + + find_library(SQLITE_LIBRARIES + NAMES sqlite3 + PATHS ${PC_SQLITE_LIBDIR} + NO_DEFAULT_PATH + ) +else(PC_SQLITE_FOUND) + find_path(SQLITE_INCLUDE_DIR + NAMES sqlite3.h + ) + + find_library(SQLITE_LIBRARIES + NAMES sqlite3 + ) +endif(PC_SQLITE_FOUND) + +if( UNIX ) + find_file(SQLITE_STATIC_LIBRARIES + libsqlite3.a + ${PC_SQLITE_LIBDIR} + ) +else( UNIX ) + # todo find static libs for other systems + # fallback to standard libs + set( SQLITE_STATIC_LIBRARIES ${SQLITE_LIBRARIES} ) +endif( UNIX ) + +if(EXISTS ${SQLITE_INCLUDE_DIR}/sqlite3.h) + file(READ ${SQLITE_INCLUDE_DIR}/sqlite3.h SQLITE3_H_CONTENT) + string(REGEX MATCH "SQLITE_VERSION[ ]*\"[0-9.]*\"\n" SQLITE_VERSION_MATCH "${SQLITE3_H_CONTENT}") + + if(SQLITE_VERSION_MATCH) + string(REGEX REPLACE ".*SQLITE_VERSION[ ]*\"(.*)\"\n" "\\1" SQLITE_VERSION ${SQLITE_VERSION_MATCH}) + + if(SQLITE_VERSION VERSION_LESS "${SQLITE_MIN_VERSION}") + message(STATUS "Sqlite ${SQLITE_VERSION} was found, but at least version ${SQLITE_MIN_VERSION} is required") + set(SQLITE_VERSION_OK FALSE) + else(SQLITE_VERSION VERSION_LESS "${SQLITE_MIN_VERSION}") + set(SQLITE_VERSION_OK TRUE) + endif(SQLITE_VERSION VERSION_LESS "${SQLITE_MIN_VERSION}") + + endif(SQLITE_VERSION_MATCH) + + if (SQLITE_VERSION_OK) + file(WRITE ${CMAKE_BINARY_DIR}/sqlite_check_unlock_notify.cpp + "#include + int main(int argc, char **argv) { + return sqlite3_unlock_notify(0, 0, 0); + }") + try_compile(SQLITE_HAS_UNLOCK_NOTIFY + ${CMAKE_BINARY_DIR}/sqlite_check_unlock_notify + ${CMAKE_BINARY_DIR}/sqlite_check_unlock_notify.cpp + LINK_LIBRARIES ${SQLITE_LIBRARIES} + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES:PATH=${SQLITE_INCLUDE_DIR}") + if (NOT SQLITE_HAS_UNLOCK_NOTIFY) + message(STATUS "Sqlite ${SQLITE_VERSION} was found, but it is not compiled with -DSQLITE_ENABLE_UNLOCK_NOTIFY") + endif() + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( Sqlite DEFAULT_MSG + SQLITE_INCLUDE_DIR + SQLITE_LIBRARIES + SQLITE_VERSION_OK + SQLITE_HAS_UNLOCK_NOTIFY) + +# show the SQLITE_INCLUDE_DIR and SQLITE_LIBRARIES variables only in the advanced view +mark_as_advanced( SQLITE_INCLUDE_DIR SQLITE_LIBRARIES ) + diff --git a/cmake/modules/FindStdlibInclude.cmake b/cmake/modules/FindStdlibInclude.cmake new file mode 100644 index 0000000..0a1915a --- /dev/null +++ b/cmake/modules/FindStdlibInclude.cmake @@ -0,0 +1,39 @@ +# This functions returns an absolute path to a C++ stdlib header file names +# @includeName@ +function(findStdlibInclude includeName filePath) + # Create the simplest possible valid C++ program - we don't care about + # what the program does, we only need to feed some valid code to the compiler + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/empty_test.cpp" + "int main() { return 0; }" + ) + # With the "-v" flag, compiler will print out list of default include paths + # (i.e. stdlib include paths) + execute_process(COMMAND ${CMAKE_CXX_COMPILER} ${CXX_STDLIB_FLAGS} -Wp,-v -E ${CMAKE_CURRENT_BINARY_DIR}/empty_test.cpp + OUTPUT_QUIET + ERROR_VARIABLE compilerSearchPaths + ) + # Turn the output to a CMake lists (\n -> ;) + STRING(REPLACE ";" "\\\;" compilerSearchPaths "${compilerSearchPaths}") + STRING(REPLACE "\n" ";" compilerSearchPaths "${compilerSearchPaths}") + foreach(rawline ${compilerSearchPaths}) + STRING(SUBSTRING "${rawline}" 0 2 lineStart) + # The output from compiler contains bunch of other things, but the include + # paths all begin with one whitespace and then path separator + # FIXME: This probably won't work on Windows...? + if ("${lineStart}" STREQUAL " /") + # Get the actual path (without the opening whitespace) + STRING(SUBSTRING ${rawline} 1 -1 path) + # The path is often relative, make it absolute without ".." + get_filename_component(path "${path}" ABSOLUTE) + set(std_file "std_file-NOTFOUND") # ensure it will be looked-up every time + find_file(std_file ${includeName} PATHS ${path}) + if (NOT "${std_file}" STREQUAL "std_file-NOTFOUND") + message(STATUS "Found C++ stdlib ${includeName} include file: ${std_file}") + set(${filePath} "${std_file}" PARENT_SCOPE) + return() + endif() + endif() + endforeach() + + set(${filePath} "" PARENT_SCOPE) +endfunction() diff --git a/cmake/modules/FindXsltproc.cmake b/cmake/modules/FindXsltproc.cmake new file mode 100644 index 0000000..33b94c9 --- /dev/null +++ b/cmake/modules/FindXsltproc.cmake @@ -0,0 +1,26 @@ +# Find xsltproc executable and provide a macro to generate D-Bus interfaces. +# +# The following variables are defined : +# XSLTPROC_EXECUTABLE - path to the xsltproc executable +# Xsltproc_FOUND - true if the program was found +# +find_program(XSLTPROC_EXECUTABLE xsltproc DOC "Path to the xsltproc executable") +mark_as_advanced(XSLTPROC_EXECUTABLE) + +if(XSLTPROC_EXECUTABLE) + set(Xsltproc_FOUND TRUE) + + # Macro to generate a D-Bus interface description from a KConfigXT file + macro(kcfg_generate_dbus_interface _kcfg _name) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml + COMMAND ${XSLTPROC_EXECUTABLE} --stringparam interfaceName ${_name} + ${CMAKE_SOURCE_DIR}/akonadi/kcfg2dbus.xsl + ${_kcfg} + > ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml + DEPENDS ${CMAKE_SOURCE_DIR}/akonadi/kcfg2dbus.xsl + ${_kcfg} + ) + endmacro() +endif() + diff --git a/cmake/modules/cmake-copyright.txt b/cmake/modules/cmake-copyright.txt new file mode 100644 index 0000000..35f7e4b --- /dev/null +++ b/cmake/modules/cmake-copyright.txt @@ -0,0 +1,56 @@ +CMake - Cross Platform Makefile Generator +Copyright 2000-2009 Kitware, Inc., Insight Software Consortium +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the names of Kitware, Inc., the Insight Software Consortium, + nor the names of their contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +The above copyright and license notice applies to distributions of +CMake in source and binary form. Some source files contain additional +notices of original copyright by their contributors; see each source +for details. Third-party software packages supplied with CMake under +compatible licenses provide their own copyright notices documented in +corresponding subdirectories. + +------------------------------------------------------------------------------ + +CMake was initially developed by Kitware with the following sponsorship: + + * National Library of Medicine at the National Institutes of Health + as part of the Insight Segmentation and Registration Toolkit (ITK). + + * US National Labs (Los Alamos, Livermore, Sandia) ASC Parallel + Visualization Initiative. + + * National Alliance for Medical Image Computing (NAMIC) is funded by the + National Institutes of Health through the NIH Roadmap for Medical Research, + Grant U54 EB005149. + + * Kitware, Inc. diff --git a/config-akonadi.h.cmake b/config-akonadi.h.cmake new file mode 100644 index 0000000..211b09e --- /dev/null +++ b/config-akonadi.h.cmake @@ -0,0 +1,8 @@ +#cmakedefine01 Backtrace_FOUND +#if Backtrace_FOUND +# include <@Backtrace_HEADER@> +#endif + +#cmakedefine HAVE_UNISTD_H 1 + +#define AKONADI_DATABASE_BACKEND "@AKONADI_DATABASE_BACKEND@" diff --git a/dox/bufferedcaching1.png b/dox/bufferedcaching1.png new file mode 100644 index 0000000..918eaf7 Binary files /dev/null and b/dox/bufferedcaching1.png differ diff --git a/dox/bufferedcaching2.png b/dox/bufferedcaching2.png new file mode 100644 index 0000000..869e1b9 Binary files /dev/null and b/dox/bufferedcaching2.png differ diff --git a/dox/bufferedcaching3.png b/dox/bufferedcaching3.png new file mode 100644 index 0000000..bbee1a9 Binary files /dev/null and b/dox/bufferedcaching3.png differ diff --git a/dox/bufferedcaching4.png b/dox/bufferedcaching4.png new file mode 100644 index 0000000..dbe08e0 Binary files /dev/null and b/dox/bufferedcaching4.png differ diff --git a/dox/bufferedcaching6.png b/dox/bufferedcaching6.png new file mode 100644 index 0000000..1f97cf6 Binary files /dev/null and b/dox/bufferedcaching6.png differ diff --git a/dox/descendantentitiesproxymodel-colfilter.png b/dox/descendantentitiesproxymodel-colfilter.png new file mode 100644 index 0000000..1c9232a Binary files /dev/null and b/dox/descendantentitiesproxymodel-colfilter.png differ diff --git a/dox/descendantentitiesproxymodel-withansecnames.png b/dox/descendantentitiesproxymodel-withansecnames.png new file mode 100644 index 0000000..f1cdc1d Binary files /dev/null and b/dox/descendantentitiesproxymodel-withansecnames.png differ diff --git a/dox/descendantentitiesproxymodel.png b/dox/descendantentitiesproxymodel.png new file mode 100644 index 0000000..01be72f Binary files /dev/null and b/dox/descendantentitiesproxymodel.png differ diff --git a/dox/entitytreemodel-collections.png b/dox/entitytreemodel-collections.png new file mode 100644 index 0000000..526de57 Binary files /dev/null and b/dox/entitytreemodel-collections.png differ diff --git a/dox/entitytreemodel-showroot.png b/dox/entitytreemodel-showroot.png new file mode 100644 index 0000000..df9804d Binary files /dev/null and b/dox/entitytreemodel-showroot.png differ diff --git a/dox/entitytreemodel-showrootwithname.png b/dox/entitytreemodel-showrootwithname.png new file mode 100644 index 0000000..3b3820e Binary files /dev/null and b/dox/entitytreemodel-showrootwithname.png differ diff --git a/dox/entitytreemodel.png b/dox/entitytreemodel.png new file mode 100644 index 0000000..2b46121 Binary files /dev/null and b/dox/entitytreemodel.png differ diff --git a/dox/mailmodelapp.png b/dox/mailmodelapp.png new file mode 100644 index 0000000..23b86e4 Binary files /dev/null and b/dox/mailmodelapp.png differ diff --git a/dox/selectionproxymodel-ordered.png b/dox/selectionproxymodel-ordered.png new file mode 100644 index 0000000..5c79d55 Binary files /dev/null and b/dox/selectionproxymodel-ordered.png differ diff --git a/dox/selectionproxymodelmultipleselection-withdescendant.png b/dox/selectionproxymodelmultipleselection-withdescendant.png new file mode 100644 index 0000000..9a7a69d Binary files /dev/null and b/dox/selectionproxymodelmultipleselection-withdescendant.png differ diff --git a/dox/selectionproxymodelmultipleselection.png b/dox/selectionproxymodelmultipleselection.png new file mode 100644 index 0000000..b98c1bc Binary files /dev/null and b/dox/selectionproxymodelmultipleselection.png differ diff --git a/dox/selectionproxymodelsimpleselection.png b/dox/selectionproxymodelsimpleselection.png new file mode 100644 index 0000000..798535d Binary files /dev/null and b/dox/selectionproxymodelsimpleselection.png differ diff --git a/dox/treeandlistapp.png b/dox/treeandlistapp.png new file mode 100644 index 0000000..0867b14 Binary files /dev/null and b/dox/treeandlistapp.png differ diff --git a/dox/treeandlistappwithdesclist.png b/dox/treeandlistappwithdesclist.png new file mode 100644 index 0000000..c5b790b Binary files /dev/null and b/dox/treeandlistappwithdesclist.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..4141947 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,21 @@ +add_subdirectory(agentserver) +add_subdirectory(akonadicontrol) +add_subdirectory(akonadictl) +if(NOT WIN32) + add_subdirectory(asapcat) +endif() +add_subdirectory(private) +add_subdirectory(interfaces) +if(SQLITE_FOUND) + add_subdirectory(qsqlite) +endif() +add_subdirectory(rds) +add_subdirectory(server) +add_subdirectory(shared) +add_subdirectory(core) +add_subdirectory(agentbase) +add_subdirectory(widgets) +add_subdirectory(selftest) +if(BUILD_TOOLS) + add_subdirectory(xml) +endif() diff --git a/src/Messages.sh b/src/Messages.sh new file mode 100755 index 0000000..84bce7f --- /dev/null +++ b/src/Messages.sh @@ -0,0 +1,3 @@ +#! /bin/sh +$EXTRACTRC `find -name "*.ui"` >> rc.cpp +$XGETTEXT `find -name "*.cpp" -o -name "*.h"` -o $podir/libakonadi5.pot diff --git a/src/agentbase/CMakeLists.txt b/src/agentbase/CMakeLists.txt new file mode 100644 index 0000000..98cd66e --- /dev/null +++ b/src/agentbase/CMakeLists.txt @@ -0,0 +1,96 @@ +set(akonadiagentbase_SRCS + agentbase.cpp + agentsearchinterface.cpp + preprocessorbase.cpp + preprocessorbase_p.cpp + recursivemover.cpp + resourcebase.cpp + resourcescheduler.cpp + resourcesettings.cpp + transportresourcebase.cpp +) + +ecm_qt_declare_logging_category(akonadiagentbase_SRCS HEADER akonadiagentbase_debug.h IDENTIFIER AKONADIAGENTBASE_LOG CATEGORY_NAME akonadiagentbase_log) + +ecm_generate_headers(AkonadiAgentBase_HEADERS + HEADER_NAMES + AgentBase + AgentSearchInterface + ResourceBase + ResourceSettings + TransportResourceBase + REQUIRED_HEADERS AkonadiAgentBase_HEADERS +) + + +KCONFIG_ADD_KCFG_FILES(akonadiagentbase_SRCS resourcebasesettings.kcfgc) + +qt5_add_dbus_interfaces(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Tracer.xml ) + +qt5_add_dbus_adaptor(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Resource.xml + resourcebase.h Akonadi::ResourceBase resourceadaptor Akonadi__ResourceAdaptor) +qt5_add_dbus_adaptor(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml + preprocessorbase_p.h Akonadi::PreprocessorBasePrivate preprocessoradaptor Akonadi__PreprocessorAdaptor) +qt5_add_dbus_adaptor(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml + agentbase.h Akonadi::AgentBase statusadaptor Akonadi__StatusAdaptor) +qt5_add_dbus_adaptor(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml + agentbase.h Akonadi::AgentBase controladaptor Akonadi__ControlAdaptor) +qt5_add_dbus_adaptor(akonadiagentbase_SRCS ../interfaces/org.freedesktop.Akonadi.Resource.Transport.xml + transportresourcebase_p.h Akonadi::TransportResourceBasePrivate transportadaptor Akonadi__TransportAdaptor) +qt5_add_dbus_adaptor(akonadiagentbase_SRCS ../interfaces/org.freedesktop.Akonadi.Agent.Search.xml + agentsearchinterface_p.h Akonadi::AgentSearchInterfacePrivate searchadaptor Akonadi__SearchAdaptor ) + +add_library(KF5AkonadiAgentBase ${akonadiagentbase_SRCS}) + +generate_export_header(KF5AkonadiAgentBase BASE_NAME akonadiagentbase) + +add_library(KF5::AkonadiAgentBase ALIAS KF5AkonadiAgentBase) + +target_include_directories(KF5AkonadiAgentBase INTERFACE "$") + +target_link_libraries(KF5AkonadiAgentBase +PUBLIC + Qt5::Widgets # for QApplication + KF5::AkonadiCore + KF5::ConfigCore + KF5::ConfigGui # for KConfigSkeleton +PRIVATE + KF5::AkonadiPrivate + KF5::DBusAddons + KF5::I18n + Qt5::Network +) + +set_target_properties(KF5AkonadiAgentBase PROPERTIES + VERSION ${AKONADI_VERSION_STRING} + SOVERSION ${AKONADI_SOVERSION} + EXPORT_NAME AkonadiAgentBase +) + +ecm_generate_pri_file(BASE_NAME AkonadiAgentBase + LIB_NAME KF5AkonadiAgentBase + DEPS "AkonadiCore AkonadiPrivate ConfigCore ConfigGui" FILENAME_VAR PRI_FILENAME +) + +install(TARGETS + KF5AkonadiAgentBase + EXPORT KF5AkonadiTargets + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadiagentbase_export.h + ${CMAKE_CURRENT_BINARY_DIR}/resourcebasesettings.h + ${AkonadiAgentBase_HEADERS} + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/AkonadiAgentBase COMPONENT Devel +) + +install(FILES + resourcebase.kcfg + DESTINATION ${KDE_INSTALL_KCFGDIR} +) + +install(FILES + ${PRI_FILENAME} + DESTINATION ${ECM_MKSPECS_INSTALL_DIR} +) diff --git a/src/agentbase/agentbase.cpp b/src/agentbase/agentbase.cpp new file mode 100644 index 0000000..0e5ff79 --- /dev/null +++ b/src/agentbase/agentbase.cpp @@ -0,0 +1,1299 @@ +/* + Copyright (c) 2006 Till Adam + Copyright (c) 2007 Volker Krause + Copyright (c) 2007 Bruno Virlet + Copyright (c) 2008 Kevin Krammer + + 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 "agentbase.h" +#include "agentbase_p.h" + +#include "akonadi_version.h" +#include "agentmanager.h" +#include "changerecorder.h" +#include "controladaptor.h" +#include "KDBusConnectionPool" +#include "itemfetchjob.h" +#include "monitor_p.h" +#include "servermanager_p.h" +#include "session.h" +#include "session_p.h" +#include "statusadaptor.h" +#include "private/standarddirs_p.h" + + +#include "akonadiagentbase_debug.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#if defined __GLIBC__ +# include // for dumping memory information +#endif + +//#define EXPERIMENTAL_INPROCESS_AGENTS 1 + +using namespace Akonadi; + +static AgentBase *sAgentBase = 0; + +AgentBase::Observer::Observer() +{ +} + +AgentBase::Observer::~Observer() +{ +} + +void AgentBase::Observer::itemAdded(const Item &item, const Collection &collection) +{ + Q_UNUSED(item); + Q_UNUSED(collection); + if (sAgentBase != 0) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::itemChanged(const Item &item, const QSet &partIdentifiers) +{ + Q_UNUSED(item); + Q_UNUSED(partIdentifiers); + if (sAgentBase != 0) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::itemRemoved(const Item &item) +{ + Q_UNUSED(item); + if (sAgentBase != 0) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + Q_UNUSED(collection); + Q_UNUSED(parent); + if (sAgentBase != 0) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::collectionChanged(const Collection &collection) +{ + Q_UNUSED(collection); + if (sAgentBase != 0) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::collectionRemoved(const Collection &collection) +{ + Q_UNUSED(collection); + if (sAgentBase != 0) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &dest) +{ + Q_UNUSED(item); + Q_UNUSED(source); + Q_UNUSED(dest); + if (sAgentBase != 0) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_UNUSED(item); + Q_UNUSED(collection); + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemLinked, + sAgentBase->d_ptr, &AgentBasePrivate::itemLinked); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_UNUSED(item); + Q_UNUSED(collection); + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemUnlinked, + sAgentBase->d_ptr, &AgentBasePrivate::itemUnlinked); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &dest) +{ + Q_UNUSED(collection); + Q_UNUSED(source); + Q_UNUSED(dest); + if (sAgentBase != 0) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::collectionChanged(const Akonadi::Collection &collection, const QSet &changedAttributes) +{ + Q_UNUSED(changedAttributes); + collectionChanged(collection); +} + +void AgentBase::ObserverV3::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet< QByteArray > &addedFlags, const QSet< QByteArray > &removedFlags) +{ + Q_UNUSED(items); + Q_UNUSED(addedFlags); + Q_UNUSED(removedFlags); + + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsFlagsChanged, + sAgentBase->d_ptr, &AgentBasePrivate::itemsFlagsChanged); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV3::itemsMoved(const Akonadi::Item::List &items, const Collection &sourceCollection, const Collection &destinationCollection) +{ + Q_UNUSED(items); + Q_UNUSED(sourceCollection); + Q_UNUSED(destinationCollection); + + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsMoved, + sAgentBase->d_ptr, &AgentBasePrivate::itemsMoved); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV3::itemsRemoved(const Akonadi::Item::List &items) +{ + Q_UNUSED(items); + + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsRemoved, + sAgentBase->d_ptr, &AgentBasePrivate::itemsRemoved); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV3::itemsLinked(const Akonadi::Item::List &items, const Collection &collection) +{ + Q_UNUSED(items); + Q_UNUSED(collection); + + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsLinked, + sAgentBase->d_ptr, &AgentBasePrivate::itemsLinked); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV3::itemsUnlinked(const Akonadi::Item::List &items, const Collection &collection) +{ + Q_UNUSED(items); + Q_UNUSED(collection) + + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsUnlinked, + sAgentBase->d_ptr, &AgentBasePrivate::itemsUnlinked); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::tagAdded(const Tag &tag) +{ + Q_UNUSED(tag); + + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::tagAdded, + sAgentBase->d_ptr, &AgentBasePrivate::tagAdded); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::tagChanged(const Tag &tag) +{ + Q_UNUSED(tag); + + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::tagChanged, + sAgentBase->d_ptr, &AgentBasePrivate::tagChanged); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::tagRemoved(const Tag &tag) +{ + Q_UNUSED(tag); + + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::tagRemoved, + sAgentBase->d_ptr, &AgentBasePrivate::tagRemoved); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::itemsTagsChanged(const Item::List &items, const QSet &addedTags, const QSet &removedTags) +{ + Q_UNUSED(items); + Q_UNUSED(addedTags); + Q_UNUSED(removedTags); + + if (sAgentBase != 0) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsTagsChanged, + sAgentBase->d_ptr, &AgentBasePrivate::itemsTagsChanged); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::relationAdded(const Akonadi::Relation &relation) +{ + Q_UNUSED(relation) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::relationAdded, + sAgentBase->d_ptr, &AgentBasePrivate::relationAdded); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::relationRemoved(const Akonadi::Relation &relation) +{ + Q_UNUSED(relation) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::relationRemoved, + sAgentBase->d_ptr, &AgentBasePrivate::relationRemoved); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::itemsRelationsChanged(const Akonadi::Item::List &items, + const Akonadi::Relation::List &addedRelations, + const Akonadi::Relation::List &removedRelations) +{ + Q_UNUSED(items) + Q_UNUSED(addedRelations) + Q_UNUSED(removedRelations) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), SIGNAL(itemsRelationsChanged(Akonadi::Item::List,Akonadi::Relation::List,Akonadi::Relation::List)), + sAgentBase, SLOT(itemsRelationsChanged(Akonadi::Item::List,Akonadi::Relation::List,Akonadi::Relation::List))); + sAgentBase->d_ptr->changeProcessed(); + } +} + +//@cond PRIVATE + +AgentBasePrivate::AgentBasePrivate(AgentBase *parent) + : q_ptr(parent) + , mStatusCode(AgentBase::Idle) + , mProgress(0) + , mNeedsNetwork(false) + , mOnline(false) + , mDesiredOnlineState(false) + , mSettings(0) + , mChangeRecorder(0) + , mTracer(0) + , mObserver(0) + , mPowerInterface(0) + , mTemporaryOfflineTimer(0) + , mEventLoopLocker(0) + , mNetworkManager(Q_NULLPTR) +{ + Internal::setClientType(Internal::Agent); +} + +AgentBasePrivate::~AgentBasePrivate() +{ + mChangeRecorder->setConfig(0); + delete mSettings; +} + +void AgentBasePrivate::init() +{ + Q_Q(AgentBase); + + Kdelibs4ConfigMigrator migrate(mId); + migrate.setConfigFiles(QStringList() << QStringLiteral("%1rc").arg(mId)); + migrate.migrate(); + + /** + * Create a default session for this process. + */ + SessionPrivate::createDefaultSession(mId.toLatin1()); + + mTracer = new org::freedesktop::Akonadi::Tracer(ServerManager::serviceName(ServerManager::Server), + QStringLiteral("/tracing"), + KDBusConnectionPool::threadConnection(), q); + + new Akonadi__ControlAdaptor(q); + new Akonadi__StatusAdaptor(q); + if (!KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/"), q, QDBusConnection::ExportAdaptors)) { + q->error(i18n("Unable to register object at dbus: %1", KDBusConnectionPool::threadConnection().lastError().message())); + } + + mSettings = new QSettings(QStringLiteral("%1/agent_config_%2").arg(StandardDirs::saveDir("config"), mId), QSettings::IniFormat); + + mChangeRecorder = new ChangeRecorder(q); + mChangeRecorder->ignoreSession(Session::defaultSession()); + mChangeRecorder->itemFetchScope().setCacheOnly(true); + mChangeRecorder->setConfig(mSettings); + + mDesiredOnlineState = mSettings->value(QStringLiteral("Agent/DesiredOnlineState"), true).toBool(); + mOnline = mDesiredOnlineState; + + // reinitialize the status message now that online state is available + mStatusMessage = defaultReadyMessage(); + + mName = mSettings->value(QStringLiteral("Agent/Name")).toString(); + if (mName.isEmpty()) { + mName = mSettings->value(QStringLiteral("Resource/Name")).toString(); + if (!mName.isEmpty()) { + mSettings->remove(QStringLiteral("Resource/Name")); + mSettings->setValue(QStringLiteral("Agent/Name"), mName); + } + } + + connect(mChangeRecorder, &Monitor::itemAdded, + this, &AgentBasePrivate::itemAdded); + connect(mChangeRecorder, &Monitor::itemChanged, + this, &AgentBasePrivate::itemChanged); + connect(mChangeRecorder, &Monitor::collectionAdded, + this, &AgentBasePrivate::collectionAdded); + connect(mChangeRecorder, SIGNAL(collectionChanged(Akonadi::Collection)), + SLOT(collectionChanged(Akonadi::Collection))); + connect(mChangeRecorder, SIGNAL(collectionChanged(Akonadi::Collection,QSet)), + SLOT(collectionChanged(Akonadi::Collection,QSet))); + connect(mChangeRecorder, &Monitor::collectionMoved, + this, &AgentBasePrivate::collectionMoved); + connect(mChangeRecorder, &Monitor::collectionRemoved, + this, &AgentBasePrivate::collectionRemoved); + connect(mChangeRecorder, &Monitor::collectionSubscribed, + this, &AgentBasePrivate::collectionSubscribed); + connect(mChangeRecorder, &Monitor::collectionUnsubscribed, + this, &AgentBasePrivate::collectionUnsubscribed); + + connect(q, SIGNAL(status(int,QString)), q, SLOT(slotStatus(int,QString))); + connect(q, SIGNAL(percent(int)), q, SLOT(slotPercent(int))); + connect(q, SIGNAL(warning(QString)), q, SLOT(slotWarning(QString))); + connect(q, SIGNAL(error(QString)), q, SLOT(slotError(QString))); + + mPowerInterface = new QDBusInterface(QStringLiteral("org.kde.Solid.PowerManagement"), + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/SuspendSession"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.SuspendSession"), + QDBusConnection::sessionBus(), this); + if (mPowerInterface->isValid()) { + connect(mPowerInterface, SIGNAL(resumingFromSuspend()), + q, SLOT(slotResumedFromSuspend())); + } else { + delete mPowerInterface; + mPowerInterface = 0; + } + + // Use reference counting to allow agents to finish internal jobs when the + // agent is stopped. + mEventLoopLocker = new QEventLoopLocker(); + + mResourceTypeName = AgentManager::self()->instance(mId).type().name(); + setProgramName(); + + QTimer::singleShot(0, q, SLOT(delayedInit())); +} + +void AgentBasePrivate::delayedInit() +{ + Q_Q(AgentBase); + + const QString serviceId = ServerManager::agentServiceName(ServerManager::Agent, mId); + if (!KDBusConnectionPool::threadConnection().registerService(serviceId)) { + qCCritical(AKONADIAGENTBASE_LOG) << "Unable to register service" << serviceId << "at dbus:" + << KDBusConnectionPool::threadConnection().lastError().message(); + } + q->setOnlineInternal(mDesiredOnlineState); + + KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Debug"), this, QDBusConnection::ExportScriptableSlots); +} + +void AgentBasePrivate::setProgramName() +{ + // ugly, really ugly, if you find another solution, change it and blame me for this code (Andras) + QString programName = mResourceTypeName; + if (!mName.isEmpty()) { + programName = i18nc("Name and type of Akonadi resource", "%1 of type %2", mName, mResourceTypeName) ; + } + + QGuiApplication::setApplicationDisplayName(programName); +} + +void AgentBasePrivate::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + if (mObserver != 0) { + mObserver->itemAdded(item, collection); + } +} + +void AgentBasePrivate::itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers) +{ + if (mObserver != 0) { + mObserver->itemChanged(item, partIdentifiers); + } +} + +void AgentBasePrivate::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &dest) +{ + AgentBase::ObserverV2 *observer2 = dynamic_cast(mObserver); + if (mObserver) { + // inter-resource moves, requires we know which resources the source and destination are in though + if (!source.resource().isEmpty() && !dest.resource().isEmpty()) { + if (source.resource() != dest.resource()) { + if (source.resource() == q_ptr->identifier()) { // moved away from us + Akonadi::Item i(item); + i.setParentCollection(source); + mObserver->itemRemoved(i); + } else if (dest.resource() == q_ptr->identifier()) { // moved to us + mObserver->itemAdded(item, dest); + } else if (observer2) { + observer2->itemMoved(item, source, dest); + } else { + // not for us, not sure if we should get here at all + changeProcessed(); + } + return; + } + } + // intra-resource move + if (observer2) { + observer2->itemMoved(item, source, dest); + } else { + // ### we cannot just call itemRemoved here as this will already trigger changeProcessed() + // so, just itemAdded() is good enough as no resource can have implemented intra-resource moves anyway + // without using ObserverV2 + mObserver->itemAdded(item, dest); + // mObserver->itemRemoved( item ); + } + } +} + +void AgentBasePrivate::itemRemoved(const Akonadi::Item &item) +{ + if (mObserver != 0) { + mObserver->itemRemoved(item); + } +} + +void AgentBasePrivate::itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + AgentBase::ObserverV2 *observer2 = dynamic_cast(mObserver); + if (observer2) { + observer2->itemLinked(item, collection); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + AgentBase::ObserverV2 *observer2 = dynamic_cast(mObserver); + if (observer2) { + observer2->itemUnlinked(item, collection); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags) +{ + AgentBase::ObserverV3 *observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsFlagsChanged(items, addedFlags, removedFlags); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &source, const Akonadi::Collection &destination) +{ + AgentBase::ObserverV3 *observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsMoved(items, source, destination); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::itemsRemoved(const Akonadi::Item::List &items) +{ + AgentBase::ObserverV3 *observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsRemoved(items); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection) +{ + if (!mObserver) { + changeProcessed(); + return; + } + + AgentBase::ObserverV3 *observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsLinked(items, collection); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection) +{ + if (!mObserver) { + return; + } + + AgentBase::ObserverV3 *observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsUnlinked(items, collection); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::tagAdded(const Akonadi::Tag &tag) +{ + if (!mObserver) { + return; + } + + AgentBase::ObserverV4 *observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->tagAdded(tag); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::tagChanged(const Akonadi::Tag &tag) +{ + if (!mObserver) { + return; + } + + AgentBase::ObserverV4 *observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->tagChanged(tag); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::tagRemoved(const Akonadi::Tag &tag) +{ + if (!mObserver) { + return; + } + + AgentBase::ObserverV4 *observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->tagRemoved(tag);; + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, const QSet &removedTags) +{ + if (!mObserver) { + return; + } + + AgentBase::ObserverV4 *observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->itemsTagsChanged(items, addedTags, removedTags); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::relationAdded(const Akonadi::Relation &relation) +{ + if (!mObserver) { + return; + } + + AgentBase::ObserverV4 *observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->relationAdded(relation); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::relationRemoved(const Akonadi::Relation &relation) +{ + if (!mObserver) { + return; + } + + AgentBase::ObserverV4 *observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->relationRemoved(relation); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemsRelationsChanged(const Akonadi::Item::List &items, + const Akonadi::Relation::List &addedRelations, + const Akonadi::Relation::List &removedRelations) +{ + if (!mObserver) { + return; + } + + AgentBase::ObserverV4 *observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->itemsRelationsChanged(items, addedRelations, removedRelations); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + if (mObserver != 0) { + mObserver->collectionAdded(collection, parent); + } +} + +void AgentBasePrivate::collectionChanged(const Akonadi::Collection &collection) +{ + AgentBase::ObserverV2 *observer2 = dynamic_cast(mObserver); + if (mObserver != 0 && observer2 == 0) { // For ObserverV2 we use the variant with the part identifiers + mObserver->collectionChanged(collection); + } +} + +void AgentBasePrivate::collectionChanged(const Akonadi::Collection &collection, const QSet &changedAttributes) +{ + AgentBase::ObserverV2 *observer2 = dynamic_cast(mObserver); + if (observer2 != 0) { + observer2->collectionChanged(collection, changedAttributes); + } +} + +void AgentBasePrivate::collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &dest) +{ + AgentBase::ObserverV2 *observer2 = dynamic_cast(mObserver); + if (observer2) { + observer2->collectionMoved(collection, source, dest); + } else if (mObserver) { + // ### we cannot just call collectionRemoved here as this will already trigger changeProcessed() + // so, just collectionAdded() is good enough as no resource can have implemented intra-resource moves anyway + // without using ObserverV2 + mObserver->collectionAdded(collection, dest); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::collectionRemoved(const Akonadi::Collection &collection) +{ + if (mObserver != 0) { + mObserver->collectionRemoved(collection); + } +} + +void AgentBasePrivate::collectionSubscribed(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + Q_UNUSED(collection); + Q_UNUSED(parent); + changeProcessed(); +} + +void AgentBasePrivate::collectionUnsubscribed(const Akonadi::Collection &collection) +{ + Q_UNUSED(collection); + changeProcessed(); +} + +void AgentBasePrivate::changeProcessed() +{ + mChangeRecorder->changeProcessed(); + QTimer::singleShot(0, mChangeRecorder, &ChangeRecorder::replayNext); +} + +void AgentBasePrivate::slotStatus(int status, const QString &message) +{ + mStatusMessage = message; + mStatusCode = 0; + + switch (status) { + case AgentBase::Idle: + if (mStatusMessage.isEmpty()) { + mStatusMessage = defaultReadyMessage(); + } + + mStatusCode = 0; + break; + case AgentBase::Running: + if (mStatusMessage.isEmpty()) { + mStatusMessage = defaultSyncingMessage(); + } + + mStatusCode = 1; + break; + case AgentBase::Broken: + if (mStatusMessage.isEmpty()) { + mStatusMessage = defaultErrorMessage(); + } + + mStatusCode = 2; + break; + + case AgentBase::NotConfigured: + if (mStatusMessage.isEmpty()) { + mStatusMessage = defaultUnconfiguredMessage(); + } + + mStatusCode = 3; + break; + + default: + Q_ASSERT(!"Unknown status passed"); + break; + } +} + +void AgentBasePrivate::slotPercent(int progress) +{ + mProgress = progress; +} + +void AgentBasePrivate::slotWarning(const QString &message) +{ + mTracer->warning(QStringLiteral("AgentBase(%1)").arg(mId), message); +} + +void AgentBasePrivate::slotError(const QString &message) +{ + mTracer->error(QStringLiteral("AgentBase(%1)").arg(mId), message); +} + +void AgentBasePrivate::slotNetworkStatusChange(bool isOnline) +{ + Q_UNUSED(isOnline); + Q_Q(AgentBase); + q->setOnlineInternal(mDesiredOnlineState); +} + +void AgentBasePrivate::slotResumedFromSuspend() +{ + if (mNeedsNetwork) { + slotNetworkStatusChange(mNetworkManager->isOnline()); + } +} + +void AgentBasePrivate::slotTemporaryOfflineTimeout() +{ + Q_Q(AgentBase); + q->setOnlineInternal(true); +} + +QString AgentBasePrivate::dumpNotificationListToString() const +{ + return mChangeRecorder->dumpNotificationListToString(); +} + +void AgentBasePrivate::dumpMemoryInfo() const +{ + // Send it to stdout, so we can debug user problems. + // since you have to explicitly call this + // it won't flood users with release builds. + QTextStream stream(stdout); + stream << dumpMemoryInfoToString(); +} + +QString AgentBasePrivate::dumpMemoryInfoToString() const +{ + // man mallinfo for more info + QString str; +#if defined __GLIBC__ + struct mallinfo mi; + mi = mallinfo(); + QTextStream stream(&str); + stream + << "Total non-mmapped bytes (arena): " << mi.arena << '\n' + << "# of free chunks (ordblks): " << mi.ordblks << '\n' + << "# of free fastbin blocks (smblks>: " << mi.smblks << '\n' + << "# of mapped regions (hblks): " << mi.hblks << '\n' + << "Bytes in mapped regions (hblkhd): " << mi.hblkhd << '\n' + << "Max. total allocated space (usmblks): " << mi.usmblks << '\n' + << "Free bytes held in fastbins (fsmblks):" << mi.fsmblks << '\n' + << "Total allocated space (uordblks): " << mi.uordblks << '\n' + << "Total free space (fordblks): " << mi.fordblks << '\n' + << "Topmost releasable block (keepcost): " << mi.keepcost << '\n'; +#else + str = QLatin1String("mallinfo() not supported"); +#endif + return str; +} + +AgentBase::AgentBase(const QString &id) + : d_ptr(new AgentBasePrivate(this)) +{ + sAgentBase = this; + d_ptr->mId = id; + d_ptr->init(); +} + +AgentBase::AgentBase(AgentBasePrivate *d, const QString &id) + : d_ptr(d) +{ + sAgentBase = this; + d_ptr->mId = id; + d_ptr->init(); +} + +AgentBase::~AgentBase() +{ + delete d_ptr; +} + +QString AgentBase::parseArguments(int argc, char **argv) +{ + Q_UNUSED(argc); + + QCommandLineOption identifierOption(QStringLiteral("identifier"), i18n("Agent identifier"), + QStringLiteral("argument")); + QCommandLineParser parser; + parser.addOption(identifierOption); + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(*qApp); + parser.setApplicationDescription(i18n("Akonadi Agent")); + + if (!parser.isSet(identifierOption)) { + qCDebug(AKONADIAGENTBASE_LOG) << "Identifier argument missing"; + exit(1); + } + + const QString identifier = parser.value(identifierOption); + if (identifier.isEmpty()) { + qCDebug(AKONADIAGENTBASE_LOG) << "Identifier argument is empty"; + exit(1); + } + + QCoreApplication::setApplicationName(ServerManager::addNamespace(identifier)); + QCoreApplication::setApplicationVersion(QStringLiteral(AKONADI_VERSION_STRING)); + + const QFileInfo fi(QString::fromLocal8Bit(argv[0])); + // strip off full path and possible .exe suffix + const QString catalog = fi.baseName(); + + QTranslator *translator = new QTranslator(); + translator->load(catalog); + QCoreApplication::installTranslator(translator); + + return identifier; +} + +// @endcond + +int AgentBase::init(AgentBase *r) +{ + KLocalizedString::setApplicationDomain("libakonadi5"); + int rv = qApp->exec(); + delete r; + return rv; +} + +int AgentBase::status() const +{ + Q_D(const AgentBase); + + return d->mStatusCode; +} + +QString AgentBase::statusMessage() const +{ + Q_D(const AgentBase); + + return d->mStatusMessage; +} + +int AgentBase::progress() const +{ + Q_D(const AgentBase); + + return d->mProgress; +} + +QString AgentBase::progressMessage() const +{ + Q_D(const AgentBase); + + return d->mProgressMessage; +} + +bool AgentBase::isOnline() const +{ + Q_D(const AgentBase); + + return d->mOnline; +} + +void AgentBase::setNeedsNetwork(bool needsNetwork) +{ + Q_D(AgentBase); + if (d->mNeedsNetwork == needsNetwork) { + return; + } + + d->mNeedsNetwork = needsNetwork; + + if (d->mNeedsNetwork) { + d->mNetworkManager = new QNetworkConfigurationManager(this); + connect(d->mNetworkManager, SIGNAL(onlineStateChanged(bool)), + this, SLOT(slotNetworkStatusChange(bool)), + Qt::UniqueConnection); + + } else { + delete d->mNetworkManager; + setOnlineInternal(d->mDesiredOnlineState); + } +} + +void AgentBase::setOnline(bool state) +{ + Q_D(AgentBase); + d->mDesiredOnlineState = state; + d->mSettings->setValue(QStringLiteral("Agent/DesiredOnlineState"), state); + setOnlineInternal(state); +} + +void AgentBase::setTemporaryOffline(int makeOnlineInSeconds) +{ + Q_D(AgentBase); + + // if not currently online, avoid bringing it online after the timeout + if (!d->mOnline) { + return; + } + + setOnlineInternal(false); + + if (!d->mTemporaryOfflineTimer) { + d->mTemporaryOfflineTimer = new QTimer(d); + d->mTemporaryOfflineTimer->setSingleShot(true); + connect(d->mTemporaryOfflineTimer, SIGNAL(timeout()), this, SLOT(slotTemporaryOfflineTimeout())); + } + d->mTemporaryOfflineTimer->setInterval(makeOnlineInSeconds * 1000); + d->mTemporaryOfflineTimer->start(); +} + +void AgentBase::setOnlineInternal(bool state) +{ + Q_D(AgentBase); + if (state && d->mNeedsNetwork) { + if (!d->mNetworkManager->isOnline()) { + //Don't go online if the resource needs network but there is none + state = false; + } + } + d->mOnline = state; + + if (d->mTemporaryOfflineTimer) { + d->mTemporaryOfflineTimer->stop(); + } + + const QString newMessage = d->defaultReadyMessage(); + if (d->mStatusMessage != newMessage && d->mStatusCode != AgentBase::Broken) { + emit status(d->mStatusCode, newMessage); + } + + doSetOnline(state); + emit onlineChanged(state); +} + +void AgentBase::doSetOnline(bool online) +{ + Q_UNUSED(online); +} + +void AgentBase::configure(WId windowId) +{ + Q_UNUSED(windowId); + emit configurationDialogAccepted(); +} + +#ifdef Q_OS_WIN //krazy:exclude=cpp +void AgentBase::configure(qlonglong windowId) +{ + configure(reinterpret_cast(windowId)); +} +#endif + +WId AgentBase::winIdForDialogs() const +{ + const bool registered = KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(QStringLiteral("org.freedesktop.akonaditray")); + if (!registered) { + return 0; + } + + QDBusInterface dbus(QStringLiteral("org.freedesktop.akonaditray"), QStringLiteral("/Actions"), + QStringLiteral("org.freedesktop.Akonadi.Tray")); + const QDBusMessage reply = dbus.call(QStringLiteral("getWinId")); + + if (reply.type() == QDBusMessage::ErrorMessage) { + return 0; + } + + const WId winid = (WId)reply.arguments().at(0).toLongLong(); + + return winid; +} + +void AgentBase::quit() +{ + Q_D(AgentBase); + aboutToQuit(); + + if (d->mSettings) { + d->mChangeRecorder->setConfig(0); + d->mSettings->sync(); + } + + delete d->mEventLoopLocker; + d->mEventLoopLocker = Q_NULLPTR; +} + +void AgentBase::aboutToQuit() +{ +} + +void AgentBase::cleanup() +{ + Q_D(AgentBase); + // prevent the monitor from picking up deletion signals for our own data if we are a resource + // and thus avoid that we kill our own data as last act before our own death + d->mChangeRecorder->blockSignals(true); + + aboutToQuit(); + + const QString fileName = d->mSettings->fileName(); + + /* + * First destroy the settings object... + */ + d->mChangeRecorder->setConfig(0); + delete d->mSettings; + d->mSettings = 0; + + /* + * ... then remove the file from hd. + */ + QFile::remove(fileName); + + /* + * ... and remove the changes file from hd. + */ + QFile::remove(fileName + QStringLiteral("_changes.dat")); + + /* + * ... and also remove the agent configuration file if there is one. + */ + QString configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1Char('/') + config()->name(); + QFile::remove(configFile); + + delete d->mEventLoopLocker; + d->mEventLoopLocker = Q_NULLPTR; +} + +void AgentBase::registerObserver(Observer *observer) +{ + // TODO in theory we should re-connect change recorder signals here that we disconnected previously + d_ptr->mObserver = observer; + + const bool hasObserverV3 = (dynamic_cast(d_ptr->mObserver) != 0); + const bool hasObserverV4 = (dynamic_cast(d_ptr->mObserver) != 0); + + disconnect(d_ptr->mChangeRecorder, &Monitor::tagAdded, + d_ptr, &AgentBasePrivate::tagAdded); + disconnect(d_ptr->mChangeRecorder, &Monitor::tagChanged, + d_ptr, &AgentBasePrivate::tagChanged); + disconnect(d_ptr->mChangeRecorder, &Monitor::tagRemoved, + d_ptr, &AgentBasePrivate::tagRemoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsTagsChanged, + d_ptr, &AgentBasePrivate::itemsTagsChanged); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsFlagsChanged, + d_ptr, &AgentBasePrivate::itemsFlagsChanged); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsMoved, + d_ptr, &AgentBasePrivate::itemsMoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsRemoved, + d_ptr, &AgentBasePrivate::itemsRemoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsLinked, + d_ptr, &AgentBasePrivate::itemsLinked); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsUnlinked, + d_ptr, &AgentBasePrivate::itemsUnlinked); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemMoved, + d_ptr, &AgentBasePrivate::itemMoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemRemoved, + d_ptr, &AgentBasePrivate::itemRemoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemLinked, + d_ptr, &AgentBasePrivate::itemLinked); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemUnlinked, + d_ptr, &AgentBasePrivate::itemUnlinked); + + if (hasObserverV4) { + connect(d_ptr->mChangeRecorder, &Monitor::tagAdded, + d_ptr, &AgentBasePrivate::tagAdded); + connect(d_ptr->mChangeRecorder, &Monitor::tagChanged, + d_ptr, &AgentBasePrivate::tagChanged); + connect(d_ptr->mChangeRecorder, &Monitor::tagRemoved, + d_ptr, &AgentBasePrivate::tagRemoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemsTagsChanged, + d_ptr, &AgentBasePrivate::itemsTagsChanged); + } + + if (hasObserverV3) { + connect(d_ptr->mChangeRecorder, &Monitor::itemsFlagsChanged, + d_ptr, &AgentBasePrivate::itemsFlagsChanged); + connect(d_ptr->mChangeRecorder, &Monitor::itemsMoved, + d_ptr, &AgentBasePrivate::itemsMoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemsRemoved, + d_ptr, &AgentBasePrivate::itemsRemoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemsLinked, + d_ptr, &AgentBasePrivate::itemsLinked); + connect(d_ptr->mChangeRecorder, &Monitor::itemsUnlinked, + d_ptr, &AgentBasePrivate::itemsUnlinked); + } else { + // V2 - don't connect these if we have V3 + connect(d_ptr->mChangeRecorder, &Monitor::itemMoved, + d_ptr, &AgentBasePrivate::itemMoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemRemoved, + d_ptr, &AgentBasePrivate::itemRemoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemLinked, + d_ptr, &AgentBasePrivate::itemLinked); + connect(d_ptr->mChangeRecorder, &Monitor::itemUnlinked, + d_ptr, &AgentBasePrivate::itemUnlinked); + } +} + +QString AgentBase::identifier() const +{ + return d_ptr->mId; +} + +void AgentBase::setAgentName(const QString &name) +{ + Q_D(AgentBase); + if (name == d->mName) { + return; + } + + // TODO: rename collection + d->mName = name; + + if (d->mName.isEmpty() || d->mName == d->mId) { + d->mSettings->remove(QStringLiteral("Resource/Name")); + d->mSettings->remove(QStringLiteral("Agent/Name")); + } else { + d->mSettings->setValue(QStringLiteral("Agent/Name"), d->mName); + } + + d->mSettings->sync(); + + d->setProgramName(); + + emit agentNameChanged(d->mName); +} + +QString AgentBase::agentName() const +{ + Q_D(const AgentBase); + if (d->mName.isEmpty()) { + return d->mId; + } else { + return d->mName; + } +} + +void AgentBase::changeProcessed() +{ + Q_D(AgentBase); + d->changeProcessed(); +} + +ChangeRecorder *AgentBase::changeRecorder() const +{ + return d_ptr->mChangeRecorder; +} + +KSharedConfigPtr AgentBase::config() +{ + return KSharedConfig::openConfig(); +} + +void AgentBase::abort() +{ + emit abortRequested(); +} + +void AgentBase::reconfigure() +{ + emit reloadConfiguration(); +} + +#include "moc_agentbase.cpp" +#include "moc_agentbase_p.cpp" diff --git a/src/agentbase/agentbase.h b/src/agentbase/agentbase.h new file mode 100644 index 0000000..5271a06 --- /dev/null +++ b/src/agentbase/agentbase.h @@ -0,0 +1,810 @@ +/* + This file is part of akonadiresources. + + Copyright (c) 2006 Till Adam + Copyright (c) 2007 Volker Krause + Copyright (c) 2008 Kevin Krammer + + 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_AGENTBASE_H +#define AKONADI_AGENTBASE_H + +#include "akonadiagentbase_export.h" +#include "item.h" + +#include + +#include + +#include +#include + +class Akonadi__ControlAdaptor; +class Akonadi__StatusAdaptor; + +namespace Akonadi +{ + +class AgentBasePrivate; +class ChangeRecorder; +class Collection; +class Item; + +/** + * @short The base class for all Akonadi agents and resources. + * + * This class is a base class for all Akonadi agents, which covers the real + * agent processes and all resources. + * + * It provides: + * - lifetime management + * - change monitoring and recording + * - configuration interface + * - problem reporting + * + * Akonadi Server supports several ways to launch agents and resources: + * - As a separate application (@see AKONADI_AGENT_MAIN) + * - As a thread in the AgentServer + * - As a separate process, using the akonadi_agent_launcher + * + * The idea is this, the agent or resource is written as a plugin instead of an + * executable (@see AgentFactory). In the AgentServer case, the AgentServer + * looks up the plugin and launches the agent in a separate thread. In the + * launcher case, a new akonadi_agent_launcher process is started for each + * agent or resource instance. + * + * When making an Agent or Resource suitable for running in the AgentServer some + * extra caution is needed. Because multiple instances of several kinds of agents + * run in the same process, one cannot blindly use global objects like KGlobal. + * For this reasons several methods where added to avoid problems in this context, + * most notably AgentBase::config(). Additionally, + * one cannot use QDBusConnection::sessionBus() with dbus < 1.4, because of a + * multithreading bug in libdbus. Instead one should use + * KDBusConnectionPool::threadConnection() which works around this problem. + * + * @author Till Adam , Volker Krause + */ +class AKONADIAGENTBASE_EXPORT AgentBase : public QObject, protected QDBusContext +{ + Q_OBJECT + +public: + /** + * @short The interface for reacting on monitored or replayed changes. + * + * The Observer provides an interface to react on monitored or replayed changes. + * + * Since the this base class does only tell the change recorder that the change + * has been processed, an AgentBase subclass which wants to actually process + * the change needs to subclass Observer and reimplement the methods it is + * interested in. + * + * Such an agent specific Observer implementation can either be done + * stand-alone, i.e. as a separate object, or by inheriting both AgentBase + * and AgentBase::Observer. + * + * The observer implementation then has registered with the agent, so it + * can forward the incoming changes to the observer. + * + * @note In the multiple inheritance approach the init() method automatically + * registers itself as the observer. + * + * @note Do not call the base implementation of reimplemented virtual methods! + * The default implementation disconnected themselves from the Akonadi::ChangeRecorder + * to enable internal optimizations for unused notifications. + * + * Example for stand-alone observer: + * @code + * class ExampleAgent : public AgentBase + * { + * public: + * ExampleAgent( const QString &id ); + * + * ~ExampleAgent(); + * + * private: + * AgentBase::Observer *mObserver; + * }; + * + * class ExampleObserver : public AgentBase::Observer + * { + * protected: + * void itemChanged( const Item &item ); + * }; + * + * ExampleAgent::ExampleAgent( const QString &id ) + : AgentBase( id ) + , mObserver( 0 ) + * { + * mObserver = new ExampleObserver(); + * registerObserver( mObserver ); + * } + * + * ExampleAgent::~ExampleAgent() + * { + * delete mObserver; + * } + * + * void ExampleObserver::itemChanged( const Item &item ) + * { + * // do something with item + * qCDebug(AKONADIAGENTBASE_LOG) << "Item id=" << item.id(); + * + * // let base implementation tell the change recorder that we + * // have processed the change + * AgentBase::Observer::itemChanged( item ); + * } + * @endcode + * + * Example for observer through multiple inheritance: + * @code + * class ExampleAgent : public AgentBase, public AgentBase::Observer + * { + * public: + * ExampleAgent( const QString &id ); + * + * protected: + * void itemChanged( const Item &item ); + * }; + * + * ExampleAgent::ExampleAgent( const QString &id ) + : AgentBase( id ) + * { + * // no need to create or register observer since + * // we are the observer and registration happens automatically + * // in init() + * } + * + * void ExampleAgent::itemChanged( const Item &item ) + * { + * // do something with item + * qCDebug(AKONADIAGENTBASE_LOG) << "Item id=" << item.id(); + * + * // let base implementation tell the change recorder that we + * // have processed the change + * AgentBase::Observer::itemChanged( item ); + * } + * @endcode + * + * @author Kevin Krammer + * + * @deprecated Use ObserverV2 instead + */ + class AKONADIAGENTBASE_EXPORT Observer // krazy:exclude=dpointer + { + public: + /** + * Creates an observer instance. + */ + Observer(); + + /** + * Destroys the observer instance. + */ + virtual ~Observer(); + + /** + * Reimplement to handle adding of new items. + * @param item The newly added item. + * @param collection The collection @p item got added to. + */ + virtual void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * Reimplement to handle changes to existing items. + * @param item The changed item. + * @param partIdentifiers The identifiers of the item parts that has been changed. + */ + virtual void itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers); + + /** + * Reimplement to handle deletion of items. + * @param item The deleted item. + */ + virtual void itemRemoved(const Akonadi::Item &item); + + /** + * Reimplement to handle adding of new collections. + * @param collection The newly added collection. + * @param parent The parent collection. + */ + virtual void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + + /** + * Reimplement to handle changes to existing collections. + * @param collection The changed collection. + */ + virtual void collectionChanged(const Akonadi::Collection &collection); + + /** + * Reimplement to handle deletion of collections. + * @param collection The deleted collection. + */ + virtual void collectionRemoved(const Akonadi::Collection &collection); + }; + + /** + * BC extension of Observer with support for monitoring item and collection moves. + * Use this one instead of Observer. + * + * @since 4.4 + */ + class AKONADIAGENTBASE_EXPORT ObserverV2 : public Observer // krazy:exclude=dpointer + { + public: + using Observer::collectionChanged; + + /** + * Reimplement to handle item moves. + * When using this class in combination with Akonadi::ResourceBase, inter-resource + * moves are handled internally already and the corresponding add or delete method + * is called instead. + * + * @param item The moved item. + * @param collectionSource The collection the item has been moved from. + * @param collectionDestination The collection the item has been moved to. + */ + virtual void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource, + const Akonadi::Collection &collectionDestination); + + /** + * Reimplement to handle item linking. + * This is only relevant for virtual resources. + * @param item The linked item. + * @param collection The collection the item is linked to. + */ + virtual void itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * Reimplement to handle item unlinking. + * This is only relevant for virtual resources. + * @param item The unlinked item. + * @param collection The collection the item is unlinked from. + */ + virtual void itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * Reimplement to handle collection moves. + * When using this class in combination with Akonadi::ResourceBase, inter-resource + * moves are handled internally already and the corresponding add or delete method + * is called instead. + * + * @param collection The moved collection. + * @param collectionSource The previous parent collection. + * @param collectionDestination The new parent collection. + */ + virtual void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource, + const Akonadi::Collection &collectionDestination); + + /** + * Reimplement to handle changes to existing collections. + * @param collection The changed collection. + * @param changedAttributes The identifiers of the collection parts/attributes that has been changed. + */ + virtual void collectionChanged(const Akonadi::Collection &collection, const QSet &changedAttributes); + }; + + /** + * BC extension of ObserverV2 with support for batch operations + * + * @warning When using ObserverV3, you will never get single-item notifications + * from AgentBase::Observer, even when you don't reimplement corresponding batch + * method from ObserverV3. For instance, when you don't reimplement itemsRemoved() + * here, you will not get any notifications about item removal whatsoever! + * + * @since 4.11 + */ + class AKONADIAGENTBASE_EXPORT ObserverV3 : public ObserverV2 // krazy:exclude=dpointer + { + public: + /** + * Reimplement to handle changes in flags of existing items + * + * @warning When using ObserverV3, you will never get notifications about + * flag changes via Observer::itemChanged(), even when you don't reimplement + * itemsFlagsChanged()! + * + * @param item The changed item. + * @param addedFlags Flags that have been added to the item + * @param removedFlags Flags that have been removed from the item + */ + virtual void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags); + + /** + * Reimplement to handle batch notification about items deletion. + * + * @param items List of deleted items + */ + virtual void itemsRemoved(const Akonadi::Item::List &items); + + /** + * Reimplement to handle batch notification about items move + * + * @param items List of moved items + * @param sourceCollection Collection from where the items were moved + * @param destinationCollection Collection to which the items were moved + */ + virtual void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &sourceCollection, + const Akonadi::Collection &destinationCollection); + + /** + * Reimplement to handle batch notifications about items linking. + * + * @param items Linked items + * @param collection Collection to which the items have been linked + */ + virtual void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + + /** + * Reimplement to handle batch notifications about items unlinking. + * + * @param items Unlinked items + * @param collection Collection from which the items have been unlinked + */ + virtual void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + }; + + /** + * Observer that adds support for item tagging + * + * @warning ObserverV4 subclasses ObserverV3 which changes behavior of some of the + * virtual methods from Observer and ObserverV2. Please make sure you read + * documentation of ObserverV3 and adapt your agent accordingly. + * + * @since 4.13 + */ + class AKONADIAGENTBASE_EXPORT ObserverV4 : public ObserverV3 // krazy:exclude=dpointer + { + public: + /** + * Reimplement to handle tags additions + * + * @param tag Newly added tag + */ + virtual void tagAdded(const Akonadi::Tag &tag); + + /** + * Reimplement to handle tags changes + * + * @param tag Tag that has been changed + */ + virtual void tagChanged(const Akonadi::Tag &tag); + + /** + * Reimplement to handle tags removal. + * + * @note All items that were tagged by @p tag will get a separate notification + * about untagging via itemsTagsChanged(). It is guaranteed that the itemsTagsChanged() + * notification will be delivered before this one. + * + * @param tag Tag that has been removed. + */ + virtual void tagRemoved(const Akonadi::Tag &tag); + + /** + * Reimplement to handle items tagging + * + * @param items Items that were tagged or untagged + * @param addedTags Set of tags that were added to all @p items + * @param removedTags Set of tags that were removed from all @p items + */ + virtual void itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, const QSet &removedTags); + + /** + * Reimplement to handle relations being added + */ + virtual void relationAdded(const Akonadi::Relation &relation); + + /** + * Reimplement to handle relations being removed + */ + virtual void relationRemoved(const Akonadi::Relation &relation); + + /** + * Reimplement to handled relations changing on items + * @param items Items that had relations added/removed from them + * @param addedRelations the list of relations that were added to all @p items + * @param removedRelations the list of relations that were removed from all @p items + */ + virtual void itemsRelationsChanged(const Akonadi::Item::List &items, + const Akonadi::Relation::List &addedRelations, + const Akonadi::Relation::List &removedRelations); + }; + + /** + * This enum describes the different states the + * agent can be in. + */ + enum Status { + Idle = 0, ///< The agent does currently nothing. + Running, ///< The agent is working on something. + Broken, ///< The agent encountered an error state. + NotConfigured ///< The agent is lacking required configuration + }; + + /** + * Use this method in the main function of your agent + * application to initialize your agent subclass. + * This method also takes care of creating a KApplication + * object and parsing command line arguments. + * + * @note In case the given class is also derived from AgentBase::Observer + * it gets registered as its own observer (see AgentBase::Observer), e.g. + * agentInstance->registerObserver( agentInstance ); + * + * @code + * + * class MyAgent : public AgentBase + * { + * ... + * }; + * + * AKONADI_AGENT_MAIN( MyAgent ) + * + * @endcode + * + * @param argc number of arguments + * @param argv arguments for the function + */ + template + static int init(int argc, char **argv) + { + // Disable session management + qunsetenv("SESSION_MANAGER"); + + QApplication app(argc, argv); + const QString id = parseArguments(argc, argv); + T *r = new T(id); + + // check if T also inherits AgentBase::Observer and + // if it does, automatically register it on itself + Observer *observer = dynamic_cast(r); + if (observer != 0) { + r->registerObserver(observer); + } + return init(r); + } + + /** + * This method returns the current status code of the agent. + * + * The following return values are possible: + * + * - 0 - Idle + * - 1 - Running + * - 2 - Broken + * - 3 - NotConfigured + */ + virtual int status() const; + + /** + * This method returns an i18n'ed description of the current status code. + */ + virtual QString statusMessage() const; + + /** + * This method returns the current progress of the agent in percentage. + */ + virtual int progress() const; + + /** + * This method returns an i18n'ed description of the current progress. + */ + virtual QString progressMessage() const; + +public Q_SLOTS: + /** + * This method is called whenever the agent shall show its configuration dialog + * to the user. It will be automatically called when the agent is started for + * the first time. + * + * @param windowId The parent window id. + * + * @note If the method is reimplemented it has to emit the configurationDialogAccepted() + * or configurationDialogRejected() signals depending on the users choice. + */ + virtual void configure(WId windowId); + +public: + /** + * This method returns the windows id, which should be used for dialogs. + */ + WId winIdForDialogs() const; + +#ifdef Q_OS_WIN + /** + * Overload of @ref configure needed because WId cannot be automatically casted + * to qlonglong on Windows. + */ + void configure(qlonglong windowId); +#endif + + /** + * Returns the instance identifier of this agent. + */ + QString identifier() const; + + /** + * This method is called when the agent is removed from + * the system, so it can do some cleanup stuff. + * + * @note If you reimplement this in a subclass make sure + * to call this base implementation at the end. + */ + virtual void cleanup(); + + /** + * Registers the given observer for reacting on monitored or recorded changes. + * + * @param observer The change handler to register. No ownership transfer, i.e. + * the caller stays owner of the pointer and can reset + * the registration by calling this method with @c 0 + */ + void registerObserver(Observer *observer); + + /** + * This method is used to set the name of the agent. + * + * @since 4.3 + * @param name name of the agent + */ + //FIXME_API: make sure location is renamed to this by agentbase + void setAgentName(const QString &name); + + /** + * Returns the name of the agent. + * + * @since 4.3 + */ + QString agentName() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever the name of the agent has changed. + * + * @param name The new name of the agent. + * + * @since 4.3 + */ + void agentNameChanged(const QString &name); + + /** + * This signal should be emitted whenever the status of the agent has been changed. + * @param status The new Status code. + * @param message A i18n'ed description of the new status. + */ + void status(int status, const QString &message = QString()); + + /** + * This signal should be emitted whenever the progress of an action in the agent + * (e.g. data transfer, connection establishment to remote server etc.) has changed. + * + * @param progress The progress of the action in percent. + */ + void percent(int progress); + + /** + * This signal shall be used to report warnings. + * + * @param message The i18n'ed warning message. + */ + void warning(const QString &message); + + /** + * This signal shall be used to report errors. + * + * @param message The i18n'ed error message. + */ + void error(const QString &message); + + /** + * This signal should be emitted whenever the status of the agent has been changed. + * @param status The object that describes the status change. + * + * @since 4.6 + */ + void advancedStatus(const QVariantMap &status); + + /** + * Emitted when another application has remotely asked the agent to abort + * its current operation. + * Connect to this signal if your agent supports abortion. After aborting + * and cleaning up, agents should return to Idle status. + * + * @since 4.4 + */ + void abortRequested(); + + /** + * Emitted if another application has changed the agent's configuration remotely + * and called AgentInstance::reconfigure(). + * + * @since 4.2 + */ + void reloadConfiguration(); + + /** + * Emitted when the online state changed. + * @param online The online state. + * @since 4.2 + */ + void onlineChanged(bool online); + + /** + * This signal is emitted whenever the user has accepted the configuration dialog. + * + * @note Implementors of agents/resources are responsible to emit this signal if + * the agent/resource reimplements configure(). + * + * @since 4.4 + */ + void configurationDialogAccepted(); + + /** + * This signal is emitted whenever the user has rejected the configuration dialog. + * + * @note Implementors of agents/resources are responsible to emit this signal if + * the agent/resource reimplements configure(). + * + * @since 4.4 + */ + void configurationDialogRejected(); + +protected: + /** + * Creates an agent base. + * + * @param id The instance id of the agent. + */ + AgentBase(const QString &id); + + /** + * Destroys the agent base. + */ + ~AgentBase(); + + /** + * This method is called whenever the agent application is about to + * quit. + * + * Reimplement this method to do session cleanup (e.g. disconnecting + * from groupware server). + */ + virtual void aboutToQuit(); + + /** + * Returns the Akonadi::ChangeRecorder object used for monitoring. + * Use this to configure which parts you want to monitor. + */ + ChangeRecorder *changeRecorder() const; + + /** + * Returns the config object for this Agent. + */ + KSharedConfigPtr config(); + + /** + * Marks the current change as processes and replays the next change if change + * recording is enabled (noop otherwise). This method is called + * from the default implementation of the change notification slots. While not + * required when not using change recording, it is nevertheless recommended + * to call this method when done with processing a change notification. + */ + void changeProcessed(); + + /** + * Returns whether the agent is currently online. + */ + bool isOnline() const; + + /** + * Sets whether the agent needs network or not. + * + * @since 4.2 + * @todo use this in combination with QNetworkConfiguration to change + * the onLine status of the agent. + * @param needsNetwork @c true if the agents needs network. Defaults to @c false + */ + void setNeedsNetwork(bool needsNetwork); + + /** + * Sets whether the agent shall be online or not. + */ + void setOnline(bool state); + +protected: + /** + * Sets the agent offline but will make it online again after a given time + * + * Use this method when the agent detects some problem with its backend but it wants + * to retry all pending operations after some time - e.g. a server can not be reached currently + * + * Example usage: + * @code + * void ExampleResource::onItemRemovedFinished(KJob *job) + * { + * if (job->error()) { + * emit status(Broken, job->errorString()); + * deferTask(); + * setTemporaryOffline(300); + * return; + * } + * ... + * } + * @endcode + * + * @since 4.13 + * @param makeOnlineInSeconds timeout in seconds after which the agent changes to online + */ + void setTemporaryOffline(int makeOnlineInSeconds = 300); + + //@cond PRIVATE + AgentBasePrivate *d_ptr; + explicit AgentBase(AgentBasePrivate *d, const QString &id); + friend class ObserverV2; + //@endcond + + /** + * This method is called whenever the @p online status has changed. + * Reimplement this method to react on online status changes. + * @param online online status + */ + virtual void doSetOnline(bool online); + +private: + //@cond PRIVATE + static QString parseArguments(int argc, char **argv); + static int init(AgentBase *r); + void setOnlineInternal(bool state); + + // D-Bus interface stuff + void abort(); + void reconfigure(); + void quit(); + + // dbus agent interface + friend class ::Akonadi__StatusAdaptor; + friend class ::Akonadi__ControlAdaptor; + + Q_DECLARE_PRIVATE(AgentBase) + Q_PRIVATE_SLOT(d_func(), void delayedInit()) + Q_PRIVATE_SLOT(d_func(), void slotStatus(int, const QString &)) + Q_PRIVATE_SLOT(d_func(), void slotPercent(int)) + Q_PRIVATE_SLOT(d_func(), void slotWarning(const QString &)) + Q_PRIVATE_SLOT(d_func(), void slotError(const QString &)) + Q_PRIVATE_SLOT(d_func(), void slotNetworkStatusChange(bool)) + Q_PRIVATE_SLOT(d_func(), void slotResumedFromSuspend()) + Q_PRIVATE_SLOT(d_func(), void slotTemporaryOfflineTimeout()) + + //@endcond +}; + +} + +#ifndef AKONADI_AGENT_MAIN +/** + * Convenience Macro for the most common main() function for Akonadi agents. + */ +#define AKONADI_AGENT_MAIN( agentClass ) \ + int main( int argc, char **argv ) \ + { \ + return Akonadi::AgentBase::init( argc, argv ); \ + } +#endif + +#endif diff --git a/src/agentbase/agentbase_p.h b/src/agentbase/agentbase_p.h new file mode 100644 index 0000000..45d774f --- /dev/null +++ b/src/agentbase/agentbase_p.h @@ -0,0 +1,157 @@ +/* + Copyright (c) 2007 Volker Krause + Copyright (c) 2008 Kevin Krammer + + 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_AGENTBASE_P_H +#define AKONADI_AGENTBASE_P_H + +#include "agentbase.h" +#include "tracerinterface.h" + +#include + +class QSettings; +class QTimer; +class QNetworkConfigurationManager; + +namespace Akonadi +{ + +/** + * @internal + */ +class AgentBasePrivate : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.dfaure") + +public: + explicit AgentBasePrivate(AgentBase *parent); + virtual ~AgentBasePrivate(); + void init(); + virtual void delayedInit(); + + void slotStatus(int status, const QString &message); + void slotPercent(int progress); + void slotWarning(const QString &message); + void slotError(const QString &message); + void slotNetworkStatusChange(bool isOnline); + void slotResumedFromSuspend(); + void slotTemporaryOfflineTimeout(); + + virtual void changeProcessed(); + + QString defaultReadyMessage() const + { + if (mOnline) { + return i18nc("@info:status Application ready for work", "Ready"); + } + return i18nc("@info:status", "Offline"); + } + + QString defaultSyncingMessage() const + { + return i18nc("@info:status", "Syncing..."); + } + + QString defaultErrorMessage() const + { + return i18nc("@info:status", "Error."); + } + + QString defaultUnconfiguredMessage() const + { + return i18nc("@info:status", "Not configured"); + } + + void setProgramName(); + + AgentBase *q_ptr; + Q_DECLARE_PUBLIC(AgentBase) + + QString mId; + QString mName; + QString mResourceTypeName; + + int mStatusCode; + QString mStatusMessage; + + uint mProgress; + QString mProgressMessage; + + bool mNeedsNetwork; + bool mOnline; + bool mDesiredOnlineState; + + QSettings *mSettings; + + ChangeRecorder *mChangeRecorder; + + org::freedesktop::Akonadi::Tracer *mTracer; + + AgentBase::Observer *mObserver; + QDBusInterface *mPowerInterface; + + QTimer *mTemporaryOfflineTimer; + + QEventLoopLocker *mEventLoopLocker; + QNetworkConfigurationManager *mNetworkManager; + +public Q_SLOTS: + // Dump the contents of the current ChangeReplay + Q_SCRIPTABLE QString dumpNotificationListToString() const; + Q_SCRIPTABLE void dumpMemoryInfo() const; + Q_SCRIPTABLE QString dumpMemoryInfoToString() const; + + virtual void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); + virtual void itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers); + virtual void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &destination); + virtual void itemRemoved(const Akonadi::Item &item); + void itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + void itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + virtual void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags); + virtual void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &source, const Akonadi::Collection &destination); + virtual void itemsRemoved(const Akonadi::Item::List &items); + virtual void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + virtual void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + + virtual void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + virtual void collectionChanged(const Akonadi::Collection &collection); + virtual void collectionChanged(const Akonadi::Collection &collection, const QSet &changedAttributes); + virtual void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination); + virtual void collectionRemoved(const Akonadi::Collection &collection); + void collectionSubscribed(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + void collectionUnsubscribed(const Akonadi::Collection &collection); + + virtual void tagAdded(const Akonadi::Tag &tag); + virtual void tagChanged(const Akonadi::Tag &tag); + virtual void tagRemoved(const Akonadi::Tag &tag); + virtual void itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, const QSet &removedTags); + + virtual void relationAdded(const Akonadi::Relation &relation); + virtual void relationRemoved(const Akonadi::Relation &relation); + virtual void itemsRelationsChanged(const Akonadi::Item::List &items, + const Akonadi::Relation::List &addedRelations, + const Akonadi::Relation::List &removedRelations); +}; + +} + +#endif diff --git a/src/agentbase/agentfactory.cpp b/src/agentbase/agentfactory.cpp new file mode 100644 index 0000000..c5c6ed2 --- /dev/null +++ b/src/agentbase/agentfactory.cpp @@ -0,0 +1,72 @@ +/* + This file is part of akonadiresources. + + Copyright (c) 2006 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 "agentfactory.h" +#include "servermanager.h" +#include "servermanager_p.h" + +#include +#include + +#include +#include + +QThreadStorage s_agentComponentDatas; + +using namespace Akonadi; + +class Akonadi::AgentFactoryBasePrivate +{ +public: + QString catalogName; +}; + +AgentFactoryBase::AgentFactoryBase(const char *catalogName, QObject *parent) + : QObject(parent) + , d(new AgentFactoryBasePrivate) +{ + d->catalogName = QString::fromLatin1(catalogName); + if (!KGlobal::hasMainComponent()) { + new KComponentData("AkonadiAgentServer", "libakonadi", KComponentData::RegisterAsMainComponent); + } + KLocalizedString::setApplicationDomain(catalogName); + + Internal::setClientType(Internal::Agent); + ServerManager::self(); // make sure it's created in the main thread +} + +AgentFactoryBase::~AgentFactoryBase() +{ + delete d; +} + +void AgentFactoryBase::createComponentData(const QString &identifier) const +{ + Q_ASSERT(!s_agentComponentDatas.hasLocalData()); + + if (QThread::currentThread() != QCoreApplication::instance()->thread()) { + s_agentComponentDatas.setLocalData(new KComponentData(ServerManager::addNamespace(identifier).toLatin1(), d->catalogName.toLatin1(), + KComponentData::SkipMainComponentRegistration)); + } else { + s_agentComponentDatas.setLocalData(new KComponentData(ServerManager::addNamespace(identifier).toLatin1(), d->catalogName.toLatin1())); + } +} diff --git a/src/agentbase/agentfactory.h b/src/agentbase/agentfactory.h new file mode 100644 index 0000000..190263a --- /dev/null +++ b/src/agentbase/agentfactory.h @@ -0,0 +1,108 @@ +/* + This file is part of akonadiresources. + + Copyright (c) 2006 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_AGENTFACTORY_H +#define AKONADI_AGENTFACTORY_H + +#include "akonadiagentbase_export.h" +#include "agentbase.h" + +#include +#include + +namespace Akonadi +{ + +class AgentFactoryBasePrivate; + +/** + * @short A factory base class for in-process agents. + * + * @see AKONADI_AGENT_FACTORY() + * @internal + * @since 4.6 + */ +class AKONADIAGENTBASE_EXPORT AgentFactoryBase : public QObject +{ + Q_OBJECT + +public: + /** + * Creates a new agent factory. + * Executed in the main thread, performs KDE infrastructure setup. + * + * @param catalogName The translation catalog of this resource. + * @param parent The parent object. + */ + explicit AgentFactoryBase(const char *catalogName, QObject *parent = Q_NULLPTR); + + virtual ~AgentFactoryBase(); + +public Q_SLOTS: + /** + * Creates a new agent instace with the given identifier. + */ + virtual QObject *createInstance(const QString &identifier) const = 0; + +protected: + void createComponentData(const QString &identifier) const; + +private: + AgentFactoryBasePrivate *const d; +}; + +/** + * @short A factory for in-process agents. + * + * @see AKONADI_AGENT_FACTORY() + * @internal + * @since 4.6 + */ +template +class AgentFactory : public AgentFactoryBase +{ +public: + /** reimplemented */ + explicit AgentFactory(const char *catalogName, QObject *parent = Q_NULLPTR) + : AgentFactoryBase(catalogName, parent) + { + } + + QObject *createInstance(const QString &identifier) const Q_DECL_OVERRIDE + { + createComponentData(identifier); + T *instance = new T(identifier); + + // check if T also inherits AgentBase::Observer and + // if it does, automatically register it on itself + Akonadi::AgentBase::Observer *observer = dynamic_cast(instance); + if (observer != 0) { + instance->registerObserver(observer); + } + + return instance; + } +}; + +} + +#endif diff --git a/src/agentbase/agentsearchinterface.cpp b/src/agentbase/agentsearchinterface.cpp new file mode 100644 index 0000000..9e30d35 --- /dev/null +++ b/src/agentbase/agentsearchinterface.cpp @@ -0,0 +1,149 @@ +/* + 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 "agentsearchinterface.h" +#include "akonadiagentbase_debug.h" +#include "agentsearchinterface_p.h" +#include "collection.h" +#include "KDBusConnectionPool" +#include "searchresultjob_p.h" +#include "searchadaptor.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "servermanager.h" +#include "agentbase.h" +#include "private/imapset_p.h" + +using namespace Akonadi; + +AgentSearchInterfacePrivate::AgentSearchInterfacePrivate(AgentSearchInterface *qq) + : q(qq) +{ + new Akonadi__SearchAdaptor(this); + KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Search"), + this, QDBusConnection::ExportAdaptors); + + QTimer::singleShot(0, this, &AgentSearchInterfacePrivate::delayedInit); +} + +void AgentSearchInterfacePrivate::delayedInit() +{ + QDBusInterface iface(ServerManager::serviceName(ServerManager::Server), + QStringLiteral("/SearchManager"), + QStringLiteral("org.freedesktop.Akonadi.SearchManager"), + QDBusConnection::sessionBus(), this); + QDBusMessage msg = iface.call(QStringLiteral("registerInstance"), dynamic_cast(q)->identifier()); +} + +void AgentSearchInterfacePrivate::addSearch(const QString &query, const QString &queryLanguage, quint64 resultCollectionId) +{ + q->addSearch(query, queryLanguage, Collection(resultCollectionId)); +} + +void AgentSearchInterfacePrivate::removeSearch(quint64 resultCollectionId) +{ + q->removeSearch(Collection(resultCollectionId)); +} + +void AgentSearchInterfacePrivate::search(const QByteArray &searchId, + const QString &query, + quint64 collectionId) +{ + mSearchId = searchId; + mCollectionId = collectionId; + + CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection(mCollectionId), CollectionFetchJob::Base, this); + fetchJob->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + fetchJob->setProperty("query", query); + connect(fetchJob, &KJob::finished, this, &AgentSearchInterfacePrivate::collectionReceived); +} + +void AgentSearchInterfacePrivate::collectionReceived(KJob *job) +{ + CollectionFetchJob *fetchJob = qobject_cast(job); + if (fetchJob->error()) { + qCCritical(AKONADIAGENTBASE_LOG) << fetchJob->errorString(); + new SearchResultJob(fetchJob->property("searchId").toByteArray(), Collection(mCollectionId), this); + return; + } + + if (fetchJob->collections().count() != 1) { + qCDebug(AKONADIAGENTBASE_LOG) << "Server requested search in invalid collection, or collection was removed in the meanwhile"; + // Tell server we are done + new SearchResultJob(fetchJob->property("searchId").toByteArray(), Collection(mCollectionId), this); + return; + } + + const Collection collection = fetchJob->collections().at(0); + q->search(fetchJob->property("query").toString(), + collection); +} + +AgentSearchInterface::AgentSearchInterface() + : d(new AgentSearchInterfacePrivate(this)) +{ +} + +AgentSearchInterface::~AgentSearchInterface() +{ + delete d; +} + +void AgentSearchInterface::searchFinished(const QVector &result, ResultScope scope) +{ + if (scope == Akonadi::AgentSearchInterface::Rid) { + QVector rids; + rids.reserve(result.size()); + Q_FOREACH (qint64 rid, result) { + rids << QByteArray::number(rid); + } + + searchFinished(rids); + return; + } + + SearchResultJob *resultJob = new SearchResultJob(d->mSearchId, Collection(d->mCollectionId), d); + resultJob->setResult(result); +} + +void AgentSearchInterface::searchFinished(const ImapSet &result, ResultScope scope) +{ + if (scope == Akonadi::AgentSearchInterface::Rid) { + QVector rids; + Q_FOREACH (const ImapInterval &interval, result.intervals()) { + for (int i = interval.begin(); i <= interval.end(); ++i) { + rids << QByteArray::number(i); + } + } + + searchFinished(rids); + return; + } + + SearchResultJob *resultJob = new SearchResultJob(d->mSearchId, Collection(d->mCollectionId), d); + resultJob->setResult(result); +} + +void AgentSearchInterface::searchFinished(const QVector &result) +{ + SearchResultJob *resultJob = new SearchResultJob(d->mSearchId, Collection(d->mCollectionId), d); + resultJob->setResult(result); +} + +#include "moc_agentsearchinterface_p.cpp" diff --git a/src/agentbase/agentsearchinterface.h b/src/agentbase/agentsearchinterface.h new file mode 100644 index 0000000..68a886f --- /dev/null +++ b/src/agentbase/agentsearchinterface.h @@ -0,0 +1,97 @@ +/* + 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. +*/ + +#ifndef AKONADI_AGENTSEARCHINTERFACE_H +#define AKONADI_AGENTSEARCHINTERFACE_H + +#include "akonadiagentbase_export.h" +#include + +namespace Akonadi +{ + +class Collection; +class AgentSearchInterfacePrivate; +class ImapSet; + +/** + * @short An interface for agents (or resources) that support searching in their backend. + * + * Inherit from this additionally to Akonadi::AgentBase (or Akonadi::ResourceBase) + * and implement its two pure virtual methods. + * + * Make sure to add the @c Search capability to the agent desktop file. + * + * @since 4.5 + */ +class AKONADIAGENTBASE_EXPORT AgentSearchInterface +{ +public: + enum ResultScope { + Uid, + Rid + }; + + /** + * Creates a new agent search interface. + */ + AgentSearchInterface(); + + /** + * Destroys the agent search interface. + */ + virtual ~AgentSearchInterface(); + + /** + * Adds a new search. + * + * @param query The query string, using the language specified in @p queryLanguage + * @param queryLanguage The query language used for @p query + * @param resultCollection The destination collection for the search results. It's a virtual + * collection, results can be added/removed using Akonadi::LinkJob and Akonadi::UnlinkJob respectively. + */ + virtual void addSearch(const QString &query, const QString &queryLanguage, const Akonadi::Collection &resultCollection) = 0; + + /** + * Removes a previously added search. + * @param resultCollection The result collection given in an previous addSearch() call. + * You do not need to take care of deleting results in there, the collection is just provided as a way to + * identify the search. + */ + virtual void removeSearch(const Akonadi::Collection &resultCollection) = 0; + + /** + * Perform a search on remote storage and return results using SearchResultJob. + * + * @since 4.13 + */ + virtual void search(const QString &query, const Collection &collection) = 0; + + void searchFinished(const QVector &result, ResultScope scope); + void searchFinished(const ImapSet &result, ResultScope scope); + void searchFinished(const QVector &result); +private: + //@cond PRIVATE + AgentSearchInterfacePrivate *const d; + //@endcond +}; + +} + +#endif diff --git a/src/agentbase/agentsearchinterface_p.h b/src/agentbase/agentsearchinterface_p.h new file mode 100644 index 0000000..a598f2d --- /dev/null +++ b/src/agentbase/agentsearchinterface_p.h @@ -0,0 +1,56 @@ +/* + 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. +*/ + +#ifndef AKOANDI_AGENTSEARCHINTERFACE_P_H +#define AKOANDI_AGENTSEARCHINTERFACE_P_H + +#include "agentsearchinterface.h" +#include "collection.h" + +#include + +class KJob; + +namespace Akonadi +{ + +class AgentSearchInterfacePrivate : public QObject +{ + Q_OBJECT +public: + explicit AgentSearchInterfacePrivate(AgentSearchInterface *qq); + + void search(const QByteArray &searchId, const QString &query, quint64 collectionId); + void addSearch(const QString &query, const QString &queryLanguage, quint64 resultCollectionId); + void removeSearch(quint64 resultCollectionId); + + QByteArray mSearchId; + qint64 mCollectionId; + +private Q_SLOTS: + void delayedInit(); + void collectionReceived(KJob *job); + +private: + AgentSearchInterface *q; +}; + +} + +#endif diff --git a/src/agentbase/preprocessorbase.cpp b/src/agentbase/preprocessorbase.cpp new file mode 100644 index 0000000..3d4589e --- /dev/null +++ b/src/agentbase/preprocessorbase.cpp @@ -0,0 +1,63 @@ +/****************************************************************************** + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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 "preprocessorbase.h" + +#include "preprocessorbase_p.h" + +#include "akonadiagentbase_debug.h" + +using namespace Akonadi; + +PreprocessorBase::PreprocessorBase(const QString &id) + : AgentBase(new PreprocessorBasePrivate(this), id) +{ +} + +PreprocessorBase::~PreprocessorBase() +{ +} + +void PreprocessorBase::finishProcessing(ProcessingResult result) +{ + Q_D(PreprocessorBase); + + Q_ASSERT_X(result != ProcessingDelayed, "PreprocessorBase::terminateProcessing", "You should never pass ProcessingDelayed to this function"); + Q_ASSERT_X(d->mInDelayedProcessing, "PreprocessorBase::terminateProcessing", "terminateProcessing() called while not in delayed processing mode"); + Q_UNUSED(result); + + d->mInDelayedProcessing = false; + emit d->itemProcessed(d->mDelayedProcessingItemId); +} + +void PreprocessorBase::setFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(PreprocessorBase); + + d->mFetchScope = fetchScope; +} + +ItemFetchScope &PreprocessorBase::fetchScope() +{ + Q_D(PreprocessorBase); + + return d->mFetchScope; +} diff --git a/src/agentbase/preprocessorbase.h b/src/agentbase/preprocessorbase.h new file mode 100644 index 0000000..e82f9ea --- /dev/null +++ b/src/agentbase/preprocessorbase.h @@ -0,0 +1,189 @@ +/****************************************************************************** + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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_PREPROCESSORBASE_H +#define AKONADI_PREPROCESSORBASE_H + +#include "akonadiagentbase_export.h" +#include "agentbase.h" +#include "collection.h" +#include "item.h" + +namespace Akonadi +{ + +class ItemFetchScope; + +class PreprocessorBasePrivate; + +/** + * @short The base class for all Akonadi preprocessor agents. + * + * This class should be used as a base class by all preprocessor agents + * since it encapsulates large parts of the protocol between + * preprocessor agent, agent manager and the Akonadi storage. + * + * Preprocessor agents are special agents that are informed about newly + * added items before any other agents. This allows them to do filtering + * on the items or any other task that shall be done before the new item + * is visible in the Akonadi storage system. + * + * The method all the preprocessors must implement is processItem(). + * + * @author Szymon Stefanek + * @since 4.4 + */ +class AKONADIAGENTBASE_EXPORT PreprocessorBase : public AgentBase +{ + Q_OBJECT + +public: + /** + * Describes the possible return values of the processItem() method. + */ + enum ProcessingResult { + /** + * Processing completed successfully for this item. + * The Akonadi server will push in a new item when it's available. + */ + ProcessingCompleted, + + /** + * Processing was delayed to a later stage. + * This must be returned when implementing asynchronous preprocessing. + * + * If this value is returned, finishProcessing() has to be called + * when processing is done. + */ + ProcessingDelayed, + + /** + * Processing for this item failed (and the failure is unrecoverable). + * The Akonadi server will push in a new item when it's available, + * after possibly logging the failure. + */ + ProcessingFailed, + + /** + * Processing for this item was refused. This is very + * similar to ProcessingFailed above but additionally remarks + * that the item that the Akonadi server pushed in wasn't + * meant for this Preprocessor. + * The Akonadi server will push in a new item when it's available, + * after possibly logging the failure and maybe taking some additional action. + */ + ProcessingRefused + }; + + /** + * This method must be implemented by every preprocessor subclass. + * @param item the item to process + * It must realize the preprocessing of the given @p item. + * + * The Akonadi server will push in for preprocessing any newly created item: + * it's your responsibility to decide if you want to process the item or not. + * + * The method should return ProcessingCompleted on success, ProcessingDelayed + * if processing is implemented asynchronously and + * ProcessingRefused or ProcessingFailed if the processing + * didn't complete. + * + * If your operation is asynchronous then you should also + * connect to the abortRequested() signal and handle it + * appropriately (as the server MAY abort your async job + * if it decides that it's taking too long). + */ + virtual ProcessingResult processItem(const Item &item) = 0; + + /** + * This method must be called if processing is implemented asynchronously. + * @param result the processing result + * You should call it when you have completed the processing + * or if an abortRequest() signal arrives (and in this case you + * will probably use ProcessingFailed as result). + * + * Valid values for @p result are ProcessingCompleted, + * PocessingRefused and ProcessingFailed. Passing any + * other value will lead to a runtime assertion. + */ + void finishProcessing(ProcessingResult result); + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(const ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + +protected: + /** + * Creates a new preprocessor base agent. + * + * @param id The instance id of the preprocessor base agent. + */ + PreprocessorBase(const QString &id); + + /** + * Destroys the preprocessor base agent. + */ + virtual ~PreprocessorBase(); + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(PreprocessorBase) + //@endcond + +}; // class PreprocessorBase + +} // namespace Akonadi + +#ifndef AKONADI_PREPROCESSOR_MAIN +/** + * Convenience Macro for the most common main() function for Akonadi preprocessors. + */ +#define AKONADI_PREPROCESSOR_MAIN( preProcessorClass ) \ + int main( int argc, char **argv ) \ + { \ + return Akonadi::PreprocessorBase::init( argc, argv ); \ + } +#endif //!AKONADI_RESOURCE_MAIN + +#endif //!_PREPROCESSORBASE_H_ diff --git a/src/agentbase/preprocessorbase_p.cpp b/src/agentbase/preprocessorbase_p.cpp new file mode 100644 index 0000000..5950ecb --- /dev/null +++ b/src/agentbase/preprocessorbase_p.cpp @@ -0,0 +1,102 @@ +/* + Copyright (c) 2009 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. +*/ + +#include "preprocessorbase_p.h" +#include "preprocessorbase.h" + +#include "KDBusConnectionPool" +#include "preprocessoradaptor.h" +#include "servermanager.h" + +#include "itemfetchjob.h" +#include "akonadiagentbase_debug.h" + +using namespace Akonadi; + +PreprocessorBasePrivate::PreprocessorBasePrivate(PreprocessorBase *parent) + : AgentBasePrivate(parent) + , mInDelayedProcessing(false) + , mDelayedProcessingItemId(0) +{ + Q_Q(PreprocessorBase); + + new Akonadi__PreprocessorAdaptor(this); + + if (!KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Preprocessor"), this, QDBusConnection::ExportAdaptors)) { + q->error(i18n("Unable to register object at dbus: %1", KDBusConnectionPool::threadConnection().lastError().message())); + } + +} + +void PreprocessorBasePrivate::delayedInit() +{ + if (!KDBusConnectionPool::threadConnection().registerService(ServerManager::agentServiceName(ServerManager::Preprocessor, mId))) { + qCCritical(AKONADIAGENTBASE_LOG) << "Unable to register service at D-Bus: " + << KDBusConnectionPool::threadConnection().lastError().message(); + } + AgentBasePrivate::delayedInit(); +} + +void PreprocessorBasePrivate::beginProcessItem(qlonglong itemId, qlonglong collectionId, const QString &mimeType) +{ + qCDebug(AKONADIAGENTBASE_LOG) << "PreprocessorBase: about to process item " << itemId << " in collection " << collectionId << " with mimeType " << mimeType; + + ItemFetchJob *fetchJob = new ItemFetchJob(Item(itemId), this); + fetchJob->setFetchScope(mFetchScope); + connect(fetchJob, &ItemFetchJob::result, this, &PreprocessorBasePrivate::itemFetched); +} + +void PreprocessorBasePrivate::itemFetched(KJob *job) +{ + Q_Q(PreprocessorBase); + + if (job->error()) { + emit itemProcessed(PreprocessorBase::ProcessingFailed); + return; + } + + ItemFetchJob *fetchJob = qobject_cast(job); + + if (fetchJob->items().isEmpty()) { + emit itemProcessed(PreprocessorBase::ProcessingFailed); + return; + } + + const Item item = fetchJob->items().at(0); + + switch (q->processItem(item)) { + case PreprocessorBase::ProcessingFailed: + case PreprocessorBase::ProcessingRefused: + case PreprocessorBase::ProcessingCompleted: + qCDebug(AKONADIAGENTBASE_LOG) << "PreprocessorBase: item processed, emitting signal (" << item.id() << ")"; + + // TODO: Handle the different status codes appropriately + + emit itemProcessed(item.id()); + + qCDebug(AKONADIAGENTBASE_LOG) << "PreprocessorBase: item processed, signal emitted (" << item.id() << ")"; + break; + case PreprocessorBase::ProcessingDelayed: + qCDebug(AKONADIAGENTBASE_LOG) << "PreprocessorBase: item processing delayed (" << item.id() << ")"; + + mInDelayedProcessing = true; + mDelayedProcessingItemId = item.id(); + break; + } +} diff --git a/src/agentbase/preprocessorbase_p.h b/src/agentbase/preprocessorbase_p.h new file mode 100644 index 0000000..f92188b --- /dev/null +++ b/src/agentbase/preprocessorbase_p.h @@ -0,0 +1,60 @@ +/* + Copyright (c) 2009 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 PREPROCESSORBASE_P_H +#define PREPROCESSORBASE_P_H + +#include "agentbase_p.h" + +#include "preprocessorbase.h" +#include "itemfetchscope.h" + +class KJob; + +namespace Akonadi +{ + +class PreprocessorBasePrivate : public AgentBasePrivate +{ + Q_OBJECT + +public: + explicit PreprocessorBasePrivate(PreprocessorBase *parent); + + void delayedInit() Q_DECL_OVERRIDE; + + void beginProcessItem(qlonglong itemId, qlonglong collectionId, const QString &mimeType); + +Q_SIGNALS: + void itemProcessed(qlonglong id); + +private Q_SLOTS: + void itemFetched(KJob *job); + +public: + bool mInDelayedProcessing; + qlonglong mDelayedProcessingItemId; + ItemFetchScope mFetchScope; + + Q_DECLARE_PUBLIC(PreprocessorBase) +}; + +} + +#endif diff --git a/src/agentbase/recursivemover.cpp b/src/agentbase/recursivemover.cpp new file mode 100644 index 0000000..c64b6b3 --- /dev/null +++ b/src/agentbase/recursivemover.cpp @@ -0,0 +1,229 @@ +/* + Copyright (c) 2012 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 "recursivemover_p.h" +#include "collectionfetchjob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "collectionfetchscope.h" + +using namespace Akonadi; + +RecursiveMover::RecursiveMover(AgentBasePrivate *parent) + : KCompositeJob(parent) + , m_agentBase(parent) + , m_currentAction(None) + , m_runningJobs(0) + , m_pendingReplay(false) +{ +} + +void RecursiveMover::start() +{ + Q_ASSERT(receivers(SIGNAL(result(KJob*)))); + + CollectionFetchJob *job = new CollectionFetchJob(m_movedCollection, CollectionFetchJob::Recursive, this); + connect(job, &CollectionFetchJob::finished, this, &RecursiveMover::collectionListResult); + addSubjob(job); + ++m_runningJobs; +} + +void RecursiveMover::setCollection(const Collection &collection, const Collection &parentCollection) +{ + m_movedCollection = collection; + m_collections.insert(collection.id(), m_movedCollection); + m_collections.insert(parentCollection.id(), parentCollection); +} + +void RecursiveMover::collectionListResult(KJob *job) +{ + Q_ASSERT(m_pendingCollections.isEmpty()); + --m_runningJobs; + + if (job->error()) { + return; // error handling is in the base class + } + + // build a parent -> children map for the following topological sorting + // while we are iterating anyway, also fill m_collections here + CollectionFetchJob *fetchJob = qobject_cast(job); + QHash colTree; + foreach (const Collection &col, fetchJob->collections()) { + colTree[col.parentCollection().id()] << col; + m_collections.insert(col.id(), col); + } + + // topological sort; BFS traversal of the tree + m_pendingCollections.push_back(m_movedCollection); + QQueue toBeProcessed; + toBeProcessed.enqueue(m_movedCollection); + while (!toBeProcessed.isEmpty()) { + const Collection col = toBeProcessed.dequeue(); + const Collection::List children = colTree.value(col.id()); + if (children.isEmpty()) { + continue; + } + m_pendingCollections += children; + foreach (const Collection &child, children) { + toBeProcessed.enqueue(child); + } + } + + replayNextCollection(); +} + +void RecursiveMover::collectionFetchResult(KJob *job) +{ + Q_ASSERT(m_currentCollection.isValid()); + --m_runningJobs; + + if (job->error()) { + return; // error handling is in the base class + } + + CollectionFetchJob *fetchJob = qobject_cast(job); + if (fetchJob->collections().size() == 1) { + m_currentCollection = fetchJob->collections().at(0); + m_currentCollection.setParentCollection(m_collections.value(m_currentCollection.parentCollection().id())); + m_collections.insert(m_currentCollection.id(), m_currentCollection); + } else { + // already deleted, move on + } + + if (!m_runningJobs && m_pendingReplay) { + replayNext(); + } +} + +void RecursiveMover::itemListResult(KJob *job) +{ + --m_runningJobs; + + if (job->error()) { + return; // error handling is in the base class + } + + foreach (const Item &item, qobject_cast(job)->items()) { + if (item.remoteId().isEmpty()) { + m_pendingItems.push_back(item); + } + } + + if (!m_runningJobs && m_pendingReplay) { + replayNext(); + } +} + +void RecursiveMover::itemFetchResult(KJob *job) +{ + Q_ASSERT(m_currentAction == None); + --m_runningJobs; + + if (job->error()) { + return; // error handling is in the base class + } + + ItemFetchJob *fetchJob = qobject_cast(job); + if (fetchJob->items().size() == 1) { + m_currentAction = AddItem; + m_agentBase->itemAdded(fetchJob->items().at(0), m_currentCollection); + } else { + // deleted since we started, skip + m_currentItem = Item(); + replayNextItem(); + } +} + +void RecursiveMover::replayNextCollection() +{ + if (!m_pendingCollections.isEmpty()) { + + m_currentCollection = m_pendingCollections.takeFirst(); + ItemFetchJob *job = new ItemFetchJob(m_currentCollection, this); + connect(job, &ItemFetchJob::result, this, &RecursiveMover::itemListResult); + addSubjob(job); + ++m_runningJobs; + + if (m_currentCollection.remoteId().isEmpty()) { + Q_ASSERT(m_currentAction == None); + m_currentAction = AddCollection; + m_agentBase->collectionAdded(m_currentCollection, m_collections.value(m_currentCollection.parentCollection().id())); + return; + } else { + //replayNextItem(); - but waiting for the fetch job to finish first + m_pendingReplay = true; + return; + } + } else { + // nothing left to do + emitResult(); + } +} + +void RecursiveMover::replayNextItem() +{ + Q_ASSERT(m_currentCollection.isValid()); + if (m_pendingItems.isEmpty()) { + replayNextCollection(); // all items processed here + return; + } else { + Q_ASSERT(m_currentAction == None); + m_currentItem = m_pendingItems.takeFirst(); + ItemFetchJob *job = new ItemFetchJob(m_currentItem, this); + job->fetchScope().fetchFullPayload(); + connect(job, &ItemFetchJob::result, this, &RecursiveMover::itemFetchResult); + addSubjob(job); + ++m_runningJobs; + } +} + +void RecursiveMover::changeProcessed() +{ + Q_ASSERT(m_currentAction != None); + + if (m_currentAction == AddCollection) { + Q_ASSERT(m_currentCollection.isValid()); + CollectionFetchJob *job = new CollectionFetchJob(m_currentCollection, CollectionFetchJob::Base, this); + job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + connect(job, &CollectionFetchJob::result, this, &RecursiveMover::collectionFetchResult); + addSubjob(job); + ++m_runningJobs; + } + + m_currentAction = None; +} + +void RecursiveMover::replayNext() +{ + // wait for runnings jobs to finish before actually doing the replay + if (m_runningJobs) { + m_pendingReplay = true; + return; + } + + m_pendingReplay = false; + + if (m_currentCollection.isValid()) { + replayNextItem(); + } else { + replayNextCollection(); + } +} + +#include "moc_recursivemover_p.cpp" diff --git a/src/agentbase/recursivemover_p.h b/src/agentbase/recursivemover_p.h new file mode 100644 index 0000000..d10ab2b --- /dev/null +++ b/src/agentbase/recursivemover_p.h @@ -0,0 +1,92 @@ +/* + Copyright (c) 2012 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_RECURSIVEMOVER_P_H +#define AKONADI_RECURSIVEMOVER_P_H + +#include "item.h" +#include "collection.h" +#include "agentbase_p.h" + +#include + +namespace Akonadi +{ + +/** + * Helper class for expanding inter-resource collection moves inside ResourceBase. + * + * @note This is intentionally not an Akonadi::Job since we don't need autostarting + * here. + */ +class RecursiveMover : public KCompositeJob +{ + Q_OBJECT +public: + explicit RecursiveMover(AgentBasePrivate *parent); + + /// Set the collection that is actually moved. + void setCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parentCollection); + + void start() Q_DECL_OVERRIDE; + + /// Call once the last replayed change has been processed. + void changeProcessed(); + +public Q_SLOTS: + /// Trigger the next change replay, will call emitResult() once everything has been replayed + void replayNext(); + +private: + void replayNextCollection(); + void replayNextItem(); + +private Q_SLOTS: + void collectionListResult(KJob *job); + void collectionFetchResult(KJob *job); + void itemListResult(KJob *job); + void itemFetchResult(KJob *job); + +private: + AgentBasePrivate *m_agentBase; + Collection m_movedCollection; + /// sorted queue of collections still to be processed + Collection::List m_pendingCollections; + /// holds up-to-date full collection objects, used for e.g. having proper parent collections for collectionAdded + QHash m_collections; + Item::List m_pendingItems; + + Collection m_currentCollection; + Item m_currentItem; + + enum CurrentAction { + None, + AddCollection, + AddItem + } m_currentAction; + + int m_runningJobs; + bool m_pendingReplay; +}; + +} + +Q_DECLARE_METATYPE(Akonadi::RecursiveMover *) + +#endif // AKONADI_RECURSIVEMOVER_P_H diff --git a/src/agentbase/resourcebase.cpp b/src/agentbase/resourcebase.cpp new file mode 100644 index 0000000..8ef6eda --- /dev/null +++ b/src/agentbase/resourcebase.cpp @@ -0,0 +1,1481 @@ +/* + Copyright (c) 2006 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 "resourcebase.h" +#include "agentbase_p.h" + +#include "resourceadaptor.h" +#include "collectiondeletejob.h" +#include "collectionsync_p.h" +#include "KDBusConnectionPool" +#include "itemsync.h" +#include "akonadi_version.h" +#include "tagsync.h" +#include "relationsync.h" +#include "resourcescheduler_p.h" +#include "tracerinterface.h" +#include "private/xdgbasedirs_p.h" + +#include "changerecorder.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "invalidatecachejob_p.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "itemmodifyjob_p.h" +#include "session.h" +#include "resourceselectjob_p.h" +#include "monitor_p.h" +#include "servermanager_p.h" +#include "recursivemover_p.h" +#include "tagmodifyjob.h" + +#include "akonadiagentbase_debug.h" +#include + +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +class Akonadi::ResourceBasePrivate : public AgentBasePrivate +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.dfaure") + +public: + ResourceBasePrivate(ResourceBase *parent) + : AgentBasePrivate(parent) + , scheduler(0) + , mItemSyncer(0) + , mItemSyncFetchScope(0) + , mItemTransactionMode(ItemSync::SingleTransaction) + , mItemMergeMode(ItemSync::RIDMerge) + , mCollectionSyncer(0) + , mTagSyncer(0) + , mRelationSyncer(0) + , mHierarchicalRid(false) + , mUnemittedProgress(0) + , mAutomaticProgressReporting(true) + , mDisableAutomaticItemDeliveryDone(false) + , mItemSyncBatchSize(10) + , mCurrentCollectionFetchJob(0) + , mScheduleAttributeSyncBeforeCollectionSync(false) + { + Internal::setClientType(Internal::Resource); + mStatusMessage = defaultReadyMessage(); + mProgressEmissionCompressor.setInterval(1000); + mProgressEmissionCompressor.setSingleShot(true); + // HACK: skip local changes of the EntityDisplayAttribute by default. Remove this for KDE5 and adjust resource implementations accordingly. + mKeepLocalCollectionChanges << "ENTITYDISPLAY"; + } + + ~ResourceBasePrivate() + { + delete mItemSyncFetchScope; + } + + Q_DECLARE_PUBLIC(ResourceBase) + + void delayedInit() Q_DECL_OVERRIDE { + const QString serviceId = ServerManager::agentServiceName(ServerManager::Resource, mId); + if (!KDBusConnectionPool::threadConnection().registerService(serviceId)) + { + QString reason = KDBusConnectionPool::threadConnection().lastError().message(); + if (reason.isEmpty()) { + reason = QStringLiteral("this service is probably running already."); + } + qCCritical(AKONADIAGENTBASE_LOG) << "Unable to register service" << serviceId << "at D-Bus:" << reason; + + if (QThread::currentThread() == QCoreApplication::instance()->thread()) { + QCoreApplication::instance()->exit(1); + } + + } else { + AgentBasePrivate::delayedInit(); + } + } + + void changeProcessed() Q_DECL_OVERRIDE { + if (m_recursiveMover) + { + m_recursiveMover->changeProcessed(); + QTimer::singleShot(0, m_recursiveMover.data(), &RecursiveMover::replayNext); + return; + } + + mChangeRecorder->changeProcessed(); + if (!mChangeRecorder->isEmpty()) + { + scheduler->scheduleChangeReplay(); + } + scheduler->taskDone(); + } + + void slotAbortRequested(); + + void slotDeliveryDone(KJob *job); + void slotCollectionSyncDone(KJob *job); + void slotLocalListDone(KJob *job); + void slotSynchronizeCollection(const Collection &col); + void slotItemRetrievalCollectionFetchDone(KJob *job); + void slotCollectionListDone(KJob *job); + void slotSynchronizeCollectionAttributes(const Collection &col); + void slotCollectionListForAttributesDone(KJob *job); + void slotCollectionAttributesSyncDone(KJob *job); + void slotSynchronizeTags(); + void slotSynchronizeRelations(); + void slotAttributeRetrievalCollectionFetchDone(KJob *job); + + void slotItemSyncDone(KJob *job); + + void slotPercent(KJob *job, unsigned long percent); + void slotDelayedEmitProgress(); + void slotDeleteResourceCollection(); + void slotDeleteResourceCollectionDone(KJob *job); + void slotCollectionDeletionDone(KJob *job); + + void slotInvalidateCache(const Akonadi::Collection &collection); + + void slotPrepareItemRetrieval(const Akonadi::Item &item); + void slotPrepareItemRetrievalResult(KJob *job); + + void changeCommittedResult(KJob *job); + + void slotRecursiveMoveReplay(RecursiveMover *mover); + void slotRecursiveMoveReplayResult(KJob *job); + + void slotTagSyncDone(KJob *job); + void slotRelationSyncDone(KJob *job); + + void slotSessionReconnected() + { + Q_Q(ResourceBase); + + new ResourceSelectJob(q->identifier()); + } + + void createItemSyncInstanceIfMissing() + { + Q_Q(ResourceBase); + Q_ASSERT_X(scheduler->currentTask().type == ResourceScheduler::SyncCollection, + "createItemSyncInstance", "Calling items retrieval methods although no item retrieval is in progress"); + if (!mItemSyncer) { + mItemSyncer = new ItemSync(q->currentCollection()); + mItemSyncer->setTransactionMode(mItemTransactionMode); + mItemSyncer->setBatchSize(mItemSyncBatchSize); + mItemSyncer->setMergeMode(mItemMergeMode); + if (mItemSyncFetchScope) { + mItemSyncer->setFetchScope(*mItemSyncFetchScope); + } + mItemSyncer->setDisableAutomaticDeliveryDone(mDisableAutomaticItemDeliveryDone); + mItemSyncer->setProperty("collection", QVariant::fromValue(q->currentCollection())); + connect(mItemSyncer, SIGNAL(percent(KJob*,ulong)), q, SLOT(slotPercent(KJob*,ulong))); + connect(mItemSyncer, SIGNAL(result(KJob*)), q, SLOT(slotItemSyncDone(KJob*))); + connect(mItemSyncer, &ItemSync::readyForNextBatch, q, &ResourceBase::retrieveNextItemSyncBatch); + } + Q_ASSERT(mItemSyncer); + } + +public Q_SLOTS: + // Dump the state of the scheduler + Q_SCRIPTABLE QString dumpToString() const + { + Q_Q(const ResourceBase); + return scheduler->dumpToString() + QLatin1Char('\n') + q->dumpResourceToString(); + } + + Q_SCRIPTABLE void dump() + { + scheduler->dump(); + } + + Q_SCRIPTABLE void clear() + { + scheduler->clear(); + } + +protected Q_SLOTS: + // reimplementations from AgentbBasePrivate, containing sanity checks that only apply to resources + // such as making sure that RIDs are present as well as translations of cross-resource moves + // TODO: we could possibly add recovery code for no-RID notifications by re-enquing those to the change recorder + // as the corresponding Add notifications, although that contains a risk of endless fail/retry loops + + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE { + if (collection.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + AgentBasePrivate::itemAdded(item, collection); + } + + void itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &partIdentifiers) Q_DECL_OVERRIDE { + if (item.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + AgentBasePrivate::itemChanged(item, partIdentifiers); + } + + void itemsFlagsChanged(const Item::List &items, const QSet< QByteArray > &addedFlags, + const QSet< QByteArray > &removedFlags) Q_DECL_OVERRIDE { + if (addedFlags.isEmpty() && removedFlags.isEmpty()) + { + changeProcessed(); + return; + } + + Item::List validItems; + foreach (const Akonadi::Item &item, items) + { + if (!item.remoteId().isEmpty()) { + validItems << item; + } + } + if (validItems.isEmpty()) + { + changeProcessed(); + return; + } + + AgentBasePrivate::itemsFlagsChanged(validItems, addedFlags, removedFlags); + } + + void itemsTagsChanged(const Item::List &items, const QSet &addedTags, const QSet &removedTags) Q_DECL_OVERRIDE { + if (addedTags.isEmpty() && removedTags.isEmpty()) + { + changeProcessed(); + return; + } + + Item::List validItems; + foreach (const Akonadi::Item &item, items) + { + if (!item.remoteId().isEmpty()) { + validItems << item; + } + } + if (validItems.isEmpty()) + { + changeProcessed(); + return; + } + + AgentBasePrivate::itemsTagsChanged(validItems, addedTags, removedTags); + } + + // TODO move the move translation code from AgentBasePrivate here, it's wrong for agents + void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &destination) Q_DECL_OVERRIDE { + if (item.remoteId().isEmpty() || destination.remoteId().isEmpty() || destination == source) + { + changeProcessed(); + return; + } + AgentBasePrivate::itemMoved(item, source, destination); + } + + void itemsMoved(const Item::List &items, const Collection &source, const Collection &destination) Q_DECL_OVERRIDE { + if (destination.remoteId().isEmpty() || destination == source) + { + changeProcessed(); + return; + } + + Item::List validItems; + foreach (const Akonadi::Item &item, items) + { + if (!item.remoteId().isEmpty()) { + validItems << item; + } + } + if (validItems.isEmpty()) + { + changeProcessed(); + return; + } + + AgentBasePrivate::itemsMoved(validItems, source, destination); + } + + void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE { + if (item.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + AgentBasePrivate::itemRemoved(item); + } + + void itemsRemoved(const Item::List &items) Q_DECL_OVERRIDE { + Item::List validItems; + foreach (const Akonadi::Item &item, items) + { + if (!item.remoteId().isEmpty()) { + validItems << item; + } + } + if (validItems.isEmpty()) + { + changeProcessed(); + return; + } + + AgentBasePrivate::itemsRemoved(validItems); + } + + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE { + if (parent.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + AgentBasePrivate::collectionAdded(collection, parent); + } + + void collectionChanged(const Akonadi::Collection &collection) Q_DECL_OVERRIDE { + if (collection.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + AgentBasePrivate::collectionChanged(collection); + } + + void collectionChanged(const Akonadi::Collection &collection, const QSet< QByteArray > &partIdentifiers) Q_DECL_OVERRIDE { + if (collection.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + AgentBasePrivate::collectionChanged(collection, partIdentifiers); + } + + void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination) Q_DECL_OVERRIDE { + // unknown destination or source == destination means we can't do/don't have to do anything + if (destination.remoteId().isEmpty() || source == destination) + { + changeProcessed(); + return; + } + + // inter-resource moves, requires we know which resources the source and destination are in though + if (!source.resource().isEmpty() && !destination.resource().isEmpty() && source.resource() != destination.resource()) + { + if (source.resource() == q_ptr->identifier()) { // moved away from us + AgentBasePrivate::collectionRemoved(collection); + } else if (destination.resource() == q_ptr->identifier()) { // moved to us + scheduler->taskDone(); // stop change replay for now + RecursiveMover *mover = new RecursiveMover(this); + mover->setCollection(collection, destination); + scheduler->scheduleMoveReplay(collection, mover); + } + return; + } + + // intra-resource move, requires the moved collection to have a valid id though + if (collection.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + + // intra-resource move, ie. something we can handle internally + AgentBasePrivate::collectionMoved(collection, source, destination); + } + + void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE { + if (collection.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + AgentBasePrivate::collectionRemoved(collection); + } + + void tagAdded(const Akonadi::Tag &tag) Q_DECL_OVERRIDE { + if (!tag.isValid()) + { + changeProcessed(); + return; + } + + AgentBasePrivate::tagAdded(tag); + } + + void tagChanged(const Akonadi::Tag &tag) Q_DECL_OVERRIDE { + if (tag.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + + AgentBasePrivate::tagChanged(tag); + } + + void tagRemoved(const Akonadi::Tag &tag) Q_DECL_OVERRIDE { + if (tag.remoteId().isEmpty()) + { + changeProcessed(); + return; + } + + AgentBasePrivate::tagRemoved(tag); + } + +public: + // synchronize states + Collection currentCollection; + + ResourceScheduler *scheduler; + ItemSync *mItemSyncer; + ItemFetchScope *mItemSyncFetchScope; + ItemSync::TransactionMode mItemTransactionMode; + ItemSync::MergeMode mItemMergeMode; + CollectionSync *mCollectionSyncer; + TagSync *mTagSyncer; + RelationSync *mRelationSyncer; + bool mHierarchicalRid; + QTimer mProgressEmissionCompressor; + int mUnemittedProgress; + QMap mUnemittedAdvancedStatus; + bool mAutomaticProgressReporting; + bool mDisableAutomaticItemDeliveryDone; + QPointer m_recursiveMover; + int mItemSyncBatchSize; + QSet mKeepLocalCollectionChanges; + KJob *mCurrentCollectionFetchJob; + bool mScheduleAttributeSyncBeforeCollectionSync; +}; + +ResourceBase::ResourceBase(const QString &id) + : AgentBase(new ResourceBasePrivate(this), id) +{ + Q_D(ResourceBase); + + qDBusRegisterMetaType(); + + new Akonadi__ResourceAdaptor(this); + + d->scheduler = new ResourceScheduler(this); + + d->mChangeRecorder->setChangeRecordingEnabled(true); + d->mChangeRecorder->setCollectionMoveTranslationEnabled(false); // we deal with this ourselves + connect(d->mChangeRecorder, &ChangeRecorder::changesAdded, + d->scheduler, &ResourceScheduler::scheduleChangeReplay); + + d->mChangeRecorder->setResourceMonitored(d->mId.toLatin1()); + d->mChangeRecorder->fetchCollection(true); + + connect(d->scheduler, &ResourceScheduler::executeFullSync, + this, &ResourceBase::retrieveCollections); + connect(d->scheduler, &ResourceScheduler::executeCollectionTreeSync, + this, &ResourceBase::retrieveCollections); + connect(d->scheduler, SIGNAL(executeCollectionSync(Akonadi::Collection)), + SLOT(slotSynchronizeCollection(Akonadi::Collection))); + connect(d->scheduler, SIGNAL(executeCollectionAttributesSync(Akonadi::Collection)), + SLOT(slotSynchronizeCollectionAttributes(Akonadi::Collection))); + connect(d->scheduler, SIGNAL(executeTagSync()), + SLOT(slotSynchronizeTags())); + connect(d->scheduler, SIGNAL(executeRelationSync()), + SLOT(slotSynchronizeRelations())); + connect(d->scheduler, SIGNAL(executeItemFetch(Akonadi::Item,QSet)), + SLOT(slotPrepareItemRetrieval(Akonadi::Item))); + connect(d->scheduler, SIGNAL(executeResourceCollectionDeletion()), + SLOT(slotDeleteResourceCollection())); + connect(d->scheduler, SIGNAL(executeCacheInvalidation(Akonadi::Collection)), + SLOT(slotInvalidateCache(Akonadi::Collection))); + connect(d->scheduler, SIGNAL(status(int,QString)), + SIGNAL(status(int,QString))); + connect(d->scheduler, &ResourceScheduler::executeChangeReplay, + d->mChangeRecorder, &ChangeRecorder::replayNext); + connect(d->scheduler, SIGNAL(executeRecursiveMoveReplay(RecursiveMover*)), + SLOT(slotRecursiveMoveReplay(RecursiveMover*))); + connect(d->scheduler, &ResourceScheduler::fullSyncComplete, this, &ResourceBase::synchronized); + connect(d->scheduler, &ResourceScheduler::collectionTreeSyncComplete, this, &ResourceBase::collectionTreeSynchronized); + connect(d->mChangeRecorder, &ChangeRecorder::nothingToReplay, d->scheduler, &ResourceScheduler::taskDone); + connect(d->mChangeRecorder, &Monitor::collectionRemoved, + d->scheduler, &ResourceScheduler::collectionRemoved); + connect(this, SIGNAL(abortRequested()), this, SLOT(slotAbortRequested())); + connect(this, &ResourceBase::synchronized, d->scheduler, &ResourceScheduler::taskDone); + connect(this, &ResourceBase::collectionTreeSynchronized, d->scheduler, &ResourceScheduler::taskDone); + connect(this, &AgentBase::agentNameChanged, + this, &ResourceBase::nameChanged); + + connect(&d->mProgressEmissionCompressor, SIGNAL(timeout()), + this, SLOT(slotDelayedEmitProgress())); + + d->scheduler->setOnline(d->mOnline); + if (!d->mChangeRecorder->isEmpty()) { + d->scheduler->scheduleChangeReplay(); + } + + new ResourceSelectJob(identifier()); + + connect(d->mChangeRecorder->session(), SIGNAL(reconnected()), SLOT(slotSessionReconnected())); +} + +ResourceBase::~ResourceBase() +{ +} + +void ResourceBase::synchronize() +{ + d_func()->scheduler->scheduleFullSync(); +} + +void ResourceBase::setName(const QString &name) +{ + AgentBase::setAgentName(name); +} + +QString ResourceBase::name() const +{ + return AgentBase::agentName(); +} + +QString ResourceBase::parseArguments(int argc, char **argv) +{ + Q_UNUSED(argc); + + QCommandLineOption identifierOption(QStringLiteral("identifier"), + i18nc("@label command line option", "Resource identifier"), + QStringLiteral("argument")); + QCommandLineParser parser; + parser.addOption(identifierOption); + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(*qApp); + parser.setApplicationDescription(i18n("Akonadi Resource")); + + if (!parser.isSet(identifierOption)) { + qCDebug(AKONADIAGENTBASE_LOG) << "Identifier argument missing"; + exit(1); + } + + const QString identifier = parser.value(identifierOption); + + if (identifier.isEmpty()) { + qCDebug(AKONADIAGENTBASE_LOG) << "Identifier is empty"; + exit(1); + } + + QCoreApplication::setApplicationName(ServerManager::addNamespace(identifier)); + QCoreApplication::setApplicationVersion(QStringLiteral(AKONADI_VERSION_STRING)); + + const QFileInfo fi(QString::fromLocal8Bit(argv[0])); + // strip off full path and possible .exe suffix + const QString catalog = fi.baseName(); + + QTranslator *translator = new QTranslator(); + translator->load(catalog); + QCoreApplication::installTranslator(translator); + + return identifier; +} + +int ResourceBase::init(ResourceBase *r) +{ +#warning port to the new way of doing this +// KLocalizedString::insertCatalog( QLatin1String( "libakonadi" ) ); + int rv = qApp->exec(); + delete r; + return rv; +} + +void ResourceBasePrivate::slotAbortRequested() +{ + Q_Q(ResourceBase); + + scheduler->cancelQueues(); + q->abortActivity(); +} + +void ResourceBase::itemRetrieved(const Item &item) +{ + Q_D(ResourceBase); + Q_ASSERT(d->scheduler->currentTask().type == ResourceScheduler::FetchItem); + if (!item.isValid()) { + d->scheduler->currentTask().sendDBusReplies(i18nc("@info", "Invalid item retrieved")); + d->scheduler->taskDone(); + return; + } + + Item i(item); + QSet requestedParts = d->scheduler->currentTask().itemParts; + foreach (const QByteArray &part, requestedParts) { + if (!item.loadedPayloadParts().contains(part)) { + qCWarning(AKONADIAGENTBASE_LOG) << "Item does not provide part" << part; + } + } + + ItemModifyJob *job = new ItemModifyJob(i); + job->d_func()->setSilent(true); + // FIXME: remove once the item with which we call retrieveItem() has a revision number + job->disableRevisionCheck(); + connect(job, SIGNAL(result(KJob*)), SLOT(slotDeliveryDone(KJob*))); +} + +void ResourceBasePrivate::slotDeliveryDone(KJob *job) +{ + Q_Q(ResourceBase); + Q_ASSERT(scheduler->currentTask().type == ResourceScheduler::FetchItem); + if (job->error()) { + emit q->error(i18nc("@info", "Error while creating item: %1", job->errorString())); + } + scheduler->currentTask().sendDBusReplies(job->error() ? job->errorString() : QString()); + scheduler->taskDone(); +} + +void ResourceBase::collectionAttributesRetrieved(const Collection &collection) +{ + Q_D(ResourceBase); + Q_ASSERT(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionAttributes); + if (!collection.isValid()) { + emit attributesSynchronized(d->scheduler->currentTask().collection.id()); + d->scheduler->taskDone(); + return; + } + + CollectionModifyJob *job = new CollectionModifyJob(collection); + connect(job, SIGNAL(result(KJob*)), SLOT(slotCollectionAttributesSyncDone(KJob*))); +} + +void ResourceBasePrivate::slotCollectionAttributesSyncDone(KJob *job) +{ + Q_Q(ResourceBase); + Q_ASSERT(scheduler->currentTask().type == ResourceScheduler::SyncCollectionAttributes); + if (job->error()) { + emit q->error(i18nc("@info", "Error while updating collection: %1", job->errorString())); + } + emit q->attributesSynchronized(scheduler->currentTask().collection.id()); + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotDeleteResourceCollection() +{ + Q_Q(ResourceBase); + + CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); + job->fetchScope().setResource(q->identifier()); + connect(job, SIGNAL(result(KJob*)), q, SLOT(slotDeleteResourceCollectionDone(KJob*))); +} + +void ResourceBasePrivate::slotDeleteResourceCollectionDone(KJob *job) +{ + Q_Q(ResourceBase); + if (job->error()) { + emit q->error(job->errorString()); + scheduler->taskDone(); + } else { + const CollectionFetchJob *fetchJob = static_cast(job); + + if (!fetchJob->collections().isEmpty()) { + CollectionDeleteJob *job = new CollectionDeleteJob(fetchJob->collections().at(0)); + connect(job, SIGNAL(result(KJob*)), q, SLOT(slotCollectionDeletionDone(KJob*))); + } else { + // there is no resource collection, so just ignore the request + scheduler->taskDone(); + } + } +} + +void ResourceBasePrivate::slotCollectionDeletionDone(KJob *job) +{ + Q_Q(ResourceBase); + if (job->error()) { + emit q->error(job->errorString()); + } + + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotInvalidateCache(const Akonadi::Collection &collection) +{ + Q_Q(ResourceBase); + InvalidateCacheJob *job = new InvalidateCacheJob(collection, q); + connect(job, &KJob::result, scheduler, &ResourceScheduler::taskDone); +} + +void ResourceBase::changeCommitted(const Item &item) +{ + changesCommitted(Item::List() << item); +} + +void ResourceBase::changesCommitted(const Item::List &items) +{ + TransactionSequence *transaction = new TransactionSequence(this); + connect(transaction, SIGNAL(finished(KJob*)), + this, SLOT(changeCommittedResult(KJob*))); + + // Modify the items one-by-one, because STORE does not support mass RID change + Q_FOREACH (const Item &item, items) { + ItemModifyJob *job = new ItemModifyJob(item, transaction); + job->d_func()->setClean(); + job->disableRevisionCheck(); // TODO: remove, but where/how do we handle the error? + job->setIgnorePayload(true); // we only want to reset the dirty flag and update the remote id + } +} + +void ResourceBase::changeCommitted(const Collection &collection) +{ + CollectionModifyJob *job = new CollectionModifyJob(collection); + connect(job, SIGNAL(result(KJob*)), SLOT(changeCommittedResult(KJob*))); +} + +void ResourceBasePrivate::changeCommittedResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADIAGENTBASE_LOG) << job->errorText(); + } + + Q_Q(ResourceBase); + if (qobject_cast(job)) { + if (job->error()) { + emit q->error(i18nc("@info", "Updating local collection failed: %1.", job->errorText())); + } + mChangeRecorder->d_ptr->invalidateCache(static_cast(job)->collection()); + } else { + if (job->error()) { + emit q->error(i18nc("@info", "Updating local items failed: %1.", job->errorText())); + } + // Item and tag cache is invalidated by modify job + } + + changeProcessed(); +} + +void ResourceBase::changeCommitted(const Tag &tag) +{ + TagModifyJob *job = new TagModifyJob(tag); + connect(job, SIGNAL(result(KJob*)), SLOT(changeCommittedResult(KJob*))); +} + +QString ResourceBase::requestItemDelivery(qint64 uid, const QString &remoteId, const QString &mimeType, const QByteArrayList &parts) +{ + Q_D(ResourceBase); + if (!isOnline()) { + const QString errorMsg = i18nc("@info", "Cannot fetch item in offline mode."); + emit error(errorMsg); + return errorMsg; + } + + setDelayedReply(true); + // FIXME: we need at least the revision number too + Item item(uid); + item.setMimeType(mimeType); + item.setRemoteId(remoteId); + + d->scheduler->scheduleItemFetch(item, QSet::fromList(parts), message()); + + return QString(); + +} + +void ResourceBase::collectionsRetrieved(const Collection::List &collections) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || + d->scheduler->currentTask().type == ResourceScheduler::SyncAll, + "ResourceBase::collectionsRetrieved()", + "Calling collectionsRetrieved() although no collection retrieval is in progress"); + if (!d->mCollectionSyncer) { + d->mCollectionSyncer = new CollectionSync(identifier()); + d->mCollectionSyncer->setHierarchicalRemoteIds(d->mHierarchicalRid); + d->mCollectionSyncer->setKeepLocalChanges(d->mKeepLocalCollectionChanges); + connect(d->mCollectionSyncer, SIGNAL(percent(KJob*,ulong)), SLOT(slotPercent(KJob*,ulong))); + connect(d->mCollectionSyncer, SIGNAL(result(KJob*)), SLOT(slotCollectionSyncDone(KJob*))); + } + d->mCollectionSyncer->setRemoteCollections(collections); +} + +void ResourceBase::collectionsRetrievedIncremental(const Collection::List &changedCollections, + const Collection::List &removedCollections) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || + d->scheduler->currentTask().type == ResourceScheduler::SyncAll, + "ResourceBase::collectionsRetrievedIncremental()", + "Calling collectionsRetrievedIncremental() although no collection retrieval is in progress"); + if (!d->mCollectionSyncer) { + d->mCollectionSyncer = new CollectionSync(identifier()); + d->mCollectionSyncer->setHierarchicalRemoteIds(d->mHierarchicalRid); + d->mCollectionSyncer->setKeepLocalChanges(d->mKeepLocalCollectionChanges); + connect(d->mCollectionSyncer, SIGNAL(percent(KJob*,ulong)), SLOT(slotPercent(KJob*,ulong))); + connect(d->mCollectionSyncer, SIGNAL(result(KJob*)), SLOT(slotCollectionSyncDone(KJob*))); + } + d->mCollectionSyncer->setRemoteCollections(changedCollections, removedCollections); +} + +void ResourceBase::setCollectionStreamingEnabled(bool enable) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || + d->scheduler->currentTask().type == ResourceScheduler::SyncAll, + "ResourceBase::setCollectionStreamingEnabled()", + "Calling setCollectionStreamingEnabled() although no collection retrieval is in progress"); + if (!d->mCollectionSyncer) { + d->mCollectionSyncer = new CollectionSync(identifier()); + d->mCollectionSyncer->setHierarchicalRemoteIds(d->mHierarchicalRid); + connect(d->mCollectionSyncer, SIGNAL(percent(KJob*,ulong)), SLOT(slotPercent(KJob*,ulong))); + connect(d->mCollectionSyncer, SIGNAL(result(KJob*)), SLOT(slotCollectionSyncDone(KJob*))); + } + d->mCollectionSyncer->setStreamingEnabled(enable); +} + +void ResourceBase::collectionsRetrievalDone() +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || + d->scheduler->currentTask().type == ResourceScheduler::SyncAll, + "ResourceBase::collectionsRetrievalDone()", + "Calling collectionsRetrievalDone() although no collection retrieval is in progress"); + // streaming enabled, so finalize the sync + if (d->mCollectionSyncer) { + d->mCollectionSyncer->retrievalDone(); + } else { + // user did the sync himself, we are done now + // FIXME: we need the same special case for SyncAll as in slotCollectionSyncDone here! + d->scheduler->taskDone(); + } +} + +void ResourceBase::setKeepLocalCollectionChanges(const QSet &parts) +{ + Q_D(ResourceBase); + d->mKeepLocalCollectionChanges = parts; +} + +void ResourceBasePrivate::slotCollectionSyncDone(KJob *job) +{ + Q_Q(ResourceBase); + mCollectionSyncer = 0; + if (job->error()) { + if (job->error() != Job::UserCanceled) { + emit q->error(job->errorString()); + } + } else { + if (scheduler->currentTask().type == ResourceScheduler::SyncAll) { + CollectionFetchJob *list = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); + list->setFetchScope(q->changeRecorder()->collectionFetchScope()); + list->fetchScope().setResource(mId); + list->fetchScope().setListFilter(CollectionFetchScope::Sync); + q->connect(list, SIGNAL(result(KJob*)), q, SLOT(slotLocalListDone(KJob*))); + return; + } else if (scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree) { + scheduler->scheduleCollectionTreeSyncCompletion(); + } + } + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotLocalListDone(KJob *job) +{ + Q_Q(ResourceBase); + if (job->error()) { + emit q->error(job->errorString()); + } else { + Collection::List cols = static_cast(job)->collections(); + foreach (const Collection &col, cols) { + scheduler->scheduleSync(col); + } + scheduler->scheduleFullSyncCompletion(); + } + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotSynchronizeCollection(const Collection &col) +{ + Q_Q(ResourceBase); + currentCollection = col; + // This can happen due to FetchHelper::triggerOnDemandFetch() in the akonadi server (not an error). + if (!col.remoteId().isEmpty()) { + // check if this collection actually can contain anything + QStringList contentTypes = currentCollection.contentMimeTypes(); + contentTypes.removeAll(Collection::mimeType()); + contentTypes.removeAll(Collection::virtualMimeType()); + if (!contentTypes.isEmpty() || col.isVirtual()) { + if (mAutomaticProgressReporting) { + emit q->status(AgentBase::Running, i18nc("@info:status", "Syncing folder '%1'", currentCollection.displayName())); + } + + Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(col, CollectionFetchJob::Base, this); + fetchJob->setFetchScope(q->changeRecorder()->collectionFetchScope()); + connect(fetchJob, SIGNAL(result(KJob*)), q, SLOT(slotItemRetrievalCollectionFetchDone(KJob*))); + mCurrentCollectionFetchJob = fetchJob; + return; + } + } + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotItemRetrievalCollectionFetchDone(KJob *job) +{ + Q_Q(ResourceBase); + mCurrentCollectionFetchJob = 0; + if (job->error()) { + qCWarning(AKONADIAGENTBASE_LOG) << "Failed to retrieve collection for sync: " << job->errorString(); + q->cancelTask(i18n("Failed to retrieve collection for sync.")); + return; + } + Akonadi::CollectionFetchJob *fetchJob = static_cast(job); + const Collection::List collections = fetchJob->collections(); + if (collections.isEmpty()) { + qCWarning(AKONADIAGENTBASE_LOG) << "The fetch job returned empty collection set. This is unexpected."; + q->cancelTask(i18n("Failed to retrieve collection for sync.")); + return; + } + q->retrieveItems(collections.at(0)); +} + +int ResourceBase::itemSyncBatchSize() const +{ + Q_D(const ResourceBase); + return d->mItemSyncBatchSize; +} + +void ResourceBase::setItemSyncBatchSize(int batchSize) +{ + Q_D(ResourceBase); + d->mItemSyncBatchSize = batchSize; +} + +void ResourceBase::setScheduleAttributeSyncBeforeItemSync(bool enable) +{ + Q_D(ResourceBase); + d->mScheduleAttributeSyncBeforeCollectionSync = enable; +} + +void ResourceBasePrivate::slotSynchronizeCollectionAttributes(const Collection &col) +{ + Q_Q(ResourceBase); + Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(col, CollectionFetchJob::Base, this); + fetchJob->setFetchScope(q->changeRecorder()->collectionFetchScope()); + connect(fetchJob, SIGNAL(result(KJob*)), q, SLOT(slotAttributeRetrievalCollectionFetchDone(KJob*))); + Q_ASSERT(!mCurrentCollectionFetchJob); + mCurrentCollectionFetchJob = fetchJob; +} + +void ResourceBasePrivate::slotAttributeRetrievalCollectionFetchDone(KJob *job) +{ + mCurrentCollectionFetchJob = 0; + Q_Q(ResourceBase); + if (job->error()) { + qCWarning(AKONADIAGENTBASE_LOG) << "Failed to retrieve collection for attribute sync: " << job->errorString(); + q->cancelTask(i18n("Failed to retrieve collection for attribute sync.")); + return; + } + Akonadi::CollectionFetchJob *fetchJob = static_cast(job); + QMetaObject::invokeMethod(q, "retrieveCollectionAttributes", Q_ARG(Akonadi::Collection, fetchJob->collections().at(0))); +} + +void ResourceBasePrivate::slotSynchronizeTags() +{ + Q_Q(ResourceBase); + QMetaObject::invokeMethod(q, "retrieveTags"); +} + +void ResourceBasePrivate::slotSynchronizeRelations() +{ + Q_Q(ResourceBase); + QMetaObject::invokeMethod(q, "retrieveRelations"); +} + +void ResourceBasePrivate::slotPrepareItemRetrieval(const Akonadi::Item &item) +{ + Q_Q(ResourceBase); + ItemFetchJob *fetch = new ItemFetchJob(item, this); + fetch->fetchScope().setAncestorRetrieval(q->changeRecorder()->itemFetchScope().ancestorRetrieval()); + fetch->fetchScope().setCacheOnly(true); + + // copy list of attributes to fetch + const QSet attributes = q->changeRecorder()->itemFetchScope().attributes(); + foreach (const QByteArray &attribute, attributes) { + fetch->fetchScope().fetchAttribute(attribute); + } + + q->connect(fetch, SIGNAL(result(KJob*)), SLOT(slotPrepareItemRetrievalResult(KJob*))); +} + +void ResourceBasePrivate::slotPrepareItemRetrievalResult(KJob *job) +{ + Q_Q(ResourceBase); + Q_ASSERT_X(scheduler->currentTask().type == ResourceScheduler::FetchItem, + "ResourceBasePrivate::slotPrepareItemRetrievalResult()", + "Preparing item retrieval although no item retrieval is in progress"); + if (job->error()) { + q->cancelTask(job->errorText()); + return; + } + ItemFetchJob *fetch = qobject_cast(job); + if (fetch->items().count() != 1) { + q->cancelTask(i18n("The requested item no longer exists")); + return; + } + const Item item = fetch->items().at(0); + const QSet parts = scheduler->currentTask().itemParts; + if (!q->retrieveItem(item, parts)) { + q->cancelTask(); + } +} + +void ResourceBasePrivate::slotRecursiveMoveReplay(RecursiveMover *mover) +{ + Q_Q(ResourceBase); + Q_ASSERT(mover); + Q_ASSERT(!m_recursiveMover); + m_recursiveMover = mover; + connect(mover, SIGNAL(result(KJob*)), q, SLOT(slotRecursiveMoveReplayResult(KJob*))); + mover->start(); +} + +void ResourceBasePrivate::slotRecursiveMoveReplayResult(KJob *job) +{ + Q_Q(ResourceBase); + m_recursiveMover = 0; + + if (job->error()) { + q->deferTask(); + return; + } + + changeProcessed(); +} + +void ResourceBase::itemsRetrievalDone() +{ + Q_D(ResourceBase); + // streaming enabled, so finalize the sync + if (d->mItemSyncer) { + d->mItemSyncer->deliveryDone(); + } else { + // user did the sync himself, we are done now + d->scheduler->taskDone(); + } +} + +void ResourceBase::clearCache() +{ + Q_D(ResourceBase); + d->scheduler->scheduleResourceCollectionDeletion(); +} + +void ResourceBase::invalidateCache(const Collection &collection) +{ + Q_D(ResourceBase); + d->scheduler->scheduleCacheInvalidation(collection); +} + +Collection ResourceBase::currentCollection() const +{ + Q_D(const ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollection, + "ResourceBase::currentCollection()", + "Trying to access current collection although no item retrieval is in progress"); + return d->currentCollection; +} + +Item ResourceBase::currentItem() const +{ + Q_D(const ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::FetchItem, + "ResourceBase::currentItem()", + "Trying to access current item although no item retrieval is in progress"); + return d->scheduler->currentTask().item; +} + +void ResourceBase::synchronizeCollectionTree() +{ + d_func()->scheduler->scheduleCollectionTreeSync(); +} + +void ResourceBase::synchronizeTags() +{ + d_func()->scheduler->scheduleTagSync(); +} + +void ResourceBase::synchronizeRelations() +{ + d_func()->scheduler->scheduleRelationSync(); +} + +void ResourceBase::cancelTask() +{ + Q_D(ResourceBase); + if (d->mCurrentCollectionFetchJob) { + d->mCurrentCollectionFetchJob->kill(); + d->mCurrentCollectionFetchJob = 0; + } + switch (d->scheduler->currentTask().type) { + case ResourceScheduler::FetchItem: + itemRetrieved(Item()); // sends the error reply and + break; + case ResourceScheduler::ChangeReplay: + d->changeProcessed(); + break; + case ResourceScheduler::SyncCollectionTree: + case ResourceScheduler::SyncAll: + if (d->mCollectionSyncer) { + d->mCollectionSyncer->rollback(); + } else { + d->scheduler->taskDone(); + } + break; + case ResourceScheduler::SyncCollection: + if (d->mItemSyncer) { + d->mItemSyncer->rollback(); + } else { + d->scheduler->taskDone(); + } + break; + default: + d->scheduler->taskDone(); + } +} + +void ResourceBase::cancelTask(const QString &msg) +{ + cancelTask(); + + emit error(msg); +} + +void ResourceBase::deferTask() +{ + Q_D(ResourceBase); + d->scheduler->deferTask(); +} + +void ResourceBase::doSetOnline(bool state) +{ + d_func()->scheduler->setOnline(state); +} + +void ResourceBase::synchronizeCollection(qint64 collectionId) +{ + synchronizeCollection(collectionId, false); +} + +void ResourceBase::synchronizeCollection(qint64 collectionId, bool recursive) +{ + CollectionFetchJob *job = new CollectionFetchJob(Collection(collectionId), recursive ? CollectionFetchJob::Recursive : CollectionFetchJob::Base); + job->setFetchScope(changeRecorder()->collectionFetchScope()); + job->fetchScope().setResource(identifier()); + job->fetchScope().setListFilter(CollectionFetchScope::Sync); + connect(job, SIGNAL(result(KJob*)), SLOT(slotCollectionListDone(KJob*))); +} + +void ResourceBasePrivate::slotCollectionListDone(KJob *job) +{ + if (!job->error()) { + const Collection::List list = static_cast(job)->collections(); + Q_FOREACH (const Collection &collection, list) { + //We also get collections that should not be synced but are part of the tree. + if (collection.shouldList(Collection::ListSync) || collection.referenced()) { + if (mScheduleAttributeSyncBeforeCollectionSync) { + scheduler->scheduleAttributesSync(collection); + } + scheduler->scheduleSync(collection); + } + } + } else { + qCWarning(AKONADIAGENTBASE_LOG) << "Failed to fetch collection for collection sync: " << job->errorString(); + } +} + +void ResourceBase::synchronizeCollectionAttributes(const Akonadi::Collection &col) +{ + Q_D(ResourceBase); + d->scheduler->scheduleAttributesSync(col); +} + +void ResourceBase::synchronizeCollectionAttributes(qint64 collectionId) +{ + CollectionFetchJob *job = new CollectionFetchJob(Collection(collectionId), CollectionFetchJob::Base); + job->setFetchScope(changeRecorder()->collectionFetchScope()); + job->fetchScope().setResource(identifier()); + connect(job, SIGNAL(result(KJob*)), SLOT(slotCollectionListForAttributesDone(KJob*))); +} + +void ResourceBasePrivate::slotCollectionListForAttributesDone(KJob *job) +{ + if (!job->error()) { + Collection::List list = static_cast(job)->collections(); + if (!list.isEmpty()) { + Collection col = list.first(); + scheduler->scheduleAttributesSync(col); + } + } + // TODO: error handling +} + +void ResourceBase::setTotalItems(int amount) +{ + qCDebug(AKONADIAGENTBASE_LOG) << amount; + Q_D(ResourceBase); + setItemStreamingEnabled(true); + if (d->mItemSyncer) { + d->mItemSyncer->setTotalItems(amount); + } +} + +void ResourceBase::setDisableAutomaticItemDeliveryDone(bool disable) +{ + Q_D(ResourceBase); + if (d->mItemSyncer) { + d->mItemSyncer->setDisableAutomaticDeliveryDone(disable); + } + d->mDisableAutomaticItemDeliveryDone = disable; +} + +void ResourceBase::setItemStreamingEnabled(bool enable) +{ + Q_D(ResourceBase); + d->createItemSyncInstanceIfMissing(); + if (d->mItemSyncer) { + d->mItemSyncer->setStreamingEnabled(enable); + } +} + +void ResourceBase::itemsRetrieved(const Item::List &items) +{ + Q_D(ResourceBase); + d->createItemSyncInstanceIfMissing(); + if (d->mItemSyncer) { + d->mItemSyncer->setFullSyncItems(items); + } +} + +void ResourceBase::itemsRetrievedIncremental(const Item::List &changedItems, + const Item::List &removedItems) +{ + Q_D(ResourceBase); + d->createItemSyncInstanceIfMissing(); + if (d->mItemSyncer) { + d->mItemSyncer->setIncrementalSyncItems(changedItems, removedItems); + } +} + +void ResourceBasePrivate::slotItemSyncDone(KJob *job) +{ + mItemSyncer = 0; + Q_Q(ResourceBase); + if (job->error() && job->error() != Job::UserCanceled) { + emit q->error(job->errorString()); + } + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotDelayedEmitProgress() +{ + Q_Q(ResourceBase); + if (mAutomaticProgressReporting) { + emit q->percent(mUnemittedProgress); + + Q_FOREACH (const QVariantMap &statusMap, mUnemittedAdvancedStatus) { + emit q->advancedStatus(statusMap); + } + } + mUnemittedProgress = 0; + mUnemittedAdvancedStatus.clear(); +} + +void ResourceBasePrivate::slotPercent(KJob *job, unsigned long percent) +{ + mUnemittedProgress = percent; + + const Collection collection = job->property("collection").value(); + if (collection.isValid()) { + QVariantMap statusMap; + statusMap.insert(QStringLiteral("key"), QStringLiteral("collectionSyncProgress")); + statusMap.insert(QStringLiteral("collectionId"), collection.id()); + statusMap.insert(QStringLiteral("percent"), static_cast(percent)); + + mUnemittedAdvancedStatus[collection.id()] = statusMap; + } + // deliver completion right away, intermediate progress at 1s intervals + if (percent == 100) { + mProgressEmissionCompressor.stop(); + slotDelayedEmitProgress(); + } else if (!mProgressEmissionCompressor.isActive()) { + mProgressEmissionCompressor.start(); + } +} + +void ResourceBase::setHierarchicalRemoteIdentifiersEnabled(bool enable) +{ + Q_D(ResourceBase); + d->mHierarchicalRid = enable; +} + +void ResourceBase::scheduleCustomTask(QObject *receiver, const char *method, const QVariant &argument, SchedulePriority priority) +{ + Q_D(ResourceBase); + d->scheduler->scheduleCustomTask(receiver, method, argument, priority); +} + +void ResourceBase::taskDone() +{ + Q_D(ResourceBase); + d->scheduler->taskDone(); +} + +void ResourceBase::retrieveCollectionAttributes(const Collection &collection) +{ + collectionAttributesRetrieved(collection); +} + +void ResourceBase::retrieveTags() +{ + Q_D(ResourceBase); + d->scheduler->taskDone(); +} + +void ResourceBase::retrieveRelations() +{ + Q_D(ResourceBase); + d->scheduler->taskDone(); +} + +void Akonadi::ResourceBase::abortActivity() +{ +} + +void ResourceBase::setItemTransactionMode(ItemSync::TransactionMode mode) +{ + Q_D(ResourceBase); + d->mItemTransactionMode = mode; +} + +void ResourceBase::setItemSynchronizationFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(ResourceBase); + if (!d->mItemSyncFetchScope) { + d->mItemSyncFetchScope = new ItemFetchScope; + } + *(d->mItemSyncFetchScope) = fetchScope; +} + +void ResourceBase::setItemMergingMode(ItemSync::MergeMode mode) +{ + Q_D(ResourceBase); + d->mItemMergeMode = mode; +} + +void ResourceBase::setAutomaticProgressReporting(bool enabled) +{ + Q_D(ResourceBase); + d->mAutomaticProgressReporting = enabled; +} + +QString ResourceBase::dumpNotificationListToString() const +{ + Q_D(const ResourceBase); + return d->dumpNotificationListToString(); +} + +QString ResourceBase::dumpSchedulerToString() const +{ + Q_D(const ResourceBase); + return d->dumpToString(); +} + +void ResourceBase::dumpMemoryInfo() const +{ + Q_D(const ResourceBase); + return d->dumpMemoryInfo(); +} + +QString ResourceBase::dumpMemoryInfoToString() const +{ + Q_D(const ResourceBase); + return d->dumpMemoryInfoToString(); +} + +void ResourceBase::tagsRetrieved(const Tag::List &tags, const QHash &tagMembers) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncTags || + d->scheduler->currentTask().type == ResourceScheduler::SyncAll || + d->scheduler->currentTask().type == ResourceScheduler::Custom, + "ResourceBase::tagsRetrieved()", + "Calling tagsRetrieved() although no tag retrieval is in progress"); + if (!d->mTagSyncer) { + d->mTagSyncer = new TagSync(this); + connect(d->mTagSyncer, SIGNAL(percent(KJob*,ulong)), SLOT(slotPercent(KJob*,ulong))); + connect(d->mTagSyncer, SIGNAL(result(KJob*)), SLOT(slotTagSyncDone(KJob*))); + } + d->mTagSyncer->setFullTagList(tags); + d->mTagSyncer->setTagMembers(tagMembers); +} + +void ResourceBasePrivate::slotTagSyncDone(KJob *job) +{ + Q_Q(ResourceBase); + mTagSyncer = 0; + if (job->error()) { + if (job->error() != Job::UserCanceled) { + qCWarning(AKONADIAGENTBASE_LOG) << "TagSync failed: " << job->errorString(); + emit q->error(job->errorString()); + } + } + + scheduler->taskDone(); +} + +void ResourceBase::relationsRetrieved(const Relation::List &relations) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncRelations || + d->scheduler->currentTask().type == ResourceScheduler::SyncAll || + d->scheduler->currentTask().type == ResourceScheduler::Custom, + "ResourceBase::relationsRetrieved()", + "Calling relationsRetrieved() although no relation retrieval is in progress"); + if (!d->mRelationSyncer) { + d->mRelationSyncer = new RelationSync(this); + connect(d->mRelationSyncer, SIGNAL(percent(KJob*,ulong)), SLOT(slotPercent(KJob*,ulong))); + connect(d->mRelationSyncer, SIGNAL(result(KJob*)), SLOT(slotRelationSyncDone(KJob*))); + } + d->mRelationSyncer->setRemoteRelations(relations); +} + +void ResourceBasePrivate::slotRelationSyncDone(KJob *job) +{ + Q_Q(ResourceBase); + mRelationSyncer = 0; + if (job->error()) { + if (job->error() != Job::UserCanceled) { + qCWarning(AKONADIAGENTBASE_LOG) << "RelationSync failed: " << job->errorString(); + emit q->error(job->errorString()); + } + } + + scheduler->taskDone(); +} + +#include "resourcebase.moc" +#include "moc_resourcebase.cpp" diff --git a/src/agentbase/resourcebase.h b/src/agentbase/resourcebase.h new file mode 100644 index 0000000..43c79f9 --- /dev/null +++ b/src/agentbase/resourcebase.h @@ -0,0 +1,858 @@ +/* + This file is part of akonadiresources. + + Copyright (c) 2006 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_RESOURCEBASE_H +#define AKONADI_RESOURCEBASE_H + +#include "akonadiagentbase_export.h" +#include "agentbase.h" +#include "collection.h" +#include "item.h" +#include "itemsync.h" + +class KJob; +class Akonadi__ResourceAdaptor; +class ResourceState; + +namespace Akonadi +{ + +class ResourceBasePrivate; + +/** + * @short The base class for all Akonadi resources. + * + * This class should be used as a base class by all resource agents, + * because it encapsulates large parts of the protocol between + * resource agent, agent manager and the Akonadi storage. + * + * It provides many convenience methods to make implementing a + * new Akonadi resource agent as simple as possible. + * + *

How to write a resource

+ * + * The following provides an overview of what you need to do to implement + * your own Akonadi resource. In the following, the term 'backend' refers + * to the entity the resource connects with Akonadi, be it a single file + * or a remote server. + * + * @todo Complete this (online/offline state management) + * + *
Basic %Resource Framework
+ * + * The following is needed to create a new resource: + * - A new class deriving from Akonadi::ResourceBase, implementing at least all + * pure-virtual methods, see below for further details. + * - call init() in your main() function. + * - a .desktop file similar to the following example + * \code + * [Desktop Entry] + * Encoding=UTF-8 + * Name=My Akonadi Resource + * Type=AkonadiResource + * Exec=akonadi_my_resource + * Icon=my-icon + * + * X-Akonadi-MimeTypes= + * X-Akonadi-Capabilities=Resource + * X-Akonadi-Identifier=akonadi_my_resource + * \endcode + * + *
Handling PIM Items
+ * + * To follow item changes in the backend, the following steps are necessary: + * - Implement retrieveItems() to synchronize all items in the given + * collection. If the backend supports incremental retrieval, + * implementing support for that is recommended to improve performance. + * - Convert the items provided by the backend to Akonadi items. + * This typically happens either in retrieveItems() if you retrieved + * the collection synchronously (not recommended for network backends) or + * in the result slot of the asynchronous retrieval job. + * Converting means to create Akonadi::Item objects for every retrieved + * item. It's very important that every object has its remote identifier set. + * - Call itemsRetrieved() or itemsRetrievedIncremental() respectively + * with the item objects created above. The Akonadi storage will then be + * updated automatically. Note that it is usually not necessary to manipulate + * any item in the Akonadi storage manually. + * + * To fetch item data on demand, the method retrieveItem() needs to be + * reimplemented. Fetch the requested data there and call itemRetrieved() + * with the result item. + * + * To write local changes back to the backend, you need to re-implement + * the following three methods: + * - itemAdded() + * - itemChanged() + * - itemRemoved() + * + * Note that these three functions don't get the full payload of the items by default, + * you need to change the item fetch scope of the change recorder to fetch the full + * payload. This can be expensive with big payloads, though.
+ * Once you have handled changes in these methods, call changeCommitted(). + * These methods are called whenever a local item related to this resource is + * added, modified or deleted. They are only called if the resource is online, otherwise + * all changes are recorded and replayed as soon the resource is online again. + * + *
Handling Collections
+ * + * To follow collection changes in the backend, the following steps are necessary: + * - Implement retrieveCollections() to retrieve collections from the backend. + * If the backend supports incremental collections updates, implementing + * support for that is recommended to improve performance. + * - Convert the collections of the backend to Akonadi collections. + * This typically happens either in retrieveCollections() if you retrieved + * the collection synchronously (not recommended for network backends) or + * in the result slot of the asynchronous retrieval job. + * Converting means to create Akonadi::Collection objects for every retrieved + * collection. It's very important that every object has its remote identifier + * and its parent remote identifier set. + * - Call collectionsRetrieved() or collectionsRetrievedIncremental() respectively + * with the collection objects created above. The Akonadi storage will then be + * updated automatically. Note that it is usually not necessary to manipulate + * any collection in the Akonadi storage manually. + * + * + * To write local collection changes back to the backend, you need to re-implement + * the following three methods: + * - collectionAdded() + * - collectionChanged() + * - collectionRemoved() + * Once you have handled changes in these methods call changeCommitted(). + * These methods are called whenever a local collection related to this resource is + * added, modified or deleted. They are only called if the resource is online, otherwise + * all changes are recorded and replayed as soon the resource is online again. + * + * @todo Convenience base class for collection-less resources + */ +// FIXME_API: API dox need to be updated for Observer approach (kevin) +class AKONADIAGENTBASE_EXPORT ResourceBase : public AgentBase +{ + Q_OBJECT + +public: + /** + * Use this method in the main function of your resource + * application to initialize your resource subclass. + * This method also takes care of creating a KApplication + * object and parsing command line arguments. + * + * @note In case the given class is also derived from AgentBase::Observer + * it gets registered as its own observer (see AgentBase::Observer), e.g. + * resourceInstance->registerObserver( resourceInstance ); + * + * @code + * + * class MyResource : public ResourceBase + * { + * ... + * }; + * + * int main( int argc, char **argv ) + * { + * return ResourceBase::init( argc, argv ); + * } + * + * @endcode + * + * @param argc number of arguments + * @param argv string arguments + */ + template + static int init(int argc, char **argv) + { + // Disable session management + qunsetenv("SESSION_MANAGER"); + + QApplication app(argc, argv); + const QString id = parseArguments(argc, argv); + T *r = new T(id); + + // check if T also inherits AgentBase::Observer and + // if it does, automatically register it on itself + Observer *observer = dynamic_cast(r); + if (observer != 0) { + r->registerObserver(observer); + } + + return init(r); + } + + /** + * This method is used to set the name of the resource. + */ + void setName(const QString &name); + + /** + * Returns the name of the resource. + */ + QString name() const; + + /** + * Enable or disable automatic progress reporting. By default, it is enabled. + * When enabled, the resource will automatically emit the signals percent() and status() + * while syncing items or collections. + * + * The automatic progress reporting is done on a per item / per collection basis, so if a + * finer granularity is desired, automatic reporting should be disabled and the subclass should + * emit the percent() and status() signals itself. + * + * @param enabled Whether or not automatic emission of the signals is enabled. + * @since 4.7 + */ + void setAutomaticProgressReporting(bool enabled); + +Q_SIGNALS: + /** + * This signal is emitted whenever the name of the resource has changed. + * + * @param name The new name of the resource. + */ + void nameChanged(const QString &name); + + /** + * Emitted when a full synchronization has been completed. + */ + void synchronized(); + + /** + * Emitted when a collection attributes synchronization has been completed. + * + * @param collectionId The identifier of the collection whose attributes got synchronized. + * @since 4.6 + */ + void attributesSynchronized(qlonglong collectionId); + + /** + * Emitted when a collection tree synchronization has been completed. + * + * @since 4.8 + */ + void collectionTreeSynchronized(); + + /** + * Emitted when the item synchronization processed the current batch and is ready for a new one. + * Use this to throttle the delivery to not overload Akonadi. + * + * Throttling can be used during item retrieval (retrieveItems(Akonadi::Collection)) in streaming mode. + * To throttle only deliver itemSyncBatchSize() items, and wait for this signal, then again deliver + * @param remainingBatchSize items. + * + * By always only providing the number of items required to process the batch, the items don't pile + * up in memory and items are only provided as fast as Akonadi can process them. + * + * @see batchSize() + * + * @since 4.14 + */ + void retrieveNextItemSyncBatch(int remainingBatchSize); + +protected Q_SLOTS: + /** + * Retrieve the collection tree from the remote server and supply it via + * collectionsRetrieved() or collectionsRetrievedIncremental(). + * @see collectionsRetrieved(), collectionsRetrievedIncremental() + */ + virtual void retrieveCollections() = 0; + + /** + * Retrieve all tags from the backend + * @see tagsRetrieved() + */ + virtual void retrieveTags(); + + /** + * Retrieve all relations from the backend + * @see relationsRetrieved() + */ + virtual void retrieveRelations(); + + /** + * Retrieve the attributes of a single collection from the backend. The + * collection to retrieve attributes for is provided as @p collection. + * Add the attributes parts and call collectionAttributesRetrieved() + * when done. + * + * @param collection The collection whose attributes should be retrieved. + * @see collectionAttributesRetrieved() + * @since 4.6 + */ + virtual void retrieveCollectionAttributes(const Akonadi::Collection &collection); + + /** + * Retrieve all (new/changed) items in collection @p collection. + * It is recommended to use incremental retrieval if the backend supports that + * and provide the result by calling itemsRetrievedIncremental(). + * If incremental retrieval is not possible, provide the full listing by calling + * itemsRetrieved( const Item::List& ). + * In any case, ensure that all items have a correctly set remote identifier + * to allow synchronizing with items already existing locally. + * In case you don't want to use the built-in item syncing code, store the retrieved + * items manually and call itemsRetrieved() once you are done. + * @param collection The collection whose items to retrieve. + * @see itemsRetrieved( const Item::List& ), itemsRetrievedIncremental(), itemsRetrieved(), currentCollection(), batchSize() + */ + virtual void retrieveItems(const Akonadi::Collection &collection) = 0; + + /** + * Returns the batch size used during the item sync. + * + * This can be used to throttle the item delivery. + * + * @see retrieveNextItemSyncBatch(int), retrieveItems(Akonadi::Collection) + * @since 4.14 + */ + int itemSyncBatchSize() const; + + /** + * Set the batch size used during the item sync. + * The default is 10. + * + * @see retrieveNextItemSyncBatch(int) + * @since 4.14 + */ + void setItemSyncBatchSize(int batchSize); + + /** + * Set to true to scheudle an attribute sync before every item sync. + * The default is false. + * + * @since 4.15 + */ + void setScheduleAttributeSyncBeforeItemSync(bool); + + /** + * Retrieve a single item from the backend. The item to retrieve is provided as @p item. + * Add the requested payload parts and call itemRetrieved() when done. + * @param item The empty item whose payload should be retrieved. Use this object when delivering + * the result instead of creating a new item to ensure conflict detection will work. + * @param parts The item parts that should be retrieved. + * @return false if there is an immediate error when retrieving the item. + * @see itemRetrieved() + */ + virtual bool retrieveItem(const Akonadi::Item &item, const QSet &parts) = 0; + + /** + * Abort any activity in progress in the backend. By default this method does nothing. + * + * @since 4.6 + */ + virtual void abortActivity(); + + /** + * Dump resource internals, for debugging. + * @since 4.9 + */ + virtual QString dumpResourceToString() const + { + return QString(); + } + +protected: + /** + * Creates a base resource. + * + * @param id The instance id of the resource. + */ + ResourceBase(const QString &id); + + /** + * Destroys the base resource. + */ + ~ResourceBase(); + + /** + * Call this method from retrieveItem() once the result is available. + * + * @param item The retrieved item. + */ + void itemRetrieved(const Item &item); + + /** + * Call this method from retrieveCollectionAttributes() once the result is available. + * + * @param collection The collection whose attributes got retrieved. + * @since 4.6 + */ + void collectionAttributesRetrieved(const Collection &collection); + + /** + * Resets the dirty flag of the given item and updates the remote id. + * + * Call whenever you have successfully written changes back to the server. + * This implicitly calls changeProcessed(). + * @param item The changed item. + */ + void changeCommitted(const Item &item); + + /** + * Resets the dirty flag of all given items and updates remote ids. + * + * Call whenever you have successfully written changes back to the server. + * This implicitly calls changeProcessed(). + * @param items Changed items + * + * @since 4.11 + */ + void changesCommitted(const Item::List &items); + + /** + * Resets the dirty flag of the given tag and updates the remote id. + * + * Call whenever you have successfully written changes back to the server. + * This implicitly calls changeProcessed(). + * @param tag Changed tag. + * + * @since 4.13 + */ + void changeCommitted(const Tag &tag); + + /** + * Call whenever you have successfully handled or ignored a collection + * change notification. + * + * This will update the remote identifier of @p collection if necessary, + * as well as any other collection attributes. + * This implicitly calls changeProcessed(). + * @param collection The collection which changes have been handled. + */ + void changeCommitted(const Collection &collection); + + /** + * Call this to supply the full folder tree retrieved from the remote server. + * + * @param collections A list of collections. + * @see collectionsRetrievedIncremental() + */ + void collectionsRetrieved(const Collection::List &collections); + + void tagsRetrieved(const Tag::List &tags, const QHash &tagMembers); + void relationsRetrieved(const Relation::List &relations); + + /** + * Call this to supply incrementally retrieved collections from the remote server. + * + * @param changedCollections Collections that have been added or changed. + * @param removedCollections Collections that have been deleted. + * @see collectionsRetrieved() + */ + void collectionsRetrievedIncremental(const Collection::List &changedCollections, + const Collection::List &removedCollections); + + /** + * Enable collection streaming, that is collections don't have to be delivered at once + * as result of a retrieveCollections() call but can be delivered by multiple calls + * to collectionsRetrieved() or collectionsRetrievedIncremental(). When all collections + * have been retrieved, call collectionsRetrievalDone(). + * @param enable @c true if collection streaming should be enabled, @c false by default + */ + void setCollectionStreamingEnabled(bool enable); + + /** + * Call this method to indicate you finished synchronizing the collection tree. + * + * This is not needed if you use the built in syncing without collection streaming + * and call collectionsRetrieved() or collectionRetrievedIncremental() instead. + * If collection streaming is enabled, call this method once all collections have been delivered + * using collectionsRetrieved() or collectionsRetrievedIncremental(). + */ + void collectionsRetrievalDone(); + + /** + * Allows to keep locally changed collection parts during the collection sync. + * + * This is useful for resources to be able to provide default values during the collection + * sync, while preserving eventual more up-to date values. + * + * Valid values are attribute types and "CONTENTMIMETYPE" for the collections content mimetypes. + * + * By default this is enabled for the EntityDisplayAttribute. + * + * @param parts A set parts for which local changes should be preserved. + * @since 4.14 + */ + void setKeepLocalCollectionChanges(const QSet &parts); + + /** + * Call this method to supply the full collection listing from the remote server. Items not present in the list + * will be dropped from the Akonadi database. + * + * If the remote server supports incremental listing, it's strongly + * recommended to use itemsRetrievedIncremental() instead. + * @param items A list of items. + * @see itemsRetrievedIncremental(). + */ + void itemsRetrieved(const Item::List &items); + + /** + * Call this method when you want to use the itemsRetrieved() method + * in streaming mode and indicate the amount of items that will arrive + * that way. + * + * @warning By default this will end the item sync automatically once + * sufficient items were delivered. To disable this and only make use + * of the progress reporting, use setDisableAutomaticItemDeliveryDone() + * + * @note Use setItemStreamingEnabled( true ) + itemsRetrieved[Incremental]() + * + itemsRetrieved() and avoid the automatic delivery based on the total + * number of items. + * + * @param amount number of items that will arrive in streaming mode + */ + void setTotalItems(int amount); + + /** + * Disables the automatic completion of the item sync, + * based on the number of delivered items. + * + * This ensures that the item sync only finishes once itemsRetrieved() + * is called, while still making it possible to use the automatic progress + * reporting based on setTotalItems(). + * + * @note This needs to be called once, before the item sync started. + * + * @see setTotalItems(int) + * @since 4.14 + */ + void setDisableAutomaticItemDeliveryDone(bool disable); + + /** + * Enable item streaming. + * Item streaming is disabled by default. + * @param enable @c true if items are delivered in chunks rather in one big block. + */ + void setItemStreamingEnabled(bool enable); + + /** + * Set transaction mode for item sync'ing. + * @param mode item transaction mode + * @see Akonadi::ItemSync::TransactionMode + * @since 4.6 + */ + void setItemTransactionMode(ItemSync::TransactionMode mode); + + /** + * Set merge mode for item sync'ing. + * + * Default merge mode is RIDMerge. + * + * @note This method must be called before first call to itemRetrieved(), + * itemsRetrieved() or itemsRetrievedIncremental(). + * + * @param mode Item merging mode (see ItemCreateJob for details on item merging) + * @see Akonadi::ItemSync::MergeMode + * @ince 4.14.11 + */ + void setItemMergingMode(ItemSync::MergeMode mode); + + /** + * Set the fetch scope applied for item synchronization. + * By default, the one set on the changeRecorder() is used. However, it can make sense + * to specify a specialized fetch scope for synchronization to improve performance. + * The rule of thumb is to remove anything from this fetch scope that does not provide + * additional information regarding whether and item has changed or not. This is primarily + * relevant for backends not supporting incremental retrieval. + * @param fetchScope The fetch scope to use by the internal Akonadi::ItemSync instance. + * @see Akonadi::ItemSync + * @since 4.6 + */ + void setItemSynchronizationFetchScope(const ItemFetchScope &fetchScope); + + /** + * Call this method to supply incrementally retrieved items from the remote server. + * + * @param changedItems Items changed in the backend. + * @param removedItems Items removed from the backend. + */ + void itemsRetrievedIncremental(const Item::List &changedItems, + const Item::List &removedItems); + + /** + * Call this method to indicate you finished synchronizing the current collection. + * + * This is not needed if you use the built in syncing without item streaming + * and call itemsRetrieved() or itemsRetrievedIncremental() instead. + * If item streaming is enabled, call this method once all items have been delivered + * using itemsRetrieved() or itemsRetrievedIncremental(). + * @see retrieveItems() + */ + void itemsRetrievalDone(); + + /** + * Call this method to remove all items and collections of the resource from the + * server cache. + * + * The method should not be used anymore + * + * @see invalidateCache() + * @since 4.3 + */ + void clearCache(); + + /** + * Call this method to invalidate all cached content in @p collection. + * + * The method should be used when the backend indicated that the cached content + * is no longer valid. + * + * @param collection parent of the content to be invalidated in cache + * @since 4.8 + */ + void invalidateCache(const Collection &collection); + + /** + * Returns the collection that is currently synchronized. + * @note Calling this method is only allowed during a collection synchronization task, that + * is directly or indirectly from retrieveItems(). + */ + Collection currentCollection() const; + + /** + * Returns the item that is currently retrieved. + * @note Calling this method is only allowed during fetching a single item, that + * is directly or indirectly from retrieveItem(). + */ + Item currentItem() const; + + /** + * This method is called whenever the resource should start synchronize all data. + */ + void synchronize(); + + /** + * This method is called whenever the collection with the given @p id + * shall be synchronized. + */ + void synchronizeCollection(qint64 id); + + /** + * This method is called whenever the collection with the given @p id + * shall be synchronized. + * @param recursive if true, a recursive synchronization is done + */ + void synchronizeCollection(qint64 id, bool recursive); + + /** + * This method is called whenever the collection with the given @p id + * shall have its attributes synchronized. + * + * @param id The id of the collection to synchronize + * @since 4.6 + */ + void synchronizeCollectionAttributes(qint64 id); + + /** + * Synchronizes the collection attributes. + * + * @param col The id of the collection to synchronize + * @since 4.15 + */ + void synchronizeCollectionAttributes(const Akonadi::Collection &col); + + /** + * Refetches the Collections. + */ + void synchronizeCollectionTree(); + + /** + * Refetches Tags. + */ + void synchronizeTags(); + + /** + * Refetches Relations. + */ + void synchronizeRelations(); + + /** + * Stops the execution of the current task and continues with the next one. + */ + void cancelTask(); + + /** + * Stops the execution of the current task and continues with the next one. + * Additionally an error message is emitted. + * @param error additional error message to be emitted + */ + void cancelTask(const QString &error); + + /** + * Stops the execution of the current task and continues with the next one. + * The current task will be tried again later. + * + * This can be used to delay the task processing until the resource has reached a safe + * state, e.g. login to a server succeeded. + * + * @note This does not change the order of tasks so if there is no task with higher priority + * e.g. a custom task added with #Prepend the deferred task will be processed again. + * + * @since 4.3 + */ + void deferTask(); + + /** + * Inherited from AgentBase. + * + * When going offline, the scheduler aborts the current task, so you should + * do the same in your resource, if the task implementation is asynchronous. + */ + void doSetOnline(bool online) Q_DECL_OVERRIDE; + + /** + * Indicate the use of hierarchical remote identifiers. + * + * This means that it is possible to have two different items with the same + * remoteId in different Collections. + * + * This should be called in the resource constructor as needed. + * + * @param enable whether to enable use of hierarchical remote identifiers + * @since 4.4 + */ + void setHierarchicalRemoteIdentifiersEnabled(bool enable); + + friend class ResourceScheduler; + friend class ::ResourceState; + + /** + * Describes the scheduling priority of a task that has been queued + * for execution. + * + * @see scheduleCustomTask + * @since 4.4 + */ + enum SchedulePriority { + Prepend, ///< The task will be executed as soon as the current task has finished. + AfterChangeReplay, ///< The task is scheduled after the last ChangeReplay task in the queue + Append ///< The task will be executed after all tasks currently in the queue are finished + }; + + /** + * Schedules a custom task in the internal scheduler. It will be queued with + * all other tasks such as change replays and retrieval requests and eventually + * executed by calling the specified method. With the priority parameter the + * time of execution of the Task can be influenced. @see SchedulePriority + * @param receiver The object the slot should be called on. + * @param method The name of the method (and only the name, not signature, not SLOT(...) macro), + * that should be called to execute this task. The method has to be a slot and take a QVariant as + * argument. + * @param argument A QVariant argument passed to the method specified above. Use this to pass task + * parameters. + * @param priority Priority of the task. Use this to influence the position in + * the execution queue. + * @since 4.4 + */ + void scheduleCustomTask(QObject *receiver, const char *method, const QVariant &argument, SchedulePriority priority = Append); + + /** + * Indicate that the current task is finished. Use this method from the slot called via scheduleCustomTaks(). + * As with all the other callbacks, make sure to either call taskDone() or cancelTask()/deferTask() on all + * exit paths, otherwise the resource will hang. + * @since 4.4 + */ + void taskDone(); + + /** + * Dump the contents of the current ChangeReplay + * @since 4.8.1 + */ + QString dumpNotificationListToString() const; + + /** + * Dumps memory usage information to stdout. + * For now it outputs the result of glibc's mallinfo(). + * This is useful to check if your memory problems are due to poor management by glibc. + * Look for a high value on fsmblks when interpreting results. + * man mallinfo for more details. + * @since 4.11 + */ + void dumpMemoryInfo() const; + + /** + * Returns a string with memory usage information. + * @see dumpMemoryInfo() + * + * @since 4.11 + */ + QString dumpMemoryInfoToString() const; + + /** + * Dump the state of the scheduler + * @since 4.8.1 + */ + QString dumpSchedulerToString() const; + +private: + static QString parseArguments(int argc, char **argv); + static int init(ResourceBase *r); + + // dbus resource interface + friend class ::Akonadi__ResourceAdaptor; + + QString requestItemDelivery(qint64 uid, const QString &remoteId, const QString &mimeType, const QByteArrayList &parts); + +private: + Q_DECLARE_PRIVATE(ResourceBase) + + Q_PRIVATE_SLOT(d_func(), void slotAbortRequested()) + Q_PRIVATE_SLOT(d_func(), void slotDeliveryDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionSyncDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotDeleteResourceCollection()) + Q_PRIVATE_SLOT(d_func(), void slotDeleteResourceCollectionDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionDeletionDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotInvalidateCache(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void slotLocalListDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotSynchronizeCollection(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionListDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotSynchronizeCollectionAttributes(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionListForAttributesDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionAttributesSyncDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotItemSyncDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotPercent(KJob *, unsigned long)) + Q_PRIVATE_SLOT(d_func(), void slotDelayedEmitProgress()) + Q_PRIVATE_SLOT(d_func(), void slotPrepareItemRetrieval(const Akonadi::Item &item)) + Q_PRIVATE_SLOT(d_func(), void slotPrepareItemRetrievalResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void changeCommittedResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotSessionReconnected()) + Q_PRIVATE_SLOT(d_func(), void slotRecursiveMoveReplay(RecursiveMover *)) + Q_PRIVATE_SLOT(d_func(), void slotRecursiveMoveReplayResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotTagSyncDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotRelationSyncDone(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void slotSynchronizeTags()) + Q_PRIVATE_SLOT(d_func(), void slotSynchronizeRelations()) + Q_PRIVATE_SLOT(d_func(), void slotItemRetrievalCollectionFetchDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotAttributeRetrievalCollectionFetchDone(KJob *)) +}; + +} + +#ifndef AKONADI_RESOURCE_MAIN +/** + * Convenience Macro for the most common main() function for Akonadi resources. + */ +#define AKONADI_RESOURCE_MAIN( resourceClass ) \ + int main( int argc, char **argv ) \ + { \ + return Akonadi::ResourceBase::init( argc, argv ); \ + } +#endif + +#endif diff --git a/src/agentbase/resourcebase.kcfg b/src/agentbase/resourcebase.kcfg new file mode 100644 index 0000000..9de6327 --- /dev/null +++ b/src/agentbase/resourcebase.kcfg @@ -0,0 +1,13 @@ + + + + + + 5 + This setting allows administrators to set a minimum delay between two mail checks. The user will not be able to choose a value smaller than the value set here. + + + diff --git a/src/agentbase/resourcebasesettings.kcfgc b/src/agentbase/resourcebasesettings.kcfgc new file mode 100644 index 0000000..1605f36 --- /dev/null +++ b/src/agentbase/resourcebasesettings.kcfgc @@ -0,0 +1,9 @@ +File=resourcebase.kcfg +ClassName=ResourceBaseSettings +NameSpace=Akonadi +Singleton=true +ItemAccessors=true +Mutators=true +Visibility=AKONADIAGENTBASE_EXPORT +SetUserTextx=true +IncludeFiles=akonadiagentbase_export.h diff --git a/src/agentbase/resourcescheduler.cpp b/src/agentbase/resourcescheduler.cpp new file mode 100644 index 0000000..0bd4734 --- /dev/null +++ b/src/agentbase/resourcescheduler.cpp @@ -0,0 +1,623 @@ +/* + 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 "resourcescheduler_p.h" + +#include "KDBusConnectionPool" +#include "recursivemover_p.h" + +#include "akonadiagentbase_debug.h" +#include + +#include +#include +#include + +using namespace Akonadi; + +qint64 ResourceScheduler::Task::latestSerial = 0; +static QDBusAbstractInterface *s_resourcetracker = 0; + +//@cond PRIVATE + +ResourceScheduler::ResourceScheduler(QObject *parent) + : QObject(parent) + , mCurrentTasksQueue(-1) + , mOnline(false) +{ +} + +void ResourceScheduler::scheduleFullSync() +{ + Task t; + t.type = SyncAll; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncAll"); + scheduleNext(); +} + +void ResourceScheduler::scheduleCollectionTreeSync() +{ + Task t; + t.type = SyncCollectionTree; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncCollectionTree"); + scheduleNext(); +} + +void ResourceScheduler::scheduleTagSync() +{ + Task t; + t.type = SyncTags; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncTags"); + scheduleNext(); +} + +void ResourceScheduler::scheduleRelationSync() +{ + Task t; + t.type = SyncRelations; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncRelations"); + scheduleNext(); +} + +void ResourceScheduler::scheduleSync(const Collection &col) +{ + Task t; + t.type = SyncCollection; + t.collection = col; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncCollection", QString::number(col.id())); + scheduleNext(); +} + +void ResourceScheduler::scheduleAttributesSync(const Collection &collection) +{ + Task t; + t.type = SyncCollectionAttributes; + t.collection = collection; + + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncCollectionAttributes", QString::number(collection.id())); + scheduleNext(); +} + +void ResourceScheduler::scheduleItemFetch(const Item &item, const QSet &parts, const QDBusMessage &msg) +{ + Task t; + t.type = FetchItem; + t.item = item; + t.itemParts = parts; + + // if the current task does already fetch the requested item, break here but + // keep the dbus message, so we can send the reply later on + if (mCurrentTask == t) { + mCurrentTask.dbusMsgs << msg; + return; + } + + // If this task is already in the queue, merge with it. + TaskList &queue = queueForTaskType(t.type); + const int idx = queue.indexOf(t); + if (idx != -1) { + queue[ idx ].dbusMsgs << msg; + return; + } + + t.dbusMsgs << msg; + queue << t; + signalTaskToTracker(t, "FetchItem", QString::number(item.id())); + scheduleNext(); +} + +void ResourceScheduler::scheduleResourceCollectionDeletion() +{ + Task t; + t.type = DeleteResourceCollection; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "DeleteResourceCollection"); + scheduleNext(); +} + +void ResourceScheduler::scheduleCacheInvalidation(const Collection &collection) +{ + Task t; + t.type = InvalideCacheForCollection; + t.collection = collection; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "InvalideCacheForCollection", QString::number(collection.id())); + scheduleNext(); +} + +void ResourceScheduler::scheduleChangeReplay() +{ + Task t; + t.type = ChangeReplay; + TaskList &queue = queueForTaskType(t.type); + // see ResourceBase::changeProcessed() for why we do not check for mCurrentTask == t here like in the other tasks + if (queue.contains(t)) { + return; + } + queue << t; + signalTaskToTracker(t, "ChangeReplay"); + scheduleNext(); +} + +void ResourceScheduler::scheduleMoveReplay(const Collection &movedCollection, RecursiveMover *mover) +{ + Task t; + t.type = RecursiveMoveReplay; + t.collection = movedCollection; + t.argument = QVariant::fromValue(mover); + TaskList &queue = queueForTaskType(t.type); + + if (queue.contains(t) || mCurrentTask == t) { + return; + } + + queue << t; + signalTaskToTracker(t, "RecursiveMoveReplay", QString::number(t.collection.id())); + scheduleNext(); +} + +void Akonadi::ResourceScheduler::scheduleFullSyncCompletion() +{ + Task t; + t.type = SyncAllDone; + TaskList &queue = queueForTaskType(t.type); + // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost + queue << t; + signalTaskToTracker(t, "SyncAllDone"); + scheduleNext(); +} + +void Akonadi::ResourceScheduler::scheduleCollectionTreeSyncCompletion() +{ + Task t; + t.type = SyncCollectionTreeDone; + TaskList &queue = queueForTaskType(t.type); + // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost + queue << t; + signalTaskToTracker(t, "SyncCollectionTreeDone"); + scheduleNext(); +} + +void Akonadi::ResourceScheduler::scheduleCustomTask(QObject *receiver, const char *methodName, const QVariant &argument, ResourceBase::SchedulePriority priority) +{ + Task t; + t.type = Custom; + t.receiver = receiver; + t.methodName = methodName; + t.argument = argument; + QueueType queueType = GenericTaskQueue; + if (priority == ResourceBase::AfterChangeReplay) { + queueType = AfterChangeReplayQueue; + } else if (priority == ResourceBase::Prepend) { + queueType = PrependTaskQueue; + } + TaskList &queue = mTaskList[queueType]; + + if (queue.contains(t)) { + return; + } + + switch (priority) { + case ResourceBase::Prepend: + queue.prepend(t); + break; + default: + queue.append(t); + break; + } + + signalTaskToTracker(t, "Custom-" + t.methodName); + scheduleNext(); +} + +void ResourceScheduler::taskDone() +{ + if (isEmpty()) { + emit status(AgentBase::Idle, i18nc("@info:status Application ready for work", "Ready")); + } + + if (s_resourcetracker) { + QList argumentList; + argumentList << QString::number(mCurrentTask.serial) + << QString(); + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList); + } + + mCurrentTask = Task(); + mCurrentTasksQueue = -1; + scheduleNext(); +} + +void ResourceScheduler::deferTask() +{ + if (mCurrentTask.type == Invalid) { + return; + } + + if (s_resourcetracker) { + QList argumentList; + argumentList << QString::number(mCurrentTask.serial) + << QString(); + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList); + } + + Task t = mCurrentTask; + mCurrentTask = Task(); + + Q_ASSERT(mCurrentTasksQueue >= 0 && mCurrentTasksQueue < NQueueCount); + mTaskList[mCurrentTasksQueue].prepend(t); + mCurrentTasksQueue = -1; + + signalTaskToTracker(t, "DeferedTask"); + + scheduleNext(); +} + +bool ResourceScheduler::isEmpty() +{ + for (int i = 0; i < NQueueCount; ++i) { + if (!mTaskList[i].isEmpty()) { + return false; + } + } + return true; +} + +void ResourceScheduler::scheduleNext() +{ + if (mCurrentTask.type != Invalid || isEmpty() || !mOnline) { + return; + } + QTimer::singleShot(0, this, &ResourceScheduler::executeNext); +} + +void ResourceScheduler::executeNext() +{ + if (mCurrentTask.type != Invalid || isEmpty()) { + return; + } + + for (int i = 0; i < NQueueCount; ++i) { + if (!mTaskList[i].isEmpty()) { + mCurrentTask = mTaskList[i].takeFirst(); + mCurrentTasksQueue = i; + break; + } + } + + if (s_resourcetracker) { + QList argumentList; + argumentList << QString::number(mCurrentTask.serial); + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobStarted"), argumentList); + } + + switch (mCurrentTask.type) { + case SyncAll: + emit executeFullSync(); + break; + case SyncCollectionTree: + emit executeCollectionTreeSync(); + break; + case SyncCollection: + emit executeCollectionSync(mCurrentTask.collection); + break; + case SyncCollectionAttributes: + emit executeCollectionAttributesSync(mCurrentTask.collection); + break; + case SyncTags: + emit executeTagSync(); + break; + case FetchItem: + emit executeItemFetch(mCurrentTask.item, mCurrentTask.itemParts); + break; + case DeleteResourceCollection: + emit executeResourceCollectionDeletion(); + break; + case InvalideCacheForCollection: + emit executeCacheInvalidation(mCurrentTask.collection); + break; + case ChangeReplay: + emit executeChangeReplay(); + break; + case RecursiveMoveReplay: + emit executeRecursiveMoveReplay(mCurrentTask.argument.value()); + break; + case SyncAllDone: + emit fullSyncComplete(); + break; + case SyncCollectionTreeDone: + emit collectionTreeSyncComplete(); + break; + case SyncRelations: + emit executeRelationSync(); + break; + case Custom: { + const QByteArray methodSig = mCurrentTask.methodName + QByteArray("(QVariant)"); + const bool hasSlotWithVariant = mCurrentTask.receiver->metaObject()->indexOfMethod(methodSig.constData()) != -1; + bool success = false; + if (hasSlotWithVariant) { + success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData(), Q_ARG(QVariant, mCurrentTask.argument)); + Q_ASSERT_X(success || !mCurrentTask.argument.isValid(), "ResourceScheduler::executeNext", "Valid argument was provided but the method wasn't found"); + } + if (!success) { + success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData()); + } + + if (!success) { + qCCritical(AKONADIAGENTBASE_LOG) << "Could not invoke slot" << mCurrentTask.methodName << "on" << mCurrentTask.receiver << "with argument" << mCurrentTask.argument; + } + break; + } + default: { + qCCritical(AKONADIAGENTBASE_LOG) << "Unhandled task type" << mCurrentTask.type; + dump(); + Q_ASSERT(false); + } + } +} + +ResourceScheduler::Task ResourceScheduler::currentTask() const +{ + return mCurrentTask; +} + +void ResourceScheduler::setOnline(bool state) +{ + if (mOnline == state) { + return; + } + mOnline = state; + if (mOnline) { + scheduleNext(); + } else { + if (mCurrentTask.type != Invalid) { + // abort running task + queueForTaskType(mCurrentTask.type).prepend(mCurrentTask); + mCurrentTask = Task(); + mCurrentTasksQueue = -1; + } + // abort pending synchronous tasks, might take longer until the resource goes online again + TaskList &itemFetchQueue = queueForTaskType(FetchItem); + for (QList< Task >::iterator it = itemFetchQueue.begin(); it != itemFetchQueue.end();) { + if ((*it).type == FetchItem) { + (*it).sendDBusReplies(i18nc("@info", "Job canceled.")); + it = itemFetchQueue.erase(it); + if (s_resourcetracker) { + QList argumentList; + argumentList << QString::number(mCurrentTask.serial) + << i18nc("@info", "Job canceled."); + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList); + } + } else { + ++it; + } + } + } +} + +void ResourceScheduler::signalTaskToTracker(const Task &task, const QByteArray &taskType, const QString &debugString) +{ + // if there's a job tracer running, tell it about the new job + if (!s_resourcetracker && KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(QStringLiteral("org.kde.akonadiconsole"))) { + s_resourcetracker = new QDBusInterface(QStringLiteral("org.kde.akonadiconsole"), + QStringLiteral("/resourcesJobtracker"), + QStringLiteral("org.freedesktop.Akonadi.JobTracker"), + KDBusConnectionPool::threadConnection(), 0); + } + + if (s_resourcetracker) { + QList argumentList; + argumentList << static_cast(parent())->identifier() // "session" (in our case resource) + << QString::number(task.serial) // "job" + << QString() // "parent job" + << QString::fromLatin1(taskType) // "job type" + << debugString // "job debugging string" + ; + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobCreated"), argumentList); + } +} + +void ResourceScheduler::collectionRemoved(const Akonadi::Collection &collection) +{ + if (!collection.isValid()) { // should not happen, but you never know... + return; + } + TaskList &queue = queueForTaskType(SyncCollection); + for (QList::iterator it = queue.begin(); it != queue.end();) { + if ((*it).type == SyncCollection && (*it).collection == collection) { + it = queue.erase(it); + qCDebug(AKONADIAGENTBASE_LOG) << " erasing"; + } else { + ++it; + } + } +} + +void ResourceScheduler::Task::sendDBusReplies(const QString &errorMsg) +{ + Q_FOREACH (const QDBusMessage &msg, dbusMsgs) { + QDBusMessage reply(msg.createReply()); + const QString methodName = msg.member(); + if (methodName == QLatin1String("requestItemDelivery")) { + reply << errorMsg; + } else if (methodName.isEmpty()) { + continue; // unittest calls scheduleItemFetch with empty QDBusMessage + } else { + qCCritical(AKONADIAGENTBASE_LOG) << "Got unexpected member:" << methodName; + } + KDBusConnectionPool::threadConnection().send(reply); + } +} + +ResourceScheduler::QueueType ResourceScheduler::queueTypeForTaskType(TaskType type) +{ + switch (type) { + case ChangeReplay: + case RecursiveMoveReplay: + return ChangeReplayQueue; + case FetchItem: + case SyncCollectionAttributes: + return UserActionQueue; + default: + return GenericTaskQueue; + } +} + +ResourceScheduler::TaskList &ResourceScheduler::queueForTaskType(TaskType type) +{ + const QueueType qt = queueTypeForTaskType(type); + return mTaskList[qt]; +} + +void ResourceScheduler::dump() +{ + qCDebug(AKONADIAGENTBASE_LOG) << dumpToString(); +} + +QString ResourceScheduler::dumpToString() const +{ + QString ret; + QTextStream str(&ret); + str << "ResourceScheduler: " << (mOnline ? "Online" : "Offline") << endl; + str << " current task: " << mCurrentTask << endl; + for (int i = 0; i < NQueueCount; ++i) { + const TaskList &queue = mTaskList[i]; + if (queue.isEmpty()) { + str << " queue " << i << " is empty" << endl; + } else { + str << " queue " << i << " " << queue.size() << " tasks:" << endl; + for (QList::const_iterator it = queue.begin(); it != queue.end(); ++it) { + str << " " << (*it) << endl; + } + } + } + return ret; +} + +void ResourceScheduler::clear() +{ + qCDebug(AKONADIAGENTBASE_LOG) << "Clearing ResourceScheduler queues:"; + for (int i = 0; i < NQueueCount; ++i) { + TaskList &queue = mTaskList[i]; + queue.clear(); + } + mCurrentTask = Task(); + mCurrentTasksQueue = -1; +} + +void Akonadi::ResourceScheduler::cancelQueues() +{ + for (int i = 0; i < NQueueCount; ++i) { + TaskList &queue = mTaskList[i]; + if (s_resourcetracker) { + foreach (const Task &t, queue) { + QList argumentList; + argumentList << QString::number(t.serial) << QString(); + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList); + } + } + queue.clear(); + } +} + +static const char s_taskTypes[][27] = { + "Invalid (no task)", + "SyncAll", + "SyncCollectionTree", + "SyncCollection", + "SyncCollectionAttributes", + "SyncTags", + "FetchItem", + "ChangeReplay", + "RecursiveMoveReplay", + "DeleteResourceCollection", + "InvalideCacheForCollection", + "SyncAllDone", + "SyncCollectionTreeDone", + "SyncRelations", + "Custom" +}; + +QTextStream &Akonadi::operator<<(QTextStream &d, const ResourceScheduler::Task &task) +{ + d << task.serial << " " << s_taskTypes[task.type] << " "; + if (task.type != ResourceScheduler::Invalid) { + if (task.collection.isValid()) { + d << "collection " << task.collection.id() << " "; + } + if (task.item.id() != -1) { + d << "item " << task.item.id() << " "; + } + if (!task.methodName.isEmpty()) { + d << task.methodName << " " << task.argument.toString(); + } + } + return d; +} + +QDebug Akonadi::operator<<(QDebug d, const ResourceScheduler::Task &task) +{ + QString s; + QTextStream str(&s); + str << task; + d << s; + return d; +} + +//@endcond + +#include "moc_resourcescheduler_p.cpp" diff --git a/src/agentbase/resourcescheduler_p.h b/src/agentbase/resourcescheduler_p.h new file mode 100644 index 0000000..41bf222 --- /dev/null +++ b/src/agentbase/resourcescheduler_p.h @@ -0,0 +1,293 @@ +/* + 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_RESOURCESCHEDULER_P_H +#define AKONADI_RESOURCESCHEDULER_P_H + +#include "agentbase.h" +#include "collection.h" +#include "item.h" +#include "resourcebase.h" + +#include +#include +#include + +namespace Akonadi +{ + +class RecursiveMover; + +//@cond PRIVATE + +/** + @internal + + Manages synchronization and fetch requests for a resource. + + @todo Attach to the ResourceBase Monitor, +*/ +class ResourceScheduler : public QObject +{ + Q_OBJECT + +public: + // If you change this enum, keep s_taskTypes in sync in resourcescheduler.cpp + enum TaskType { + Invalid, + SyncAll, + SyncCollectionTree, + SyncCollection, + SyncCollectionAttributes, + SyncTags, + FetchItem, + ChangeReplay, + RecursiveMoveReplay, + DeleteResourceCollection, + InvalideCacheForCollection, + SyncAllDone, + SyncCollectionTreeDone, + SyncRelations, + Custom + }; + + class Task + { + static qint64 latestSerial; + + public: + Task() + : serial(++latestSerial) + , type(Invalid) + , receiver(0) + { + } + qint64 serial; + TaskType type; + Collection collection; + Item item; + QSet itemParts; + QList dbusMsgs; + QObject *receiver; + QByteArray methodName; + QVariant argument; + + void sendDBusReplies(const QString &errorMsg); + + bool operator==(const Task &other) const + { + return type == other.type + && (collection == other.collection || (!collection.isValid() && !other.collection.isValid())) + && (item == other.item || (!item.isValid() && !other.item.isValid())) + && itemParts == other.itemParts + && receiver == other.receiver + && methodName == other.methodName + && argument == other.argument; + } + }; + + explicit ResourceScheduler(QObject *parent = 0); + + /** + Schedules a full synchronization. + */ + void scheduleFullSync(); + + /** + Schedules a collection tree sync. + */ + void scheduleCollectionTreeSync(); + + /** + Schedules the synchronization of a single collection. + @param col The collection to synchronize. + */ + void scheduleSync(const Collection &col); + + /** + Schedules synchronizing the attributes of a single collection. + @param collection The collection to synchronize attributes from. + */ + void scheduleAttributesSync(const Collection &collection); + + void scheduleTagSync(); + void scheduleRelationSync(); + + /** + Schedules fetching of a single PIM item. + @param item The item to fetch. + @param parts List of names of the parts of the item to fetch. + @param msg The associated D-Bus message. + */ + void scheduleItemFetch(const Item &item, const QSet &parts, const QDBusMessage &msg); + + /** + Schedules deletion of the resource collection. + This method is used to implement the ResourceBase::clearCache() functionality. + */ + void scheduleResourceCollectionDeletion(); + + /** + * Schedule cache invalidation for @p collection. + * @see ResourceBase::invalidateCache() + */ + void scheduleCacheInvalidation(const Collection &collection); + + /** + Insert synchronization completion marker into the task queue. + */ + void scheduleFullSyncCompletion(); + + /** + Insert collection tree synchronization completion marker into the task queue. + */ + void scheduleCollectionTreeSyncCompletion(); + + /** + Insert a custom task. + @param methodName The method name, without signature, do not use the SLOT() macro + */ + void scheduleCustomTask(QObject *receiver, const char *methodName, const QVariant &argument, ResourceBase::SchedulePriority priority = ResourceBase::Append); + + /** + * Schedule a recursive move replay. + */ + void scheduleMoveReplay(const Collection &movedCollection, RecursiveMover *mover); + + /** + Returns true if no tasks are running or in the queue. + */ + bool isEmpty(); + + /** + Returns the current task. + */ + Task currentTask() const; + + /** + Sets the online state. + */ + void setOnline(bool state); + + /** + Print debug output showing the state of the scheduler. + */ + void dump(); + /** + Print debug output showing the state of the scheduler. + */ + QString dumpToString() const; + + /** + Clear the state of the scheduler. Warning: this is intended to be + used purely in debugging scenarios, as it might cause loss of uncommitted + local changes. + */ + void clear(); + + /** + Cancel everything the scheduler has still in queue. Keep the current task running though. + It can be seen as a less aggressive clear() used when the user requested the resource to + abort its activities. It properly cancel all the tasks in there. + */ + void cancelQueues(); + +public Q_SLOTS: + /** + Schedules replaying changes. + */ + void scheduleChangeReplay(); + + /** + The current task has been finished + */ + void taskDone(); + + /** + The current task can't be finished now and will be rescheduled later + */ + void deferTask(); + + /** + Remove tasks that affect @p collection. + */ + void collectionRemoved(const Akonadi::Collection &collection); + +Q_SIGNALS: + void executeFullSync(); + void executeCollectionAttributesSync(const Akonadi::Collection &col); + void executeCollectionSync(const Akonadi::Collection &col); + void executeCollectionTreeSync(); + void executeTagSync(); + void executeRelationSync(); + void executeItemFetch(const Akonadi::Item &item, const QSet &parts); + void executeResourceCollectionDeletion(); + void executeCacheInvalidation(const Akonadi::Collection &collection); + void executeChangeReplay(); + void executeRecursiveMoveReplay(RecursiveMover *mover); + void collectionTreeSyncComplete(); + void fullSyncComplete(); + void status(int status, const QString &message = QString()); + +private Q_SLOTS: + void scheduleNext(); + void executeNext(); + +private: + void signalTaskToTracker(const Task &task, const QByteArray &taskType, const QString &debugString = QString()); + + // We have a number of task queues, by order of priority. + // * PrependTaskQueue is for deferring the current task + // * ChangeReplay must be first: + // change replays have to happen before we pull changes from the backend, otherwise + // we will overwrite our still unsaved local changes if the backend can't do + // incremental retrieval + // + // * then the stuff that is "immediately after change replay", like writeFile calls. + // * then tasks which the user is waiting for, like ItemFetch (clicking on a mail) or + // SyncCollectionAttributes (folder properties dialog in kmail) + // * then everything else (which includes the background email checking, which can take quite some time). + enum QueueType { + PrependTaskQueue, + ChangeReplayQueue, // one task at most + AfterChangeReplayQueue, // also one task at most, currently + UserActionQueue, + GenericTaskQueue, + NQueueCount + }; + typedef QList TaskList; + + static QueueType queueTypeForTaskType(TaskType type); + TaskList &queueForTaskType(TaskType type); + + TaskList mTaskList[NQueueCount]; + + Task mCurrentTask; + int mCurrentTasksQueue; // queue mCurrentTask came from + bool mOnline; +}; + +QDebug operator<<(QDebug, const ResourceScheduler::Task &task); +QTextStream &operator<<(QTextStream &, const ResourceScheduler::Task &task); + +//@endcond + +} + +#endif diff --git a/src/agentbase/resourcesettings.cpp b/src/agentbase/resourcesettings.cpp new file mode 100644 index 0000000..9110fe4 --- /dev/null +++ b/src/agentbase/resourcesettings.cpp @@ -0,0 +1,42 @@ +/* + Copyright (C) 2010 Laurent Montel + + 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 "resourcesettings.h" + +using namespace Akonadi; + +ResourceSettings *ResourceSettings::mSelf = Q_NULLPTR; + +ResourceSettings *ResourceSettings::self() +{ + if (!mSelf) { + mSelf = new ResourceSettings(); + mSelf->load(); + } + + return mSelf; +} + +ResourceSettings::ResourceSettings() +{ +} + +ResourceSettings::~ResourceSettings() +{ +} diff --git a/src/agentbase/resourcesettings.h b/src/agentbase/resourcesettings.h new file mode 100644 index 0000000..56ce859 --- /dev/null +++ b/src/agentbase/resourcesettings.h @@ -0,0 +1,43 @@ +/* + Copyright (C) 2010 Laurent Montel + + 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 RESOURCESETTINGS_H +#define RESOURCESETTINGS_H + +#include "akonadiagentbase_export.h" +#include "resourcebasesettings.h" + +namespace Akonadi +{ + +class AKONADIAGENTBASE_EXPORT ResourceSettings : public Akonadi::ResourceBaseSettings //krazy:exclude=dpointer +{ + Q_OBJECT +public: + static ResourceSettings *self(); + +private: + ResourceSettings(); + virtual ~ResourceSettings(); + static ResourceSettings *mSelf; +}; + +} + +#endif /* RESOURCESETTINGS_H */ diff --git a/src/agentbase/transportresourcebase.cpp b/src/agentbase/transportresourcebase.cpp new file mode 100644 index 0000000..6c627b4 --- /dev/null +++ b/src/agentbase/transportresourcebase.cpp @@ -0,0 +1,82 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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 "transportresourcebase.h" +#include "transportresourcebase_p.h" + +#include "KDBusConnectionPool" +#include "transportadaptor.h" + +#include "itemfetchjob.h" +#include "itemfetchscope.h" + +#include + +using namespace Akonadi; + +TransportResourceBasePrivate::TransportResourceBasePrivate(TransportResourceBase *qq) + : QObject() + , q(qq) +{ + new Akonadi__TransportAdaptor(this); + KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Transport"), + this, QDBusConnection::ExportAdaptors); +} + +void TransportResourceBasePrivate::send(Item::Id id) +{ + ItemFetchJob *job = new ItemFetchJob(Item(id)); + job->fetchScope().fetchFullPayload(); + job->setProperty("id", QVariant(id)); + connect(job, &KJob::result, this, &TransportResourceBasePrivate::fetchResult); +} + +void TransportResourceBasePrivate::fetchResult(KJob *job) +{ + if (job->error()) { + const Item::Id id = job->property("id").toLongLong(); + emit transportResult(id, (int)TransportResourceBase::TransportFailed, job->errorText()); + return; + } + + ItemFetchJob *fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + + const Item item = fetchJob->items().at(0); + q->sendItem(item); +} + +TransportResourceBase::TransportResourceBase() + : d(new TransportResourceBasePrivate(this)) +{ +} + +TransportResourceBase::~TransportResourceBase() +{ + delete d; +} + +void TransportResourceBase::itemSent(const Item &item, + TransportResult result, + const QString &message) +{ + emit d->transportResult(item.id(), (int)result, message); +} + +#include "moc_transportresourcebase_p.cpp" diff --git a/src/agentbase/transportresourcebase.h b/src/agentbase/transportresourcebase.h new file mode 100644 index 0000000..831ebe7 --- /dev/null +++ b/src/agentbase/transportresourcebase.h @@ -0,0 +1,105 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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_TRANSPORTRESOURCEBASE_H +#define AKONADI_TRANSPORTRESOURCEBASE_H + +#include "akonadiagentbase_export.h" +#include "item.h" + +#include + +namespace Akonadi +{ + +class TransportResourceBasePrivate; + +/** + * @short Resource implementing mail transport capability. + * + * This class allows a resource to provide mail transport (i.e. sending + * mail). A resource than can provide mail transport inherits from both + * ResourceBase and TransportResourceBase, implements the virtual method + * sendItem(), and calls itemSent() when finished sending. + * + * The resource must also have the "MailTransport" capability flag. For example + * the desktop file may contain: + \code + X-Akonadi-Capabilities=Resource,MailTransport + \endcode + * + * For an example of a transport-enabled resource, see + * kdepim/runtime/resources/mailtransport_dummy + * + * @author Constantin Berzan + * @since 4.4 + */ +class AKONADIAGENTBASE_EXPORT TransportResourceBase +{ +public: + /** + * Creates a new transport resource base. + */ + TransportResourceBase(); + + /** + * Destroys the transport resource base. + */ + virtual ~TransportResourceBase(); + + /** + * Describes the result of the transport process. + */ + enum TransportResult { + TransportSucceeded, ///< The transport process succeeded. + TransportFailed ///< The transport process failed. + }; + + /** + * This method is called when the given @p item shall be send. + * When the sending is done or an error occurred during + * sending, call itemSent() with the appropriate result flag. + * + * @param item The message item to be send. + * @see itemSent(). + */ + virtual void sendItem(const Akonadi::Item &item) = 0; + + /** + * This method marks the sending of the passed @p item + * as finished. + * + * @param item The item that was sent. + * @param result The result that indicates whether the sending + * was successful or not. + * @param message An optional text explanation of the result. + * @see Transport. + */ + void itemSent(const Akonadi::Item &item, TransportResult result, + const QString &message = QString()); + +private: + //@cond PRIVATE + TransportResourceBasePrivate *const d; + //@endcond +}; + +} + +#endif diff --git a/src/agentbase/transportresourcebase_p.h b/src/agentbase/transportresourcebase_p.h new file mode 100644 index 0000000..900f4eb --- /dev/null +++ b/src/agentbase/transportresourcebase_p.h @@ -0,0 +1,68 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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_TRANSPORTRESOURCEBASE_P_H +#define AKONADI_TRANSPORTRESOURCEBASE_P_H + +#include "transportresourcebase.h" + +#include + +class Akonadi__TransportAdaptor; + +namespace Akonadi +{ + +class TransportResourceBase; + +/** + @internal + This class hosts the D-Bus adaptor for TransportResourceBase. +*/ +class TransportResourceBasePrivate : public QObject +{ + Q_OBJECT +public: + explicit TransportResourceBasePrivate(TransportResourceBase *qq); + +Q_SIGNALS: + /** + * Emitted when an item has been sent. + * @param item The id of the item that was sent. + * @param result The result of the sending operation. + * @param message An optional textual explanation of the result. + * @since 4.4 + */ + void transportResult(qlonglong item, int result, const QString &message); // D-Bus signal + +private Q_SLOTS: + void fetchResult(KJob *job); + +private: + friend class TransportResourceBase; + friend class ::Akonadi__TransportAdaptor; + + void send(Akonadi::Item::Id message); // D-Bus call + + TransportResourceBase *const q; +}; + +} // namespace Akonadi + +#endif // AKONADI_TRANSPORTRESOURCEBASE_P_H diff --git a/src/agentserver/CMakeLists.txt b/src/agentserver/CMakeLists.txt new file mode 100644 index 0000000..86695d8 --- /dev/null +++ b/src/agentserver/CMakeLists.txt @@ -0,0 +1,53 @@ +# Agent server +set(akonadi_agent_server_srcs + agentpluginloader.cpp + agentserver.cpp + agentthread.cpp + main.cpp +) + +ecm_qt_declare_logging_category(akonadi_agent_server_srcs HEADER akonadiagentserver_debug.h IDENTIFIER AKONADIAGENTSERVER_LOG CATEGORY_NAME log_akonadiagentserver) + +add_executable(akonadi_agent_server ${akonadi_agent_server_srcs}) + +target_link_libraries(akonadi_agent_server + akonadi_shared + KF5AkonadiPrivate + Qt5::Core + Qt5::DBus + Qt5::Widgets +) + +# Agent plugin launcher +set(akonadi_agent_launcher_SRCS + agentpluginloader.cpp + agentlauncher.cpp + akonadiagentserver_debug.cpp +) + +add_executable(akonadi_agent_launcher MACOSX_BUNDLE ${akonadi_agent_launcher_SRCS}) + +target_link_libraries(akonadi_agent_launcher + akonadi_shared + KF5AkonadiPrivate + Qt5::Core + Qt5::Widgets +) + +if(Q_WS_MAC) + set_target_properties(akonadi_agent_launcher PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template) + set_target_properties(akonadi_agent_launcher PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.agentlauncher") + set_target_properties(akonadi_agent_launcher PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Akonadi Agent Launcher") +endif() + +# Install both helper apps. +if(Q_WS_MAC) + install(TARGETS akonadi_agent_launcher + DESTINATION ${AKONADI_BUNDLE_PATH}) +else() + install(TARGETS akonadi_agent_launcher + DESTINATION ${BIN_INSTALL_DIR}) +endif() + +install(TARGETS akonadi_agent_server + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}}) diff --git a/src/agentserver/TODO b/src/agentserver/TODO new file mode 100644 index 0000000..fb58b82 --- /dev/null +++ b/src/agentserver/TODO @@ -0,0 +1,3 @@ +* When the AgentServer process crashes and is restarted by ProcessControl, + somehow the agents/resources that where running must be restarted as + well. diff --git a/src/agentserver/agentlauncher.cpp b/src/agentserver/agentlauncher.cpp new file mode 100644 index 0000000..a7359f4 --- /dev/null +++ b/src/agentserver/agentlauncher.cpp @@ -0,0 +1,61 @@ +/* + Copyright (c) 2010 Bertjan Broeksema + + 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 "agentpluginloader.h" +#include "akonadiagentserver_debug.h" + +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + app.setQuitOnLastWindowClosed(false); + + if (app.arguments().size() != 3) { // Expected usage: ./agent_launcher ${plugin_name} ${identifier} + qCDebug(AKONADIAGENTSERVER_LOG) << "Invalid usage: expected: ./agent_launcher pluginName agentIdentifier"; + return 1; + } + + const QString agentPluginName = app.arguments().at(1); + const QString agentIdentifier = app.arguments().at(2); + + AgentPluginLoader loader; + QPluginLoader *factory = loader.load(agentPluginName); + if (factory == 0) { + return 1; + } + + QObject *instance = 0; + const bool invokeSucceeded = QMetaObject::invokeMethod(factory->instance(), + "createInstance", + Qt::DirectConnection, + Q_RETURN_ARG(QObject *, instance), + Q_ARG(QString, agentIdentifier)); + if (invokeSucceeded) { + qCDebug(AKONADIAGENTSERVER_LOG) << "Agent instance created in separate process."; + } else { + qCDebug(AKONADIAGENTSERVER_LOG) << "Agent instance creation in separate process failed"; + return 2; + } + + const int rv = app.exec(); + delete instance; + return rv; +} diff --git a/src/agentserver/agentpluginloader.cpp b/src/agentserver/agentpluginloader.cpp new file mode 100644 index 0000000..1bf26b4 --- /dev/null +++ b/src/agentserver/agentpluginloader.cpp @@ -0,0 +1,56 @@ +/* + Copyright (c) 2010 Bertjan Broeksema + + 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 "agentpluginloader.h" + +#include +#include + +using namespace Akonadi; + +AgentPluginLoader::AgentPluginLoader() +{ +} + +AgentPluginLoader::~AgentPluginLoader() +{ + qDeleteAll(m_pluginLoaders); + m_pluginLoaders.clear(); +} + +QPluginLoader *AgentPluginLoader::load(const QString &pluginName) +{ + const QString pluginFile = XdgBaseDirs::findPluginFile(pluginName); + if (pluginFile.isEmpty()) { + akError() << Q_FUNC_INFO << "plugin file:" << pluginName << "not found!"; + return 0; + } + + if (m_pluginLoaders.contains(pluginFile)) { + return m_pluginLoaders.value(pluginFile); + } else { + QPluginLoader *loader = new QPluginLoader(pluginFile); + if (!loader->load()) { + akError() << Q_FUNC_INFO << "Failed to load agent: " << loader->errorString(); + delete loader; + return 0; + } + m_pluginLoaders.insert(pluginFile, loader); + return loader; + } +} diff --git a/src/agentserver/agentpluginloader.h b/src/agentserver/agentpluginloader.h new file mode 100644 index 0000000..63dadba --- /dev/null +++ b/src/agentserver/agentpluginloader.h @@ -0,0 +1,49 @@ +/* + Copyright (c) 2010 Bertjan Broeksema + + 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 AGENTPLUGINLOADER_H +#define AGENTPLUGINLOADER_H + +#include +#include + +class AgentPluginLoader +{ +public: + AgentPluginLoader(); + + /** + Deletes all instantiated QPluginLoaders. + */ + ~AgentPluginLoader(); + + /** + Returns the loader for plugins with @param pluginName. Callers must not + take ownership over the returned loader. Loaders will be unloaded and deleted + when the AgentPluginLoader goes out of scope/gets deleted. + + @return the plugin for @param pluginName or 0 if the plugin is not found. + */ + QPluginLoader *load(const QString &pluginName); + +private: + Q_DISABLE_COPY(AgentPluginLoader) + QHash m_pluginLoaders; +}; + +#endif // AGENTPLUGINLOADER_H diff --git a/src/agentserver/agentserver.cpp b/src/agentserver/agentserver.cpp new file mode 100644 index 0000000..1a1372f --- /dev/null +++ b/src/agentserver/agentserver.cpp @@ -0,0 +1,135 @@ +/* + Copyright (c) 2010 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 "agentserver.h" +#include "akonadiagentserver_debug.h" +#include "agentthread.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +AgentServer::AgentServer(QObject *parent) + : QObject(parent) + , m_processingConfigureRequests(false) + , m_quiting(false) +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral(AKONADI_DBUS_AGENTSERVER_PATH), + this, QDBusConnection::ExportScriptableSlots); +} + +AgentServer::~AgentServer() +{ + qCDebug(AKONADIAGENTSERVER_LOG) << Q_FUNC_INFO; + if (!m_quiting) { + quit(); + } +} + +void AgentServer::agentInstanceConfigure(const QString &identifier, qlonglong windowId) +{ + m_configureQueue.enqueue(ConfigureInfo(identifier, windowId)); + if (!m_processingConfigureRequests) { // Start processing the requests if needed. + QTimer::singleShot(0, this, &AgentServer::processConfigureRequest); + } +} + +bool AgentServer::started(const QString &identifier) const +{ + return m_agents.contains(identifier); +} + +void AgentServer::startAgent(const QString &identifier, const QString &typeIdentifier, const QString &fileName) +{ + akDebug() << Q_FUNC_INFO << identifier << typeIdentifier << fileName; + + //First try to load it staticly + Q_FOREACH (QObject *plugin, QPluginLoader::staticInstances()) { + if (plugin->objectName() == fileName) { + AgentThread *thread = new AgentThread(identifier, plugin, this); + m_agents.insert(identifier, thread); + thread->start(); + return; + } + } + + QPluginLoader *loader = m_agentLoader.load(fileName); + if (loader == 0) { + return; // No plugin found, debug output in AgentLoader. + } + + Q_ASSERT(loader->isLoaded()); + + AgentThread *thread = new AgentThread(identifier, loader->instance(), this); + m_agents.insert(identifier, thread); + thread->start(); +} + +void AgentServer::stopAgent(const QString &identifier) +{ + if (!m_agents.contains(identifier)) { + return; + } + + AgentThread *thread = m_agents.take(identifier); + thread->quit(); + thread->wait(); + delete thread; +} + +void AgentServer::quit() +{ + Q_ASSERT(!m_quiting); + m_quiting = true; + + QMutableHashIterator it(m_agents); + while (it.hasNext()) { + it.next(); + stopAgent(it.key()); + } + + QCoreApplication::instance()->quit(); +} + +void AgentServer::processConfigureRequest() +{ + if (m_processingConfigureRequests) { + return; // Protect against reentrancy + } + + m_processingConfigureRequests = true; + + while (!m_configureQueue.empty()) { + const ConfigureInfo info = m_configureQueue.dequeue(); + // call configure on the agent with id info.first for windowId info.second. + Q_ASSERT(m_agents.contains(info.first)); + AgentThread *thread = m_agents.value(info.first); + thread->configure(info.second); + } + + m_processingConfigureRequests = false; +} diff --git a/src/agentserver/agentserver.h b/src/agentserver/agentserver.h new file mode 100644 index 0000000..c166f5d --- /dev/null +++ b/src/agentserver/agentserver.h @@ -0,0 +1,64 @@ +/* + Copyright (c) 2010 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_AGENTSERVER_H +#define AKONADI_AGENTSERVER_H + +#include "agentpluginloader.h" + +#include +#include +#include + +namespace Akonadi { + +class AgentThread; + +class AgentServer : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.AgentServer") + + typedef QPair ConfigureInfo; + +public: + explicit AgentServer(QObject *parent = 0); + ~AgentServer(); + +public Q_SLOTS: + Q_SCRIPTABLE void agentInstanceConfigure(const QString &identifier, qlonglong windowId); + Q_SCRIPTABLE bool started(const QString &identifier) const; + Q_SCRIPTABLE void startAgent(const QString &identifier, const QString &typeIdentifier, const QString &fileName); + Q_SCRIPTABLE void stopAgent(const QString &identifier); + Q_SCRIPTABLE void quit(); + +private Q_SLOTS: + void processConfigureRequest(); + +private: + QHash m_agents; + QQueue m_configureQueue; + AgentPluginLoader m_agentLoader; + bool m_processingConfigureRequests; + bool m_quiting; +}; + +} + +#endif diff --git a/src/agentserver/agentthread.cpp b/src/agentserver/agentthread.cpp new file mode 100644 index 0000000..e80af2d --- /dev/null +++ b/src/agentserver/agentthread.cpp @@ -0,0 +1,64 @@ +/* + Copyright (c) 2010 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 "agentthread.h" +#include "akonadiagentserver_debug.h" + +#include +#include +#include +#include // Needed for WId + +#include +#include + +using namespace Akonadi; + +AgentThread::AgentThread(const QString &identifier, QObject *factory, QObject *parent) + : QThread(parent) + , m_identifier(identifier) + , m_factory(factory) + , m_instance(0) +{ +} + +void AgentThread::run() +{ + const bool invokeSucceeded = QMetaObject::invokeMethod(m_factory, + "createInstance", + Qt::DirectConnection, + Q_RETURN_ARG(QObject *, m_instance), + Q_ARG(QString, m_identifier)); + if (invokeSucceeded) { + qCDebug(AKONADIAGENTSERVER_LOG) << Q_FUNC_INFO << "agent instance created: " << m_instance; + } else { + qCDebug(AKONADIAGENTSERVER_LOG) << Q_FUNC_INFO << "agent instance creation failed"; + } + + exec(); + delete m_instance; +} + +void AgentThread::configure(qlonglong windowId) +{ + QMetaObject::invokeMethod(m_instance, + "configure", + Qt::DirectConnection, + Q_ARG(WId, (WId)windowId)); +} diff --git a/src/agentserver/agentthread.h b/src/agentserver/agentthread.h new file mode 100644 index 0000000..e4850c5 --- /dev/null +++ b/src/agentserver/agentthread.h @@ -0,0 +1,62 @@ +/* + Copyright (c) 2010 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_AGENTTHREAD_H +#define AKONADI_AGENTTHREAD_H + +#include + +namespace Akonadi { + +/** + * @short A class that encapsulates an agent instance inside a thread. + */ +class AgentThread : public QThread +{ + Q_OBJECT + +public: + /** + * Creates a new agent thread. + * + * @param identifier The unique identifier for this agent + * @param factory The factory object that creates the agent instance. + * @param parent The parent object. + */ + AgentThread(const QString &identifier, QObject *factory, QObject *parent = 0); + + /** + * Configures the agent. + * + * @param windowId The parent window id for the config dialog. + */ + void configure(qlonglong windowId); + +protected: + void run(); + +private: + QString m_identifier; + QObject *m_factory; + QObject *m_instance; +}; + +} + +#endif diff --git a/src/agentserver/main.cpp b/src/agentserver/main.cpp new file mode 100644 index 0000000..91f7ed5 --- /dev/null +++ b/src/agentserver/main.cpp @@ -0,0 +1,51 @@ +/* + Copyright (c) 2010 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 "agentserver.h" + +#include + +#include +#include + +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + AkApplication app(argc, argv); + app.setDescription(QStringLiteral("Akonadi Agent Server\nDo not run manually, use 'akonadictl' instead to start/stop Akonadi.")); + app.parseCommandLine(); + qApp->setQuitOnLastWindowClosed(false); + + if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock))) { + akError() << "Akonadi control process not found - aborting."; + akFatal() << "If you started akonadi_agent_server manually, try 'akonadictl start' instead."; + } + + new Akonadi::AgentServer(&app); + + if (!QDBusConnection::sessionBus().registerService(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer))) { + akFatal() << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message(); + } + + return app.exec(); +} diff --git a/src/akonadicontrol/CMakeLists.txt b/src/akonadicontrol/CMakeLists.txt new file mode 100644 index 0000000..a423e44 --- /dev/null +++ b/src/akonadicontrol/CMakeLists.txt @@ -0,0 +1,54 @@ +include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}) + +########### next target ############### + +set(control_SRCS + agenttype.cpp + agentinstance.cpp + agentprocessinstance.cpp + agentthreadinstance.cpp + agentmanager.cpp + controlmanager.cpp + main.cpp + processcontrol.cpp +) + +qt5_add_dbus_adaptor(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml agentmanager.h AgentManager) +qt5_add_dbus_adaptor(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml controlmanager.h ControlManager) +qt5_add_dbus_adaptor(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml agentmanager.h AgentManager) +qt5_add_dbus_interfaces(control_SRCS + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentServer.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Resource.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Server.xml +) +qt5_add_dbus_interface(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml resource_manager) +qt5_add_dbus_interface(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml preprocessor_manager) + +add_executable(akonadi_control ${control_SRCS}) +set_target_properties(akonadi_control PROPERTIES OUTPUT_NAME akonadi_control) + +if (WIN32) + set_target_properties(akonadi_control PROPERTIES WIN32_EXECUTABLE TRUE) + target_link_libraries(akonadi_control ${QT_QTMAIN_LIBRARY}) +endif() + +target_link_libraries(akonadi_control + akonadi_shared + KF5AkonadiPrivate + KF5::ConfigCore + Qt5::Core + Qt5::DBus + Qt5::Gui +) + +install(TARGETS akonadi_control + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) + + +configure_file(org.freedesktop.Akonadi.Control.service.cmake ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.Control.service) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.Control.service + DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}) diff --git a/src/akonadicontrol/agentinstance.cpp b/src/akonadicontrol/agentinstance.cpp new file mode 100644 index 0000000..0339430 --- /dev/null +++ b/src/akonadicontrol/agentinstance.cpp @@ -0,0 +1,230 @@ +/* + 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 "agentinstance.h" + +#include "agenttype.h" +#include "agentmanager.h" + +#include +#include + +AgentInstance::AgentInstance(AgentManager *manager) + : QObject(manager) + , mManager(manager) + , mAgentControlInterface(0) + , mAgentStatusInterface(0) + , mSearchInterface(0) + , mResourceInterface(0) + , mPreprocessorInterface(0) + , mStatus(0) + , mPercent(0) + , mOnline(false) + , mPendingQuit(false) +{ +} + +void AgentInstance::quit() +{ + if (mAgentControlInterface && mAgentControlInterface->isValid()) { + mAgentControlInterface->quit(); + } else { + mPendingQuit = true; + } +} + +void AgentInstance::cleanup() +{ + if (mAgentControlInterface && mAgentControlInterface->isValid()) { + mAgentControlInterface->cleanup(); + } +} + +bool AgentInstance::obtainAgentInterface() +{ + delete mAgentControlInterface; + delete mAgentStatusInterface; + + mAgentControlInterface = + findInterface(Akonadi::DBus::Agent, "/"); + mAgentStatusInterface = + findInterface(Akonadi::DBus::Agent, "/"); + + if (mPendingQuit && mAgentControlInterface && mAgentControlInterface->isValid()) { + mAgentControlInterface->quit(); + mPendingQuit = false; + } + + if (!mAgentControlInterface || !mAgentStatusInterface) { + return false; + } + + mSearchInterface = + findInterface(Akonadi::DBus::Agent, "/Search"); + + connect(mAgentStatusInterface, SIGNAL(status(int,QString)), SLOT(statusChanged(int,QString))); + connect(mAgentStatusInterface, &OrgFreedesktopAkonadiAgentStatusInterface::advancedStatus, this, &AgentInstance::advancedStatusChanged); + connect(mAgentStatusInterface, &OrgFreedesktopAkonadiAgentStatusInterface::percent, this, &AgentInstance::percentChanged); + connect(mAgentStatusInterface, &OrgFreedesktopAkonadiAgentStatusInterface::warning, this, &AgentInstance::warning); + connect(mAgentStatusInterface, &OrgFreedesktopAkonadiAgentStatusInterface::error, this, &AgentInstance::error); + connect(mAgentStatusInterface, &OrgFreedesktopAkonadiAgentStatusInterface::onlineChanged, this, &AgentInstance::onlineChanged); + + refreshAgentStatus(); + return true; +} + +bool AgentInstance::obtainResourceInterface() +{ + delete mResourceInterface; + mResourceInterface = + findInterface(Akonadi::DBus::Resource, "/"); + + if (!mResourceInterface) { + return false; + } + + connect(mResourceInterface, &OrgFreedesktopAkonadiResourceInterface::nameChanged, this, &AgentInstance::resourceNameChanged); + refreshResourceStatus(); + return true; +} + +bool AgentInstance::obtainPreprocessorInterface() +{ + delete mPreprocessorInterface; + mPreprocessorInterface = + findInterface(Akonadi::DBus::Preprocessor, "/"); + return mPreprocessorInterface; +} + +void AgentInstance::statusChanged(int status, const QString &statusMsg) +{ + if (mStatus == status && mStatusMessage == statusMsg) { + return; + } + mStatus = status; + mStatusMessage = statusMsg; + Q_EMIT mManager->agentInstanceStatusChanged(mIdentifier, mStatus, mStatusMessage); +} + +void AgentInstance::advancedStatusChanged(const QVariantMap &status) +{ + Q_EMIT mManager->agentInstanceAdvancedStatusChanged(mIdentifier, status); +} + +void AgentInstance::statusStateChanged(int status) +{ + statusChanged(status, mStatusMessage); +} + +void AgentInstance::statusMessageChanged(const QString &msg) +{ + statusChanged(mStatus, msg); +} + +void AgentInstance::percentChanged(int percent) +{ + if (mPercent == percent) { + return; + } + mPercent = percent; + Q_EMIT mManager->agentInstanceProgressChanged(mIdentifier, mPercent, QString()); +} + +void AgentInstance::warning(const QString &msg) +{ + Q_EMIT mManager->agentInstanceWarning(mIdentifier, msg); +} + +void AgentInstance::error(const QString &msg) +{ + Q_EMIT mManager->agentInstanceError(mIdentifier, msg); +} + +void AgentInstance::onlineChanged(bool state) +{ + if (mOnline == state) { + return; + } + mOnline = state; + Q_EMIT mManager->agentInstanceOnlineChanged(mIdentifier, state); +} + +void AgentInstance::resourceNameChanged(const QString &name) +{ + if (name == mResourceName) { + return; + } + mResourceName = name; + Q_EMIT mManager->agentInstanceNameChanged(mIdentifier, name); +} + +void AgentInstance::refreshAgentStatus() +{ + if (!hasAgentInterface()) { + return; + } + + // async calls so we are not blocked by misbehaving agents + mAgentStatusInterface->callWithCallback(QStringLiteral("status"), QList(), + this, SLOT(statusStateChanged(int)), + SLOT(errorHandler(QDBusError))); + mAgentStatusInterface->callWithCallback(QStringLiteral("statusMessage"), QList(), + this, SLOT(statusMessageChanged(QString)), + SLOT(errorHandler(QDBusError))); + mAgentStatusInterface->callWithCallback(QStringLiteral("progress"), QList(), + this, SLOT(percentChanged(int)), + SLOT(errorHandler(QDBusError))); + mAgentStatusInterface->callWithCallback(QStringLiteral("isOnline"), QList(), + this, SLOT(onlineChanged(bool)), + SLOT(errorHandler(QDBusError))); +} + +void AgentInstance::refreshResourceStatus() +{ + if (!hasResourceInterface()) { + return; + } + + // async call so we are not blocked by misbehaving resources + mResourceInterface->callWithCallback(QStringLiteral("name"), QList(), + this, SLOT(resourceNameChanged(QString)), + SLOT(errorHandler(QDBusError))); +} + +void AgentInstance::errorHandler(const QDBusError &error) +{ + //avoid using the server tracer, can result in D-BUS lockups + akError() << QStringLiteral("D-Bus communication error '%1': '%2'").arg(error.name(), error.message()) ; + // TODO try again after some time, esp. on timeout errors +} + +template +T *AgentInstance::findInterface(Akonadi::DBus::AgentType agentType, const char *path) +{ + T *iface = new T(Akonadi::DBus::agentServiceName(mIdentifier, agentType), + QLatin1String(path), QDBusConnection::sessionBus(), this); + + if (!iface || !iface->isValid()) { + akError() << Q_FUNC_INFO << "Cannot connect to agent instance with identifier" << mIdentifier + << ", error message:" << (iface ? iface->lastError().message() : QString()); + delete iface; + return 0; + } + return iface; +} diff --git a/src/akonadicontrol/agentinstance.h b/src/akonadicontrol/agentinstance.h new file mode 100644 index 0000000..de5e244 --- /dev/null +++ b/src/akonadicontrol/agentinstance.h @@ -0,0 +1,193 @@ +/* + 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 AKONADICONTROL_AGENTINSTANCE_H +#define AKONADICONTROL_AGENTINSTANCE_H + +#include "controlinterface.h" +#include "statusinterface.h" +#include "resourceinterface.h" +#include "preprocessorinterface.h" +#include "searchinterface.h" + +#include + +#include +#include +#include +#include + +class AgentManager; +class AgentType; + +/** + * Represents one agent instance and takes care of communication with it. + * + * The agent exposes multiple D-Bus interfaces. The Control and the Status + * interfaces are implemented by all the agents. The Resource and Preprocessor + * interfaces are obviously implemented only by the agents impersonating resources or + * preprocessors. + */ +class AgentInstance : public QObject +{ + Q_OBJECT +public: + typedef QSharedPointer Ptr; + + explicit AgentInstance(AgentManager *manager); + virtual ~AgentInstance() + { + } + + /** Set/get the unique identifier of this AgentInstance */ + QString identifier() const + { + return mIdentifier; + } + + void setIdentifier(const QString &identifier) + { + mIdentifier = identifier; + } + + QString agentType() const + { + return mType; + } + + int status() const + { + return mStatus; + } + + QString statusMessage() const + { + return mStatusMessage; + } + + int progress() const + { + return mPercent; + } + + bool isOnline() const + { + return mOnline; + } + + QString resourceName() const + { + return mResourceName; + } + + virtual bool start(const AgentType &agentInfo) = 0; + virtual void quit(); + virtual void cleanup(); + virtual void restartWhenIdle() = 0; + virtual void configure(qlonglong windowId) = 0; + + bool hasResourceInterface() const + { + return mResourceInterface; + } + + bool hasAgentInterface() const + { + return mAgentControlInterface && mAgentStatusInterface; + } + + bool hasPreprocessorInterface() const + { + return mPreprocessorInterface; + } + + org::freedesktop::Akonadi::Agent::Control *controlInterface() const + { + return mAgentControlInterface; + } + + org::freedesktop::Akonadi::Agent::Status *statusInterface() const + { + return mAgentStatusInterface; + } + + org::freedesktop::Akonadi::Agent::Search *searchInterface() const + { + return mSearchInterface; + } + + org::freedesktop::Akonadi::Resource *resourceInterface() const + { + return mResourceInterface; + } + + org::freedesktop::Akonadi::Preprocessor *preProcessorInterface() const + { + return mPreprocessorInterface; + } + + bool obtainAgentInterface(); + bool obtainResourceInterface(); + bool obtainPreprocessorInterface(); + +protected Q_SLOTS: + void statusChanged(int status, const QString &statusMsg); + void advancedStatusChanged(const QVariantMap &status); + void statusStateChanged(int status); + void statusMessageChanged(const QString &msg); + void percentChanged(int percent); + void warning(const QString &msg); + void error(const QString &msg); + void onlineChanged(bool state); + void resourceNameChanged(const QString &name); + + void refreshAgentStatus(); + void refreshResourceStatus(); + + void errorHandler(const QDBusError &error); + +private: + template T *findInterface(Akonadi::DBus::AgentType agentType, const char *path = 0); + +protected: + void setAgentType(const QString &agentType) + { + mType = agentType; + } + +private: + QString mIdentifier; + QString mType; + AgentManager *mManager; + org::freedesktop::Akonadi::Agent::Control *mAgentControlInterface; + org::freedesktop::Akonadi::Agent::Status *mAgentStatusInterface; + org::freedesktop::Akonadi::Agent::Search *mSearchInterface; + org::freedesktop::Akonadi::Resource *mResourceInterface; + org::freedesktop::Akonadi::Preprocessor *mPreprocessorInterface; + + int mStatus; + QString mStatusMessage; + int mPercent; + QString mResourceName; + bool mOnline; + bool mPendingQuit; + +}; + +#endif diff --git a/src/akonadicontrol/agentmanager.cpp b/src/akonadicontrol/agentmanager.cpp new file mode 100644 index 0000000..52449f7 --- /dev/null +++ b/src/akonadicontrol/agentmanager.cpp @@ -0,0 +1,876 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * Copyright (c) 2007 Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "agentmanager.h" + +#include "agentmanageradaptor.h" +#include "agentmanagerinternaladaptor.h" +#include "agentprocessinstance.h" +#include "agentserverinterface.h" +#include "agentthreadinstance.h" +#include "preprocessor_manager.h" +#include "processcontrol.h" +#include "resource_manager.h" +#include "serverinterface.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#ifndef QT_NO_DEBUG +#include +#endif +#include +#include +#include +#include + +using Akonadi::ProcessControl; + +static const bool enableAgentServerDefault = false; + +AgentManager::AgentManager(QObject *parent) + : QObject(parent) + , mAgentServer(0) +#ifndef QT_NO_DEBUG + , mAgentWatcher(new QFileSystemWatcher(this)) +#endif +{ + new AgentManagerAdaptor(this); + new AgentManagerInternalAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/AgentManager"), this); + + connect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceOwnerChanged, + this, &AgentManager::serviceOwnerChanged); + + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Server))) { + akFatal() << "akonadiserver already running!"; + } + + const QSettings settings(Akonadi::StandardDirs::agentConfigFile(Akonadi::XdgBaseDirs::ReadOnly), QSettings::IniFormat); + mAgentServerEnabled = settings.value(QStringLiteral("AgentServer/Enabled"), enableAgentServerDefault).toBool(); + + QStringList serviceArgs; + if (Akonadi::Instance::hasIdentifier()) { + serviceArgs << QStringLiteral("--instance") << Akonadi::Instance::identifier(); + } + + mStorageController = new Akonadi::ProcessControl; + mStorageController->setShutdownTimeout(15 * 1000); // the server needs more time for shutdown if we are using an internal mysqld + connect(mStorageController, &Akonadi::ProcessControl::unableToStart, this, &AgentManager::serverFailure); + mStorageController->start(QStringLiteral("akonadiserver"), serviceArgs, Akonadi::ProcessControl::RestartOnCrash); + + if (mAgentServerEnabled) { + mAgentServer = new Akonadi::ProcessControl; + connect(mAgentServer, &Akonadi::ProcessControl::unableToStart, this, &AgentManager::agentServerFailure); + mAgentServer->start(QStringLiteral("akonadi_agent_server"), serviceArgs, Akonadi::ProcessControl::RestartOnCrash); + } + +#ifndef QT_NO_DEBUG + connect(mAgentWatcher, SIGNAL(fileChanged(QString)), SLOT(agentExeChanged(QString))); +#endif +} + +void AgentManager::continueStartup() +{ + // prevent multiple calls in case the server has to be restarted + static bool first = true; + if (!first) { + return; + } + + first = false; + + readPluginInfos(); + Q_FOREACH (const AgentType &info, mAgents) { + Q_EMIT agentTypeAdded(info.identifier); + } + + const QStringList pathList = pluginInfoPathList(); + +#ifndef QT_NO_DEBUG + Q_FOREACH (const QString &path, pathList) { + QFileSystemWatcher *watcher = new QFileSystemWatcher(this); + watcher->addPath(path); + + connect(watcher, SIGNAL(directoryChanged(QString)), + this, SLOT(updatePluginInfos())); + } +#endif + + load(); + Q_FOREACH (const AgentType &info, mAgents) { + ensureAutoStart(info); + } + + // register the real service name once everything is up an running + if (!QDBusConnection::sessionBus().registerService(Akonadi::DBus::serviceName(Akonadi::DBus::Control))) { + // besides a race with an older Akonadi server I have no idea how we could possibly get here... + akFatal() << "Unable to register service as" << Akonadi::DBus::serviceName(Akonadi::DBus::Control) + << "despite having the lock. Error was:" << QDBusConnection::sessionBus().lastError().message(); + } + akDebug() << "Akonadi server is now operational."; +} + +AgentManager::~AgentManager() +{ + cleanup(); +} + +void AgentManager::cleanup() +{ + Q_FOREACH (const AgentInstance::Ptr &instance, mAgentInstances) { + instance->quit(); + } + + mAgentInstances.clear(); + + mStorageController->setCrashPolicy(ProcessControl::StopOnCrash); + org::freedesktop::Akonadi::Server *serverIface = + new org::freedesktop::Akonadi::Server(Akonadi::DBus::serviceName(Akonadi::DBus::Server), QStringLiteral("/Server"), + QDBusConnection::sessionBus(), this); + serverIface->quit(); + + if (mAgentServer) { + mAgentServer->setCrashPolicy(ProcessControl::StopOnCrash); + org::freedesktop::Akonadi::AgentServer *agentServerIface = + new org::freedesktop::Akonadi::AgentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), QDBusConnection::sessionBus(), this); + agentServerIface->quit(); + } + + delete mStorageController; + mStorageController = 0; + + delete mAgentServer; + mAgentServer = 0; +} + +QStringList AgentManager::agentTypes() const +{ + return mAgents.keys(); +} + +QString AgentManager::agentName(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QString(); + } + + return mAgents.value(identifier).name; +} + +QString AgentManager::agentComment(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QString(); + } + + return mAgents.value(identifier).comment; +} + +QString AgentManager::agentIcon(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QString(); + } + + const AgentType info = mAgents.value(identifier); + if (!info.icon.isEmpty()) { + return info.icon; + } + + return QStringLiteral("application-x-executable"); +} + +QStringList AgentManager::agentMimeTypes(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QStringList(); + } + + return mAgents.value(identifier).mimeTypes; +} + +QStringList AgentManager::agentCapabilities(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QStringList(); + } + return mAgents.value(identifier).capabilities; +} + +QVariantMap AgentManager::agentCustomProperties(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QVariantMap(); + } + + return mAgents.value(identifier).custom; +} + +AgentInstance::Ptr AgentManager::createAgentInstance(const AgentType &info) +{ + switch (info.launchMethod) { + case AgentType::Server: + return AgentInstance::Ptr(new Akonadi::AgentThreadInstance(this)); + case AgentType::Launcher: // Fall through + case AgentType::Process: + return AgentInstance::Ptr(new Akonadi::AgentProcessInstance(this)); + default: + Q_ASSERT_X(false, "AgentManger::createAgentInstance", "Unhandled AgentType::LaunchMethod case"); + } + + return AgentInstance::Ptr(); +} + +QString AgentManager::createAgentInstance(const QString &identifier) +{ + if (!checkAgentExists(identifier)) { + return QString(); + } + + const AgentType agentInfo = mAgents.value(identifier); + mAgents[identifier].instanceCounter++; + + const AgentInstance::Ptr instance = createAgentInstance(agentInfo); + if (agentInfo.capabilities.contains(AgentType::CapabilityUnique)) { + instance->setIdentifier(identifier); + } else { + instance->setIdentifier(QStringLiteral("%1_%2").arg(identifier, QString::number(agentInfo.instanceCounter))); + } + + if (mAgentInstances.contains(instance->identifier())) { + akError() << Q_FUNC_INFO << "Cannot create another instance of agent" << identifier; + return QString(); + } + + // Return from this dbus call before we do the next. Otherwise dbus brakes for + // this process. + if (calledFromDBus()) { + connection().send(message().createReply(instance->identifier())); + } + + if (!instance->start(agentInfo)) { + return QString(); + } + + mAgentInstances.insert(instance->identifier(), instance); + registerAgentAtServer(instance->identifier(), agentInfo); + save(); + + return instance->identifier(); +} + +void AgentManager::removeAgentInstance(const QString &identifier) +{ + if (!mAgentInstances.contains(identifier)) { + akError() << Q_FUNC_INFO << "Agent instance with identifier" << identifier << "does not exist"; + return; + } + + const AgentInstance::Ptr instance = mAgentInstances.value(identifier); + if (instance->hasAgentInterface()) { + instance->cleanup(); + } else { + akError() << Q_FUNC_INFO << "Agent instance" << identifier << "has no interface!"; + } + + mAgentInstances.remove(identifier); + + save(); + + org::freedesktop::Akonadi::ResourceManager resmanager(Akonadi::DBus::serviceName(Akonadi::DBus::Server), QStringLiteral("/ResourceManager"), QDBusConnection::sessionBus(), this); + resmanager.removeResourceInstance(instance->identifier()); + + // Kill the preprocessor instance, if any. + org::freedesktop::Akonadi::PreprocessorManager preProcessorManager( + Akonadi::DBus::serviceName(Akonadi::DBus::Server), + QStringLiteral("/PreprocessorManager"), + QDBusConnection::sessionBus(), + this); + + preProcessorManager.unregisterInstance(instance->identifier()); + + if (instance->hasAgentInterface()) { + akDebug() << "AgentManager::removeAgentInstance: calling instance->quit()"; + instance->quit(); + } else { + akError() << Q_FUNC_INFO << "Agent instance" << identifier << "has no interface!"; + } + + Q_EMIT agentInstanceRemoved(identifier); +} + +QString AgentManager::agentInstanceType(const QString &identifier) +{ + if (!mAgentInstances.contains(identifier)) { + akError() << Q_FUNC_INFO << "Agent instance with identifier" << identifier << "does not exist"; + return QString(); + } + + return mAgentInstances.value(identifier)->agentType(); +} + +QStringList AgentManager::agentInstances() const +{ + return mAgentInstances.keys(); +} + +int AgentManager::agentInstanceStatus(const QString &identifier) const +{ + if (!checkInstance(identifier)) { + return 2; + } + + return mAgentInstances.value(identifier)->status(); +} + +QString AgentManager::agentInstanceStatusMessage(const QString &identifier) const +{ + if (!checkInstance(identifier)) { + return QString(); + } + + return mAgentInstances.value(identifier)->statusMessage(); +} + +uint AgentManager::agentInstanceProgress(const QString &identifier) const +{ + if (!checkInstance(identifier)) { + return 0; + } + + return mAgentInstances.value(identifier)->progress(); +} + +QString AgentManager::agentInstanceProgressMessage(const QString &identifier) const +{ + Q_UNUSED(identifier); + + return QString(); +} + +void AgentManager::agentInstanceConfigure(const QString &identifier, qlonglong windowId) +{ + if (!checkAgentInterfaces(identifier, QStringLiteral("agentInstanceConfigure"))) { + return; + } + + mAgentInstances.value(identifier)->configure(windowId); +} + +bool AgentManager::agentInstanceOnline(const QString &identifier) +{ + if (!checkInstance(identifier)) { + return false; + } + + return mAgentInstances.value(identifier)->isOnline(); +} + +void AgentManager::setAgentInstanceOnline(const QString &identifier, bool state) +{ + if (!checkAgentInterfaces(identifier, QStringLiteral("setAgentInstanceOnline"))) { + return; + } + + mAgentInstances.value(identifier)->statusInterface()->setOnline(state); +} + +// resource specific methods // +void AgentManager::setAgentInstanceName(const QString &identifier, const QString &name) +{ + if (!checkResourceInterface(identifier, QStringLiteral("setAgentInstanceName"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->setName(name); +} + +QString AgentManager::agentInstanceName(const QString &identifier) const +{ + if (!checkInstance(identifier)) { + return QString(); + } + + const AgentInstance::Ptr instance = mAgentInstances.value(identifier); + if (!instance->resourceName().isEmpty()) { + return instance->resourceName(); + } + + if (!checkAgentExists(instance->agentType())) { + return QString(); + } + + return mAgents.value(instance->agentType()).name; +} + +void AgentManager::agentInstanceSynchronize(const QString &identifier) +{ + if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronize"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->synchronize(); +} + +void AgentManager::agentInstanceSynchronizeCollectionTree(const QString &identifier) +{ + if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronizeCollectionTree"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->synchronizeCollectionTree(); +} + +void AgentManager::agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection) +{ + agentInstanceSynchronizeCollection(identifier, collection, false); +} + +void AgentManager::agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection, bool recursive) +{ + if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronizeCollection"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->synchronizeCollection(collection, recursive); +} + +void AgentManager::restartAgentInstance(const QString &identifier) +{ + if (!checkInstance(identifier)) { + return; + } + + mAgentInstances.value(identifier)->restartWhenIdle(); +} + +void AgentManager::updatePluginInfos() +{ + const QHash oldInfos = mAgents; + readPluginInfos(); + + Q_FOREACH (const AgentType &oldInfo, oldInfos) { + if (!mAgents.contains(oldInfo.identifier)) { + Q_EMIT agentTypeRemoved(oldInfo.identifier); + } + } + + Q_FOREACH (const AgentType &newInfo, mAgents) { + if (!oldInfos.contains(newInfo.identifier)) { + Q_EMIT agentTypeAdded(newInfo.identifier); + ensureAutoStart(newInfo); + } + } +} + +void AgentManager::readPluginInfos() +{ +#ifndef QT_NO_DEBUG + if (!mAgentWatcher->files().isEmpty()) { + mAgentWatcher->removePaths(mAgentWatcher->files()); + } +#endif + mAgents.clear(); + + const QStringList pathList = pluginInfoPathList(); + + Q_FOREACH (const QString &path, pathList) { + const QDir directory(path, QStringLiteral("*.desktop")); + readPluginInfos(directory); + } +} + +void AgentManager::readPluginInfos(const QDir &directory) +{ + const QStringList files = directory.entryList(); + akDebug() << "PLUGINS: " << directory.canonicalPath(); + akDebug() << "PLUGINS: " << files; + + for (int i = 0; i < files.count(); ++i) { + const QString fileName = directory.absoluteFilePath(files[i]); + + AgentType agentInfo; + if (agentInfo.load(fileName, this)) { + if (mAgents.contains(agentInfo.identifier)) { + akError() << Q_FUNC_INFO << "Duplicated agent identifier" << agentInfo.identifier << "from file" << fileName; + continue; + } + + const QString disableAutostart = getEnv("AKONADI_DISABLE_AGENT_AUTOSTART"); + if (!disableAutostart.isEmpty()) { + akDebug() << "Autostarting of agents is disabled."; + agentInfo.capabilities.removeOne(AgentType::CapabilityAutostart); + } + + if (!mAgentServerEnabled && agentInfo.launchMethod == AgentType::Server) { + agentInfo.launchMethod = AgentType::Launcher; + } + + if (agentInfo.launchMethod == AgentType::Process) { + const QString executable = Akonadi::XdgBaseDirs::findExecutableFile(agentInfo.exec); + if (executable.isEmpty()) { + akError() << "Executable" << agentInfo.exec << "for agent" << agentInfo.identifier << "could not be found!"; + continue; + } +#ifndef QT_NO_DEBUG + if (!mAgentWatcher->files().contains(executable)) { + mAgentWatcher->addPath(executable); + } +#endif + } + + akDebug() << "PLUGINS inserting: " << agentInfo.identifier << agentInfo.instanceCounter << agentInfo.capabilities; + mAgents.insert(agentInfo.identifier, agentInfo); + } + } +} + +QStringList AgentManager::pluginInfoPathList() +{ + return Akonadi::XdgBaseDirs::findAllResourceDirs("data", QStringLiteral("akonadi/agents")); +} + +void AgentManager::load() +{ + org::freedesktop::Akonadi::ResourceManager resmanager(Akonadi::DBus::serviceName(Akonadi::DBus::Server), QStringLiteral("/ResourceManager"), QDBusConnection::sessionBus(), this); + const QStringList knownResources = resmanager.resourceInstances(); + + QSettings file(Akonadi::StandardDirs::agentConfigFile(Akonadi::XdgBaseDirs::ReadOnly), QSettings::IniFormat); + file.beginGroup(QStringLiteral("Instances")); + const QStringList entries = file.childGroups(); + for (int i = 0; i < entries.count(); ++i) { + const QString instanceIdentifier = entries[i]; + + if (mAgentInstances.contains(instanceIdentifier)) { + akError() << Q_FUNC_INFO << "Duplicated instance identifier" << instanceIdentifier << "found in agentsrc"; + continue; + } + + file.beginGroup(entries[i]); + + const QString agentType = file.value(QStringLiteral("AgentType")).toString(); + if (!mAgents.contains(agentType)) { + akError() << Q_FUNC_INFO << "Reference to unknown agent type" << agentType << "in agentsrc"; + file.endGroup(); + continue; + } + const AgentType type = mAgents.value(agentType); + + // recover if the db has been deleted in the meantime or got otherwise corrupted + if (!knownResources.contains(instanceIdentifier) && type.capabilities.contains(AgentType::CapabilityResource)) { + akDebug() << "Recovering instance" << instanceIdentifier << "after database loss"; + registerAgentAtServer(instanceIdentifier, type); + } + + const AgentInstance::Ptr instance = createAgentInstance(type); + instance->setIdentifier(instanceIdentifier); + if (instance->start(type)) { + mAgentInstances.insert(instanceIdentifier, instance); + } + + file.endGroup(); + } + + file.endGroup(); +} + +void AgentManager::save() +{ + QSettings file(Akonadi::StandardDirs::agentConfigFile(Akonadi::XdgBaseDirs::WriteOnly), QSettings::IniFormat); + + Q_FOREACH (const AgentType &info, mAgents) { + info.save(&file); + } + + file.beginGroup(QStringLiteral("Instances")); + file.remove(QString()); + Q_FOREACH (const AgentInstance::Ptr &instance, mAgentInstances) { + file.beginGroup(instance->identifier()); + file.setValue(QStringLiteral("AgentType"), instance->agentType()); + file.endGroup(); + } + + file.endGroup(); +} + +void AgentManager::serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(oldOwner); + // This is called by the D-Bus server when a service comes up, goes down or changes ownership for some reason + // and this is where we "hook up" our different Agent interfaces. + + //akDebug() << "Service " << name << " owner changed from " << oldOwner << " to " << newOwner; + + if ((name == Akonadi::DBus::serviceName(Akonadi::DBus::Server) || name == Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer)) && !newOwner.isEmpty()) { + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Server)) + && (!mAgentServer || QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer)))) { + // server is operational, start agents + continueStartup(); + } + } + + Akonadi::DBus::AgentType agentType = Akonadi::DBus::Unknown; + const QString agentIdentifier = Akonadi::DBus::parseAgentServiceName(name, agentType); + switch (agentType) { + case Akonadi::DBus::Agent: { + // An agent service went up or down + if (newOwner.isEmpty()) { + return; // It went down: we don't care here. + } + + if (!mAgentInstances.contains(agentIdentifier)) { + return; + } + + const AgentInstance::Ptr instance = mAgentInstances.value(agentIdentifier); + const bool restarting = instance->hasAgentInterface(); + if (!instance->obtainAgentInterface()) { + return; + } + + if (!restarting) { + Q_EMIT agentInstanceAdded(agentIdentifier); + } + + break; + } + case Akonadi::DBus::Resource: { + // A resource service went up or down + if (newOwner.isEmpty()) { + return; // It went down: we don't care here. + } + + if (!mAgentInstances.contains(agentIdentifier)) { + return; + } + + mAgentInstances.value(agentIdentifier)->obtainResourceInterface(); + + break; + } + case Akonadi::DBus::Preprocessor: { + // A preprocessor service went up or down + + // If the preprocessor is going up then the org.freedesktop.Akonadi.Agent.* interface + // should be already up (as it's registered before the preprocessor one). + // So if we don't know about the preprocessor as agent instance + // then it's not our preprocessor. + + // If the preprocessor is going down then either the agent interface already + // went down (and it has been already unregistered on the manager side) + // or it's still registered as agent and WE have to unregister it. + // The order of interface deletions depends on Qt but we handle both cases. + + // Check if we "know" about it. + akDebug() << "Preprocessor " << agentIdentifier << " is going up or down..."; + + if (!mAgentInstances.contains(agentIdentifier)) { + akDebug() << "But it isn't registered as agent... not mine (anymore?)"; + return; // not our agent (?) + } + + org::freedesktop::Akonadi::PreprocessorManager preProcessorManager( + Akonadi::DBus::serviceName(Akonadi::DBus::Server), + QStringLiteral("/PreprocessorManager"), + QDBusConnection::sessionBus(), + this); + + if (!preProcessorManager.isValid()) { + akError() << Q_FUNC_INFO << "Could not connect to PreprocessorManager via D-Bus:" << preProcessorManager.lastError().message(); + } else { + if (newOwner.isEmpty()) { + // The preprocessor went down. Unregister it on server side. + + preProcessorManager.unregisterInstance(agentIdentifier); + + } else { + + // The preprocessor went up. Register it on server side. + + if (!mAgentInstances.value(agentIdentifier)->obtainPreprocessorInterface()) { + // Hm.. couldn't hook up its preprocessor interface.. + // Make sure we don't have it in the preprocessor chain + qWarning() << "Couldn't obtain preprocessor interface for instance" << agentIdentifier; + + preProcessorManager.unregisterInstance(agentIdentifier); + return; + } + + akDebug() << "Registering preprocessor instance" << agentIdentifier; + + // Add to the preprocessor chain + preProcessorManager.registerInstance(agentIdentifier); + } + } + + break; + } + default: + break; + } +} + +bool AgentManager::checkInstance(const QString &identifier) const +{ + if (!mAgentInstances.contains(identifier)) { + qWarning() << "Agent instance with identifier " << identifier << " does not exist"; + return false; + } + + return true; +} + +bool AgentManager::checkResourceInterface(const QString &identifier, const QString &method) const +{ + if (!checkInstance(identifier)) { + return false; + } + + if (!mAgents[mAgentInstances[identifier]->agentType()].capabilities.contains(QStringLiteral("Resource"))) { + return false; + } + + if (!mAgentInstances[identifier]->hasResourceInterface()) { + qWarning() << QLatin1String("AgentManager::") + method << " Agent instance " + << identifier << " has no resource interface!"; + return false; + } + + return true; +} + +bool AgentManager::checkAgentExists(const QString &identifier) const +{ + if (!mAgents.contains(identifier)) { + qWarning() << "Agent instance " << identifier << " does not exist."; + return false; + } + + return true; +} + +bool AgentManager::checkAgentInterfaces(const QString &identifier, const QString &method) const +{ + if (!checkInstance(identifier)) { + return false; + } + + if (!mAgentInstances.value(identifier)->hasAgentInterface()) { + qWarning() << "Agent instance (" << method << ") " << identifier << " has no agent interface."; + return false; + } + + return true; +} + +void AgentManager::ensureAutoStart(const AgentType &info) +{ + if (!info.capabilities.contains(AgentType::CapabilityAutostart)) { + return; // no an autostart agent + } + + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), QDBusConnection::sessionBus(), this); + + if (mAgentInstances.contains(info.identifier) || + (agentServer.isValid() && agentServer.started(info.identifier))) { + return; // already running + } + + const AgentInstance::Ptr instance = createAgentInstance(info); + instance->setIdentifier(info.identifier); + if (instance->start(info)) { + mAgentInstances.insert(instance->identifier(), instance); + registerAgentAtServer(instance->identifier(), info); + save(); + } +} + +void AgentManager::agentExeChanged(const QString &fileName) +{ + if (!QFile::exists(fileName)) { + return; + } + + Q_FOREACH (const AgentType &type, mAgents) { + if (fileName.endsWith(type.exec)) { + Q_FOREACH (const AgentInstance::Ptr &instance, mAgentInstances) { + if (instance->agentType() == type.identifier) { + instance->restartWhenIdle(); + } + } + } + } +} + +void AgentManager::registerAgentAtServer(const QString &agentIdentifier, const AgentType &type) +{ + if (type.capabilities.contains(AgentType::CapabilityResource)) { + QScopedPointer resmanager( + new org::freedesktop::Akonadi::ResourceManager(Akonadi::DBus::serviceName(Akonadi::DBus::Server), + QStringLiteral("/ResourceManager"), + QDBusConnection::sessionBus(), this)); + resmanager->addResourceInstance(agentIdentifier, type.capabilities); + } +} + +void AgentManager::addSearch(const QString &query, const QString &queryLanguage, qint64 resultCollectionId) +{ + akDebug() << "AgentManager::addSearch" << query << queryLanguage << resultCollectionId; + Q_FOREACH (const AgentInstance::Ptr &instance, mAgentInstances) { + const AgentType type = mAgents.value(instance->agentType()); + if (type.capabilities.contains(AgentType::CapabilitySearch) && instance->searchInterface()) { + instance->searchInterface()->addSearch(query, queryLanguage, resultCollectionId); + } + } +} + +void AgentManager::removeSearch(quint64 resultCollectionId) +{ + akDebug() << "AgentManager::removeSearch" << resultCollectionId; + Q_FOREACH (const AgentInstance::Ptr &instance, mAgentInstances) { + const AgentType type = mAgents.value(instance->agentType()); + if (type.capabilities.contains(AgentType::CapabilitySearch) && instance->searchInterface()) { + instance->searchInterface()->removeSearch(resultCollectionId); + } + } +} + +void AgentManager::agentServerFailure() +{ + akError() << "Failed to start AgentServer!"; + // if ( requiresAgentServer ) + // QCoreApplication::instance()->exit( 255 ); +} + +void AgentManager::serverFailure() +{ + QCoreApplication::instance()->exit(255); +} diff --git a/src/akonadicontrol/agentmanager.h b/src/akonadicontrol/agentmanager.h new file mode 100644 index 0000000..9f23565 --- /dev/null +++ b/src/akonadicontrol/agentmanager.h @@ -0,0 +1,386 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * Copyright (c) 2007 Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AGENTMANAGER_H +#define AGENTMANAGER_H + +#include +#include +#include + +#include "agenttype.h" +#include "agentinstance.h" + +class QDir; +#ifndef QT_NO_DEBUG +class QFileSystemWatcher; +#endif + +namespace Akonadi { +class ProcessControl; +} + +/** + * The agent manager has knowledge about all available agents (it scans + * for .desktop files in the agent directory) and the available configured + * instances. + */ +class AgentManager : public QObject, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.AgentManager") + +public: + /** + * Creates a new agent manager. + * + * @param parent The parent object. + */ + AgentManager(QObject *parent = 0); + + /** + * Destroys the agent manager. + */ + ~AgentManager(); + + /** + * Called by the crash handler and dtor to terminate + * the child processes. + */ + void cleanup(); + +public Q_SLOTS: + /** + * Returns the list of identifiers of all available + * agent types. + */ + QStringList agentTypes() const; + + /** + * Returns the i18n'ed name of the agent type for + * the given @p identifier. + */ + QString agentName(const QString &identifier) const; + + /** + * Returns the i18n'ed comment of the agent type for + * the given @p identifier.. + */ + QString agentComment(const QString &identifier) const; + + /** + * Returns the icon name of the agent type for the + * given @p identifier. + */ + QString agentIcon(const QString &identifier) const; + + /** + * Returns a list of supported mimetypes of the agent type + * for the given @p identifier. + */ + QStringList agentMimeTypes(const QString &identifier) const; + + /** + * Returns a list of supported capabilities of the agent type + * for the given @p identifier. + */ + QStringList agentCapabilities(const QString &identifier) const; + + /** + * Returns a list of Custom added propeties of the agent type + * for the given @p identifier + * @since 1.11 + */ + QVariantMap agentCustomProperties(const QString &identifier) const; + + /** + * Creates a new agent of the given agent type @p identifier. + * + * @return The identifier of the new agent if created successfully, + * an empty string otherwise. + * The identifier consists of two parts, the type of the + * agent and an unique instance number, and looks like + * the following: 'file_1' or 'imap_267'. + */ + QString createAgentInstance(const QString &identifier); + + /** + * Removes the agent with the given @p identifier. + */ + void removeAgentInstance(const QString &identifier); + + /** + * Returns the type of the agent instance with the given @p identifier. + */ + QString agentInstanceType(const QString &identifier); + + /** + * Returns the list of identifiers of configured instances. + */ + QStringList agentInstances() const; + + /** + * Returns the current status code of the agent with the given @p identifier. + */ + int agentInstanceStatus(const QString &identifier) const; + + /** + * Returns the i18n'ed description of the current status of the agent with + * the given @p identifier. + */ + QString agentInstanceStatusMessage(const QString &identifier) const; + + /** + * Returns the current progress of the agent with the given @p identifier + * in percentage. + */ + uint agentInstanceProgress(const QString &identifier) const; + + /** + * Returns the i18n'ed description of the current progress of the agent with + * the given @p identifier. + */ + QString agentInstanceProgressMessage(const QString &identifier) const; + + /** + * Sets the @p name of the agent instance with the given @p identifier. + */ + void setAgentInstanceName(const QString &identifier, const QString &name); + + /** + * Returns the name of the agent instance with the given @p identifier. + */ + QString agentInstanceName(const QString &identifier) const; + + /** + * Triggers the agent instance with the given @p identifier to show + * its configuration dialog. + * @param windowId Parent window id for the configuration dialog. + */ + void agentInstanceConfigure(const QString &identifier, qlonglong windowId); + + /** + * Triggers the agent instance with the given @p identifier to start + * synchronization. + */ + void agentInstanceSynchronize(const QString &identifier); + + /** + Trigger a synchronization of the collection tree by the given resource agent. + @param identifier The resource agent identifier. + */ + void agentInstanceSynchronizeCollectionTree(const QString &identifier); + + /** + Trigger a synchronization of the given collection by its owning resource agent. + */ + void agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection); + + /** + Trigger a synchronization of the given collection by its owning resource agent. + @param recursive set it true to have sub-collection synchronized as well + */ + void agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection, bool recursive); + + /** + Returns if the agent instance @p identifier is in online mode. + */ + bool agentInstanceOnline(const QString &identifier); + + /** + Sets agent instance @p identifier to online or offline mode. + */ + void setAgentInstanceOnline(const QString &identifier, bool state); + + /** + Restarts the agent instance @p identifier. This is supposed to be used as a + development aid and not something to use during normal operations. + */ + void restartAgentInstance(const QString &identifier); + + /** + * Add a persistent search to remote search agents. + */ + void addSearch(const QString &query, const QString &queryLanguage, qint64 resultCollectionId); + + /** + * Removes a persistent search for the given result collection. + */ + void removeSearch(quint64 resultCollectionId); + +Q_SIGNALS: + /** + * This signal is emitted whenever a new agent type was installed on the system. + * + * @param agentType The identifier of the new agent type. + */ + void agentTypeAdded(const QString &agentType); + + /** + * This signal is emitted whenever an agent type was removed from the system. + * + * @param agentType The identifier of the removed agent type. + */ + void agentTypeRemoved(const QString &agentType); + + /** + * This signal is emitted whenever a new agent instance was created. + * + * @param agentIdentifier The identifier of the new agent instance. + */ + void agentInstanceAdded(const QString &agentIdentifier); + + /** + * This signal is emitted whenever an agent instance was removed. + * + * @param agentIdentifier The identifier of the removed agent instance. + */ + void agentInstanceRemoved(const QString &agentIdentifier); + + /** + * This signal is emitted whenever the status of an agent instance has + * changed. + * + * @param agentIdentifier The identifier of the agent that has changed. + * @param status The new status code. + * @param message The i18n'ed description of the new status. + */ + void agentInstanceStatusChanged(const QString &agentIdentifier, int status, const QString &message); + + /** + * This signal is emitted whenever the status of an agent instance has + * changed. + * + * @param agentIdentifier The identifier of the agent that has changed. + * @param status The object that describes the status change. + */ + void agentInstanceAdvancedStatusChanged(const QString &agentIdentifier, const QVariantMap &status); + + /** + * This signal is emitted whenever the progress of an agent instance has + * changed. + * + * @param agentIdentifier The identifier of the agent that has changed. + * @param progress The new progress in percentage. + * @param message The i18n'ed description of the new progress. + */ + void agentInstanceProgressChanged(const QString &agentIdentifier, uint progress, const QString &message); + + /** + * This signal is emitted whenever an agent instance raised a warning. + * + * @param agentIdentifier The identifier of the agent instance. + * @param message The i18n'ed warning message. + */ + void agentInstanceWarning(const QString &agentIdentifier, const QString &message); + + /** + * This signal is emitted whenever an agent instance raised an error. + * + * @param agentIdentifier The identifier of the agent instance. + * @param message The i18n'ed error message. + */ + void agentInstanceError(const QString &agentIdentifier, const QString &message); + + /** + * This signal is emitted whenever the name of the agent instance has changed. + * + * @param agentIdentifier The identifier of the agent that has changed. + * @param name The new name of the agent instance. + */ + void agentInstanceNameChanged(const QString &agentIdentifier, const QString &name); + + /** + * Emitted when the online state of an agent changed. + */ + void agentInstanceOnlineChanged(const QString &agentIdentifier, bool state); + +private Q_SLOTS: + void updatePluginInfos(); + void serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner); + void agentExeChanged(const QString &fileName); + void agentServerFailure(); + void serverFailure(); + +private: + /** + * Returns the list of directory paths where the .desktop files + * for the plugins are located. + */ + static QStringList pluginInfoPathList(); + + /** + * Loads the internal state from config file. + */ + void load(); + + /** + * Saves internal state to the config file. + */ + void save(); + + /** + * Reads the plugin information from directory. + */ + void readPluginInfos(); + + /** + * Reads the plugin information from directory. + * + * @param directory the directory to get plugin information from + */ + void readPluginInfos(const QDir &directory); + + AgentInstance::Ptr createAgentInstance(const AgentType &type); + bool checkAgentInterfaces(const QString &identifier, const QString &method) const; + bool checkInstance(const QString &identifier) const; + bool checkResourceInterface(const QString &identifier, const QString &method) const; + bool checkAgentExists(const QString &identifier) const; + void ensureAutoStart(const AgentType &info); + void continueStartup(); + void registerAgentAtServer(const QString &agentIdentifier, const AgentType &type); + +private: + /** + * The map which stores the .desktop file + * entries for every agent type. + * + * Key is the agent type (e.g. 'file' or 'imap'). + */ + QHash mAgents; + + /** + * The map which stores the active instances. + * + * Key is the instance identifier. + */ + QHash mAgentInstances; + + Akonadi::ProcessControl *mAgentServer; + Akonadi::ProcessControl *mStorageController; +#ifndef QT_NO_DEBUG + QFileSystemWatcher *mAgentWatcher; +#endif + bool mAgentServerEnabled; + + friend class AgentInstance; +}; + +#endif diff --git a/src/akonadicontrol/agentprocessinstance.cpp b/src/akonadicontrol/agentprocessinstance.cpp new file mode 100644 index 0000000..442fc09 --- /dev/null +++ b/src/akonadicontrol/agentprocessinstance.cpp @@ -0,0 +1,104 @@ +/* + Copyright (c) 2008 Volker Krause + Copyright (c) 2010 Bertjan Broeksema + + 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 "agentprocessinstance.h" + +#include "agenttype.h" +#include "processcontrol.h" + +#include +#include + +using namespace Akonadi; + +AgentProcessInstance::AgentProcessInstance(AgentManager *manager) + : AgentInstance(manager) + , mController(0) +{ +} + +bool AgentProcessInstance::start(const AgentType &agentInfo) +{ + Q_ASSERT(!identifier().isEmpty()); + if (identifier().isEmpty()) { + return false; + } + + setAgentType(agentInfo.identifier); + + Q_ASSERT(agentInfo.launchMethod == AgentType::Process || + agentInfo.launchMethod == AgentType::Launcher); + + const QString executable = (agentInfo.launchMethod == AgentType::Process) + ? XdgBaseDirs::findExecutableFile(agentInfo.exec) : agentInfo.exec; + + if (executable.isEmpty()) { + akError() << Q_FUNC_INFO << "Unable to find agent executable" << agentInfo.exec; + return false; + } + + mController = new Akonadi::ProcessControl(this); + connect(mController, &ProcessControl::unableToStart, this, &AgentProcessInstance::failedToStart); + + if (agentInfo.launchMethod == AgentType::Process) { + QStringList arguments; + arguments << QStringLiteral("--identifier") << identifier(); + mController->start(executable, arguments); + } else { + Q_ASSERT(agentInfo.launchMethod == AgentType::Launcher); + const QStringList arguments = QStringList() << executable << identifier(); + const QString agentLauncherExec = XdgBaseDirs::findExecutableFile(QStringLiteral("akonadi_agent_launcher")); + mController->start(agentLauncherExec, arguments); + } + return true; +} + +void AgentProcessInstance::quit() +{ + mController->setCrashPolicy(Akonadi::ProcessControl::StopOnCrash); + AgentInstance::quit(); +} + +void AgentProcessInstance::cleanup() +{ + mController->setCrashPolicy(Akonadi::ProcessControl::StopOnCrash); + AgentInstance::cleanup(); +} + +void AgentProcessInstance::restartWhenIdle() +{ + if (mController->isRunning()) { + if (status() != 1) { + mController->restartOnceWhenFinished(); + quit(); + } + } else { + mController->start(); + } +} + +void Akonadi::AgentProcessInstance::configure(qlonglong windowId) +{ + controlInterface()->configure(windowId); +} + +void AgentProcessInstance::failedToStart() +{ + statusChanged(2 /*Broken*/, QStringLiteral("Unable to start.")); +} diff --git a/src/akonadicontrol/agentprocessinstance.h b/src/akonadicontrol/agentprocessinstance.h new file mode 100644 index 0000000..e5a7454 --- /dev/null +++ b/src/akonadicontrol/agentprocessinstance.h @@ -0,0 +1,52 @@ +/* + Copyright (c) 2008 Volker Krause + Copyright (c) 2010 Bertjan Broeksema + + 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 AGENTPROCESSINSTANCE_H +#define AGENTPROCESSINSTANCE_H + +#include "agentinstance.h" + +namespace Akonadi { + +class ProcessControl; + +class AgentProcessInstance : public AgentInstance +{ + Q_OBJECT + +public: + explicit AgentProcessInstance(AgentManager *manager); + + virtual bool start(const AgentType &agentInfo); + virtual void quit(); + virtual void cleanup(); + virtual void restartWhenIdle(); + virtual void configure(qlonglong windowId); + +private Q_SLOTS: + void failedToStart(); + +private: + Akonadi::ProcessControl *mController; +}; + +} + +#endif // AGENTPROCESSINSTANCE_H diff --git a/src/akonadicontrol/agentthreadinstance.cpp b/src/akonadicontrol/agentthreadinstance.cpp new file mode 100644 index 0000000..dc4c364 --- /dev/null +++ b/src/akonadicontrol/agentthreadinstance.cpp @@ -0,0 +1,93 @@ +/* + Copyright (c) 2010 Bertjan Broeksema + + 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 "agentthreadinstance.h" + +#include "agentserverinterface.h" +#include "agenttype.h" + +#include + +#include + +#include + +using namespace Akonadi; + +AgentThreadInstance::AgentThreadInstance(AgentManager *manager) + : AgentInstance(manager) +{ + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration, this); + connect(watcher, &QDBusServiceWatcher::serviceRegistered, + this, &AgentThreadInstance::agentServerRegistered); +} + +bool AgentThreadInstance::start(const AgentType &agentInfo) +{ + Q_ASSERT(!identifier().isEmpty()); + if (identifier().isEmpty()) { + return false; + } + + setAgentType(agentInfo.identifier); + mAgentType = agentInfo; + + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), QDBusConnection::sessionBus()); + if (!agentServer.isValid()) { + akDebug() << "AgentServer not up (yet?)"; + return false; + } + + // TODO: let startAgent return a bool. + agentServer.startAgent(identifier(), agentInfo.identifier, agentInfo.exec); + return true; +} + +void AgentThreadInstance::quit() +{ + AgentInstance::quit(); + + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), QDBusConnection::sessionBus()); + agentServer.stopAgent(identifier()); +} + +void AgentThreadInstance::restartWhenIdle() +{ + if (status() != 1 && !identifier().isEmpty()) { + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), QDBusConnection::sessionBus()); + agentServer.stopAgent(identifier()); + agentServer.startAgent(identifier(), agentType(), mAgentType.exec); + } +} + +void AgentThreadInstance::agentServerRegistered() +{ + start(mAgentType); +} + +void Akonadi::AgentThreadInstance::configure(qlonglong windowId) +{ + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), QDBusConnection::sessionBus()); + agentServer.agentInstanceConfigure(identifier(), windowId); +} diff --git a/src/akonadicontrol/agentthreadinstance.h b/src/akonadicontrol/agentthreadinstance.h new file mode 100644 index 0000000..60384ef --- /dev/null +++ b/src/akonadicontrol/agentthreadinstance.h @@ -0,0 +1,47 @@ +/* + Copyright (c) 2010 Bertjan Broeksema + + 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 AGENTTHREADINSTANCE_H +#define AGENTTHREADINSTANCE_H + +#include "agentinstance.h" +#include "agenttype.h" + +namespace Akonadi { + +class AgentThreadInstance : public AgentInstance +{ + Q_OBJECT +public: + AgentThreadInstance(AgentManager *manager); + + virtual bool start(const AgentType &agentInfo); + virtual void quit(); + virtual void restartWhenIdle(); + virtual void configure(qlonglong windowId); + +private Q_SLOTS: + void agentServerRegistered(); + +private: + AgentType mAgentType; +}; + +} + +#endif // AGENTTHREADINSTANCE_H diff --git a/src/akonadicontrol/agenttype.cpp b/src/akonadicontrol/agenttype.cpp new file mode 100644 index 0000000..5cbb771 --- /dev/null +++ b/src/akonadicontrol/agenttype.cpp @@ -0,0 +1,113 @@ +/* + 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 "agenttype.h" +#include "agentmanager.h" + +#include +#include + +#include + +#include +#include + +using namespace Akonadi; + +const QLatin1String AgentType::CapabilityUnique = QLatin1String(AKONADI_AGENT_CAPABILITY_UNIQUE); +const QLatin1String AgentType::CapabilityResource = QLatin1String(AKONADI_AGENT_CAPABILITY_RESOURCE); +const QLatin1String AgentType::CapabilityAutostart = QLatin1String(AKONADI_AGENT_CAPABILITY_AUTOSTART); +const QLatin1String AgentType::CapabilityPreprocessor = QLatin1String(AKONADI_AGENT_CAPABILITY_PREPROCESSOR); +const QLatin1String AgentType::CapabilitySearch = QLatin1String(AKONADI_AGENT_CAPABILITY_SEARCH); + +AgentType::AgentType() + : instanceCounter(0) + , launchMethod(Process) +{ +} + +bool AgentType::load(const QString &fileName, AgentManager *manager) +{ + Q_UNUSED(manager); + + if (!KDesktopFile::isDesktopFile(fileName)) { + return false; + } + + KDesktopFile desktopFile(fileName); + KConfigGroup group = desktopFile.desktopGroup(); + + Q_FOREACH (const QString &key, group.keyList()) { + if (key.startsWith(QLatin1String("X-Akonadi-Custom-"))) { + QString customKey = key.mid(17, key.length()); + custom[customKey] = group.readEntry(key, QStringList()); + } + } + + name = desktopFile.readName(); + comment = desktopFile.readComment(); + icon = desktopFile.readIcon(); + mimeTypes = group.readEntry(QStringLiteral("X-Akonadi-MimeTypes"), QStringList()); + capabilities = group.readEntry(QStringLiteral("X-Akonadi-Capabilities"), QStringList()); + exec = group.readEntry(QStringLiteral("Exec")); + identifier = group.readEntry(QStringLiteral("X-Akonadi-Identifier")); + launchMethod = Process; // Save default + + const QString method = group.readEntry(QStringLiteral("X-Akonadi-LaunchMethod")); + if (method.compare(QLatin1String("AgentProcess"), Qt::CaseInsensitive) == 0) { + launchMethod = Process; + } else if (method.compare(QLatin1String("AgentServer"), Qt::CaseInsensitive) == 0) { + launchMethod = Server; + } else if (method.compare(QLatin1String("AgentLauncher"), Qt::CaseInsensitive) == 0) { + launchMethod = Launcher; + } else if (!method.isEmpty()) { + akError() << Q_FUNC_INFO << "Invalid exec method:" << method << "falling back to AgentProcess"; + } + + if (identifier.isEmpty()) { + akError() << Q_FUNC_INFO << "Agent desktop file" << fileName << "contains empty identifier"; + return false; + } + if (exec.isEmpty()) { + akError() << Q_FUNC_INFO << "Agent desktop file" << fileName << "contains empty Exec entry"; + return false; + } + + // autostart implies unique + if (capabilities.contains(CapabilityAutostart) && !capabilities.contains(CapabilityUnique)) { + capabilities << CapabilityUnique; + } + + // load instance count if needed + if (!capabilities.contains(CapabilityUnique)) { + QSettings agentrc(Akonadi::StandardDirs::agentConfigFile(XdgBaseDirs::ReadOnly), QSettings::IniFormat); + instanceCounter = agentrc.value(QStringLiteral("InstanceCounters/%1/InstanceCounter") + .arg(identifier), 0).toInt(); + } + + return true; +} + +void AgentType::save(QSettings *config) const +{ + Q_ASSERT(config); + if (!capabilities.contains(CapabilityUnique)) { + config->setValue(QStringLiteral("InstanceCounters/%1/InstanceCounter").arg(identifier), instanceCounter); + } +} diff --git a/src/akonadicontrol/agenttype.h b/src/akonadicontrol/agenttype.h new file mode 100644 index 0000000..b21afd4 --- /dev/null +++ b/src/akonadicontrol/agenttype.h @@ -0,0 +1,70 @@ +/* + Copyright (c) 2007 - 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 AGENTTYPE_H +#define AGENTTYPE_H + +#include +#include +#include +#include + +namespace Akonadi { +class ProcessControl; +} + +class AgentManager; +class QSettings; + +class AgentType +{ +public: + enum LaunchMethod { + Process, /// Standalone executable + Server, /// Agent plugin launched in AgentManager + Launcher /// Agent plugin launched in own process + }; + +public: + AgentType(); + bool load(const QString &fileName, AgentManager *manager); + void save(QSettings *config) const; + + QString identifier; + QString name; + QString comment; + QString icon; + QStringList mimeTypes; + QStringList capabilities; + QString exec; + QVariantMap custom; + uint instanceCounter; + LaunchMethod launchMethod; + + static const QLatin1String CapabilityUnique; + static const QLatin1String CapabilityResource; + static const QLatin1String CapabilityAutostart; + static const QLatin1String CapabilityPreprocessor; + static const QLatin1String CapabilitySearch; + +private: + QString readString(const QSettings &file, const QString &key); +}; + +#endif diff --git a/src/akonadicontrol/controlmanager.cpp b/src/akonadicontrol/controlmanager.cpp new file mode 100644 index 0000000..f9f868b --- /dev/null +++ b/src/akonadicontrol/controlmanager.cpp @@ -0,0 +1,41 @@ +/*************************************************************************** + * Copyright (C) 2007 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "controlmanager.h" + +#include +#include + +#include "controlmanageradaptor.h" + +ControlManager::ControlManager(QObject *parent) + : QObject(parent) +{ + new ControlManagerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/ControlManager"), this); +} + +ControlManager::~ControlManager() +{ +} + +void ControlManager::shutdown() +{ + QTimer::singleShot(0, QCoreApplication::instance(), &QCoreApplication::quit); +} diff --git a/src/akonadicontrol/controlmanager.h b/src/akonadicontrol/controlmanager.h new file mode 100644 index 0000000..4ede560 --- /dev/null +++ b/src/akonadicontrol/controlmanager.h @@ -0,0 +1,51 @@ +/*************************************************************************** + * Copyright (C) 2007 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef CONTROLMANAGER_H +#define CONTROLMANAGER_H + +#include + +/** + * The control manager provides a dbus method to shutdown + * the Akonadi Control process cleanly. + */ +class ControlManager : public QObject +{ + Q_OBJECT + +public: + /** + * Creates a new control manager. + */ + ControlManager(QObject *parent = 0); + + /** + * Destroys the control manager. + */ + ~ControlManager(); + +public Q_SLOTS: + /** + * Shutdown the Akonadi Control process cleanly. + */ + void shutdown(); +}; + +#endif diff --git a/src/akonadicontrol/main.cpp b/src/akonadicontrol/main.cpp new file mode 100644 index 0000000..44c2e12 --- /dev/null +++ b/src/akonadicontrol/main.cpp @@ -0,0 +1,97 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "agentmanager.h" +#include "controlmanager.h" +#include "processcontrol.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#ifdef HAVE_UNISTD_H +# include +#endif + +static AgentManager *sAgentManager = 0; + +void crashHandler(int) +{ + if (sAgentManager) { + sAgentManager->cleanup(); + } + + exit(255); +} + +int main(int argc, char **argv) +{ + AkGuiApplication app(argc, argv); + app.setDescription(QStringLiteral("Akonadi Control Process\nDo not run this manually, use 'akonadictl' instead to start/stop Akonadi.")); + app.parseCommandLine(); + + // try to acquire the lock first, that means there is no second instance trying to start up at the same time + // registering the real service name happens in AgentManager::continueStartup(), when everything is in fact up and running + if (!QDBusConnection::sessionBus().registerService(Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock))) { + // We couldn't register. Most likely, it's already running. + const QString lastError = QDBusConnection::sessionBus().lastError().message(); + if (lastError.isEmpty()) { + akError() << "Unable to register service as" << Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock) << "Maybe it's already running?"; + } else { + akError() << "Unable to register service as" << Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock) << "Error was:" << lastError; + } + return -1; + } + + // older Akonadi server versions don't use the lock service yet, so check if one is already running before we try to start another one + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Control))) { + akError() << "Another Akonadi control process is already running."; + return -1; + } + + new ControlManager; + + sAgentManager = new AgentManager; + AkonadiCrash::setEmergencyMethod(crashHandler); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + QGuiApplication::setFallbackSessionManagementEnabled(false); +#endif + // akonadi_control is started on-demand, no need to auto restart by session. + auto disableSessionManagement = [](QSessionManager &sm) { + sm.setRestartHint(QSessionManager::RestartNever); + }; + QObject::connect(qApp, &QGuiApplication::commitDataRequest, disableSessionManagement); + QObject::connect(qApp, &QGuiApplication::saveStateRequest, disableSessionManagement); + + int retval = app.exec(); + + delete sAgentManager; + sAgentManager = 0; + + return retval; +} diff --git a/src/akonadicontrol/org.freedesktop.Akonadi.Control.service.cmake b/src/akonadicontrol/org.freedesktop.Akonadi.Control.service.cmake new file mode 100644 index 0000000..3ded20a --- /dev/null +++ b/src/akonadicontrol/org.freedesktop.Akonadi.Control.service.cmake @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Akonadi.Control +Exec=${CMAKE_INSTALL_FULL_BINDIR}/akonadi_control diff --git a/src/akonadicontrol/processcontrol.cpp b/src/akonadicontrol/processcontrol.cpp new file mode 100644 index 0000000..783873e --- /dev/null +++ b/src/akonadicontrol/processcontrol.cpp @@ -0,0 +1,244 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "processcontrol.h" + +#include + +#include + +#include +#include +#include + +#ifdef Q_OS_UNIX +#include +#include +#endif + +using namespace Akonadi; + +static const int s_maxCrashCount = 2; + +ProcessControl::ProcessControl(QObject *parent) + : QObject(parent) + , mPolicy(RestartOnCrash) + , mFailedToStart(false) + , mCrashCount(0) + , mRestartOnceOnExit(false) + , mShutdownTimeout(1000) +{ + connect(&mProcess, SIGNAL(error(QProcess::ProcessError)), + this, SLOT(slotError(QProcess::ProcessError))); + connect(&mProcess, SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(slotFinished(int,QProcess::ExitStatus))); + mProcess.setProcessChannelMode(QProcess::ForwardedChannels); + + if (Akonadi::Instance::hasIdentifier()) { + QProcessEnvironment env = mProcess.processEnvironment(); + if (env.isEmpty()) { + env = QProcessEnvironment::systemEnvironment(); + } + env.insert(QStringLiteral("AKONADI_INSTANCE"), Akonadi::Instance::identifier()); + mProcess.setProcessEnvironment(env); + } +} + +ProcessControl::~ProcessControl() +{ + stop(); +} + +void ProcessControl::start(const QString &application, const QStringList &arguments, CrashPolicy policy) +{ + mFailedToStart = false; + + mApplication = application; + mArguments = arguments; + mPolicy = policy; + + start(); +} + +void ProcessControl::setCrashPolicy(CrashPolicy policy) +{ + mPolicy = policy; +} + +void ProcessControl::stop() +{ + if (mProcess.state() != QProcess::NotRunning) { + mProcess.waitForFinished(mShutdownTimeout); + mProcess.terminate(); + mProcess.waitForFinished(10000); + mProcess.kill(); + } +} + +void ProcessControl::slotError(QProcess::ProcessError error) +{ + switch (error) { + case QProcess::Crashed: + mCrashCount++; + // do nothing, we'll respawn in slotFinished + break; + case QProcess::FailedToStart: + default: + mFailedToStart = true; + break; + } + + akError() << "ProcessControl: Application" << qPrintable(mApplication) << "stopped unexpectedly (" << mProcess.errorString() << ")"; +} + +void ProcessControl::slotFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitStatus == QProcess::CrashExit) { + if (mPolicy == RestartOnCrash) { + // don't try to start an unstartable application + if (!mFailedToStart && mCrashCount <= s_maxCrashCount) { + qWarning("Application '%s' crashed! %d restarts left.", qPrintable(mApplication), s_maxCrashCount - mCrashCount); + start(); + Q_EMIT restarted(); + } else { + if (mFailedToStart) { + qWarning("Application '%s' failed to start!", qPrintable(mApplication)); + } else { + qWarning("Application '%s' crashed too often. Giving up!", qPrintable(mApplication)); + } + mPolicy = StopOnCrash; + Q_EMIT unableToStart(); + return; + } + } else { + qWarning("Application '%s' crashed. No restart!", qPrintable(mApplication)); + } + } else { + if (exitCode != 0) { + qWarning("ProcessControl: Application '%s' returned with exit code %d (%s)", + qPrintable(mApplication), exitCode, qPrintable(mProcess.errorString())); + if (mPolicy == RestartOnCrash) { + if (mCrashCount > s_maxCrashCount) { + qWarning() << mApplication << "crashed too often and will not be restarted!"; + mPolicy = StopOnCrash; + Q_EMIT unableToStart(); + return; + } + ++mCrashCount; + QTimer::singleShot(60000, this, &ProcessControl::resetCrashCount); + if (!mFailedToStart) { // don't try to start an unstartable application + start(); + Q_EMIT restarted(); + } + } + } else { + if (mRestartOnceOnExit) { + mRestartOnceOnExit = false; + qWarning("Restarting application '%s'.", qPrintable(mApplication)); + start(); + } else { + qWarning("Application '%s' exited normally...", qPrintable(mApplication)); + Q_EMIT unableToStart(); + } + } + } +} + +static bool listContains(const QStringList &list, const QString &pattern) +{ + Q_FOREACH (const QString &s, list) { + if (s.contains(pattern)) { + return true; + } + } + return false; +} + +void ProcessControl::start() +{ +#ifdef Q_OS_UNIX + QString agentValgrind = getEnv("AKONADI_VALGRIND"); + if (!agentValgrind.isEmpty() && (mApplication.contains(agentValgrind) || listContains(mArguments, agentValgrind))) { + + mArguments.prepend(mApplication); + const QString originalArguments = mArguments.join(QString::fromLocal8Bit(" ")); + mApplication = QString::fromLocal8Bit("valgrind"); + + const QString valgrindSkin = getEnv("AKONADI_VALGRIND_SKIN", QString::fromLocal8Bit("memcheck")); + mArguments.prepend(QLatin1String("--tool=") + valgrindSkin); + + const QString valgrindOptions = getEnv("AKONADI_VALGRIND_OPTIONS"); + if (!valgrindOptions.isEmpty()) { + mArguments = valgrindOptions.split(QLatin1Char(' '), QString::SkipEmptyParts) << mArguments; + } + + akDebug(); + akDebug() << "============================================================"; + akDebug() << "ProcessControl: Valgrinding process" << originalArguments; + if (!valgrindSkin.isEmpty()) { + akDebug() << "ProcessControl: Valgrind skin:" << valgrindSkin; + } + if (!valgrindOptions.isEmpty()) { + akDebug() << "ProcessControl: Additional Valgrind options:" << valgrindOptions; + } + akDebug() << "============================================================"; + akDebug(); + } +#endif + + mProcess.start(mApplication, mArguments); + if (!mProcess.waitForStarted()) { + qWarning("ProcessControl: Unable to start application '%s' (%s)", + qPrintable(mApplication), qPrintable(mProcess.errorString())); + Q_EMIT unableToStart(); + return; + } + +#ifdef Q_OS_UNIX + else { + QString agentDebug = QString::fromLocal8Bit(qgetenv("AKONADI_DEBUG_WAIT")); + pid_t pid = mProcess.pid(); + if (!agentDebug.isEmpty() && mApplication.contains(agentDebug)) { + akDebug(); + akDebug() << "============================================================"; + akDebug() << "ProcessControl: Suspending process" << mApplication; + akDebug() << "'gdb --pid" << pid << "' to debug"; + akDebug() << "'kill -SIGCONT" << pid << "' to continue"; + akDebug() << "============================================================"; + akDebug(); + kill(pid, SIGSTOP); + } + } +#endif +} + +void ProcessControl::resetCrashCount() +{ + mCrashCount = 0; +} + +bool ProcessControl::isRunning() const +{ + return mProcess.state() != QProcess::NotRunning; +} + +void ProcessControl::setShutdownTimeout(int msecs) +{ + mShutdownTimeout = msecs; +} diff --git a/src/akonadicontrol/processcontrol.h b/src/akonadicontrol/processcontrol.h new file mode 100644 index 0000000..2435d84 --- /dev/null +++ b/src/akonadicontrol/processcontrol.h @@ -0,0 +1,140 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADI_PROCESSCONTROL_H +#define AKONADI_PROCESSCONTROL_H + +#include +#include + +namespace Akonadi { + +/** + * This class starts and observes a process. Depending on the + * policy it also restarts the process when it crashes. + */ +class ProcessControl : public QObject +{ + Q_OBJECT + +public: + /** + * Theses enums describe the behaviour when the observed + * application crashed. + * + * @li StopOnCrash - The application won't be restarted. + * @li RestartOnCrash - The application is restarted with the same arguments. + */ + enum CrashPolicy { + StopOnCrash, + RestartOnCrash + }; + + /** + * Creates a new process control. + * + * @param parent The parent object. + */ + ProcessControl(QObject *parent = 0); + + /** + * Destroys the process control. + */ + ~ProcessControl(); + + /** + * Starts the @p application with the given list of @p arguments. + */ + void start(const QString &application, const QStringList &arguments = QStringList(), + CrashPolicy policy = RestartOnCrash); + + /** + * Starts the process with the previously set application and arguments. + */ + void start(); + + /** + * Stops the currently running application. + */ + void stop(); + + /** + * Sets the crash policy. + */ + void setCrashPolicy(CrashPolicy policy); + + /** + * Restart the application the next time it exits normally. + */ + void restartOnceWhenFinished() { + mRestartOnceOnExit = true; + } + + /** + * Returns true if the process is currently running. + */ + bool isRunning() const; + + /** + * Sets the time (in msecs) we wait for the process to shut down before we send terminate/kill signals. + * Default is 1 second. + * Note that it is your responsiblility to ask the process to quit, otherwise this is just + * pointless waiting. + */ + void setShutdownTimeout(int msecs); + +Q_SIGNALS: + /** + * This signal is emitted whenever the observed application + * writes something to stderr. + * + * @param errorMsg The error output of the observed application. + */ + void processErrorMessages(const QString &errorMsg); + + /** + * This signal is emitted when the server is restarted after a crash. + */ + void restarted(); + + /** + * Emitted if the process could not be started since it terminated + * too often. + */ + void unableToStart(); + +private Q_SLOTS: + void slotError(QProcess::ProcessError); + void slotFinished(int, QProcess::ExitStatus); + void resetCrashCount(); + +private: + QProcess mProcess; + QString mApplication; + QStringList mArguments; + CrashPolicy mPolicy; + bool mFailedToStart; + int mCrashCount; + bool mRestartOnceOnExit; + int mShutdownTimeout; +}; + +} + +#endif diff --git a/src/akonadictl/CMakeLists.txt b/src/akonadictl/CMakeLists.txt new file mode 100644 index 0000000..b82838d --- /dev/null +++ b/src/akonadictl/CMakeLists.txt @@ -0,0 +1,26 @@ +include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}) + +########### next target ############### + +set(akonadictl_SRCS + akonadistarter.cpp + main.cpp +) + +qt5_add_dbus_interfaces(akonadictl_SRCS + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Janitor.xml +) + +add_executable(akonadictl ${akonadictl_SRCS}) +set_target_properties(akonadictl PROPERTIES OUTPUT_NAME akonadictl) +target_link_libraries(akonadictl + akonadi_shared + KF5AkonadiPrivate + Qt5::Core + Qt5::DBus +) + +install(TARGETS akonadictl + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}} +) diff --git a/src/akonadictl/akonadistarter.cpp b/src/akonadictl/akonadistarter.cpp new file mode 100644 index 0000000..c9e4554 --- /dev/null +++ b/src/akonadictl/akonadistarter.cpp @@ -0,0 +1,87 @@ +/* + 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 "akonadistarter.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +AkonadiStarter::AkonadiStarter(QObject *parent) + : QObject(parent) + , mRegistered(false) +{ + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForOwnerChange, this); + + connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, + this, &AkonadiStarter::serviceOwnerChanged); +} + +bool AkonadiStarter::start() +{ + akDebug() << "Starting Akonadi Server..."; + + QStringList serverArgs; + if (Akonadi::Instance::hasIdentifier()) { + serverArgs << QStringLiteral("--instance") << Akonadi::Instance::identifier(); + } + + const bool ok = QProcess::startDetached(QStringLiteral("akonadi_control"), serverArgs); + if (!ok) { + akError() << "Error: unable to execute binary akonadi_control"; + return false; + } + + // safety timeout + QTimer::singleShot(5000, QCoreApplication::instance(), &QCoreApplication::quit); + // wait for the server to register with D-Bus + QCoreApplication::instance()->exec(); + + if (!mRegistered) { + akError() << "Error: akonadi_control was started but didn't register at D-Bus session bus."; + akError() << "Make sure your system is set up correctly!"; + return false; + } + + akDebug() << " done."; + return true; +} + +void AkonadiStarter::serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(name); + Q_UNUSED(oldOwner); + if (newOwner.isEmpty()) { + return; + } + + mRegistered = true; + QCoreApplication::instance()->quit(); +} diff --git a/src/akonadictl/akonadistarter.h b/src/akonadictl/akonadistarter.h new file mode 100644 index 0000000..541fe36 --- /dev/null +++ b/src/akonadictl/akonadistarter.h @@ -0,0 +1,39 @@ +/* + 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 AKONADISTARTER_H +#define AKONADISTARTER_H + +#include + +class AkonadiStarter : public QObject +{ + Q_OBJECT +public: + explicit AkonadiStarter(QObject *parent = 0); + bool start(); + +private Q_SLOTS: + void serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner); + +private: + bool mRegistered; +}; + +#endif diff --git a/src/akonadictl/main.cpp b/src/akonadictl/main.cpp new file mode 100644 index 0000000..e11e8e8 --- /dev/null +++ b/src/akonadictl/main.cpp @@ -0,0 +1,238 @@ +/*************************************************************************** + * Copyright (C) 2007 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "controlmanagerinterface.h" +#include "janitorinterface.h" +#include "akonadistarter.h" +#include + +#include +#include +#include + +#if defined(HAVE_UNISTD_H) && !defined(Q_WS_WIN) +#include +#else +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#include + +static bool startServer() +{ + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Control)) + || QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Server))) { + std::cerr << "Akonadi is already running." << std::endl; + return false; + } + AkonadiStarter starter; + return starter.start(); +} + +static bool stopServer() +{ + org::freedesktop::Akonadi::ControlManager iface(Akonadi::DBus::serviceName(Akonadi::DBus::Control), + QStringLiteral("/ControlManager"), + QDBusConnection::sessionBus(), 0); + if (!iface.isValid()) { + std::cerr << "Akonadi is not running." << std::endl; + return false; + } + + iface.shutdown(); + + return true; +} + +static bool checkAkonadiControlStatus() +{ + const bool registered = QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Control)); + std::cerr << "Akonadi Control: " << (registered ? "running" : "stopped") << std::endl; + return registered; +} + +static bool checkAkonadiServerStatus() +{ + const bool registered = QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Server)); + std::cerr << "Akonadi Server: " << (registered ? "running" : "stopped") << std::endl; + return registered; +} + +static bool checkSearchSupportStatus() +{ + QStringList searchMethods; + searchMethods << QStringLiteral("Remote Search"); + + const QString pluginOverride = QString::fromLatin1(qgetenv("AKONADI_OVERRIDE_SEARCHPLUGIN")); + if (!pluginOverride.isEmpty()) { + searchMethods << pluginOverride; + } else { + const QStringList dirs = Akonadi::XdgBaseDirs::findPluginDirs(); + Q_FOREACH (const QString &pluginDir, dirs) { + QDir dir(pluginDir + QLatin1String("/akonadi")); + const QStringList desktopFiles = dir.entryList(QStringList() << QStringLiteral("*.desktop"), QDir::Files); + Q_FOREACH (const QString &desktopFileName, desktopFiles) { + QSettings desktop(pluginDir + QLatin1String("/akonadi/") + desktopFileName, QSettings::IniFormat); + desktop.beginGroup(QStringLiteral("Desktop Entry")); + if (desktop.value(QStringLiteral("Type")).toString() != QLatin1String("AkonadiSearchPlugin")) { + continue; + } + if (!desktop.value(QStringLiteral("X-Akonadi-LoadByDefault"), true).toBool()) { + continue; + } + + searchMethods << desktop.value(QStringLiteral("Name")).toString(); + } + } + } + + // There's always at least server-search available + std::cerr << "Akonadi Server Search Support: available (" << searchMethods.join(QStringLiteral(", ")).toStdString() << ")" << std::endl; + return true; +} + +static bool checkAvailableAgentTypes() +{ + const QStringList dirs = Akonadi::XdgBaseDirs::findAllResourceDirs("data", QStringLiteral("akonadi/agents")); + QStringList types; + Q_FOREACH (const QString &pluginDir, dirs) { + QDir dir(pluginDir); + const QStringList plugins = dir.entryList(QStringList() << QStringLiteral("*.desktop"), QDir::Files); + Q_FOREACH (const QString &plugin, plugins) { + QSettings pluginInfo(pluginDir + QLatin1String("/") + plugin, QSettings::IniFormat); + pluginInfo.beginGroup(QStringLiteral("Desktop Entry")); + types << pluginInfo.value(QStringLiteral("X-Akonadi-Identifier")).toString(); + } + } + + // Remove duplicates from multiple pluginDirs + types.removeDuplicates(); + types.sort(); + + std::cerr << "Available Agent Types: "; + if (types.isEmpty()) { + std::cerr << "No agent types found!" << std::endl; + } else { + std::cerr << types.join(QStringLiteral(", ")).toStdString() << std::endl; + } + + return true; +} + +static bool statusServer() +{ + checkAkonadiControlStatus(); + checkAkonadiServerStatus(); + checkSearchSupportStatus(); + checkAvailableAgentTypes(); + return true; +} + +static void runJanitor(const QString &operation) +{ + org::freedesktop::Akonadi::Janitor janitor(Akonadi::DBus::serviceName(Akonadi::DBus::StorageJanitor), + QStringLiteral(AKONADI_DBUS_STORAGEJANITOR_PATH), + QDBusConnection::sessionBus()); + QObject::connect(&janitor, &org::freedesktop::Akonadi::Janitor::information, + [](const QString &msg) { + std::cerr << msg.toStdString() << std::endl; + }); + QObject::connect(&janitor, &org::freedesktop::Akonadi::Janitor::done, + []() { + qApp->exit(); + }); + janitor.asyncCall(operation); + qApp->exec(); +} + +int main(int argc, char **argv) +{ + AkCoreApplication app(argc, argv); + + app.setDescription(QStringLiteral("Akonadi server manipulation tool\n\n" + "Commands:\n" + " start Starts the Akonadi server with all its processes\n" + " stop Stops the Akonadi server and all its processes cleanly\n" + " restart Restart Akonadi server with all its processes\n" + " status Shows a status overview of the Akonadi server\n" + " vacuum Vacuum internal storage (WARNING: needs a lot of time and disk\n" + " space!)\n" + " fsck Check (and attempt to fix) consistency of the internal storage\n" + " (can take some time)")); + + + app.addPositionalCommandLineOption(QStringLiteral("command"), QStringLiteral("Command to execute"), + QStringLiteral("start|stop|restart|status|vacuum|fsck")); + + app.parseCommandLine(); + + const QStringList commands = app.commandLineArguments().positionalArguments(); + if (commands.size() != 1) { + app.printUsage(); + return -1; + } + + const QString command = commands[0]; + if (command == QLatin1String("start")) { + if (!startServer()) { + return 3; + } + } else if (command == QLatin1String("stop")) { + if (!stopServer()) { + return 4; + } + } else if (command == QLatin1String("status")) { + if (!statusServer()) { + return 5; + } + } else if (command == QLatin1String("restart")) { + if (!stopServer()) { + return 4; + } else { + do { +#if defined(HAVE_UNISTD_H) && !defined(Q_WS_WIN) + usleep(100000); +#else + Sleep(100000); +#endif + } while (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Control))); + if (!startServer()) { + return 3; + } + } + } else if (command == QLatin1String("vacuum")) { + runJanitor(QStringLiteral("vacuum")); + } else if (command == QLatin1String("fsck")) { + runJanitor(QStringLiteral("check")); + } + return 0; +} diff --git a/src/asapcat/CMakeLists.txt b/src/asapcat/CMakeLists.txt new file mode 100644 index 0000000..5fbd00b --- /dev/null +++ b/src/asapcat/CMakeLists.txt @@ -0,0 +1,17 @@ +set(asapcat_srcs + main.cpp + session.cpp +) + +add_executable(asapcat ${asapcat_srcs}) + +target_link_libraries(asapcat + akonadi_shared + KF5AkonadiPrivate + Qt5::Core + Qt5::Network +) + +install(TARGETS asapcat + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) diff --git a/src/asapcat/main.cpp b/src/asapcat/main.cpp new file mode 100644 index 0000000..c75e31a --- /dev/null +++ b/src/asapcat/main.cpp @@ -0,0 +1,50 @@ +/*************************************************************************** + * Copyright (C) 2013 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "session.h" + +#include + +#include +#include + +int main(int argc, char **argv) +{ + AkCoreApplication app(argc, argv); + app.setDescription(QStringLiteral("Akonadi ASAP cat\n" + "This is a development tool, only use this if you know what you are doing.")); + + app.addPositionalCommandLineOption(QStringLiteral("input"), QStringLiteral("Input file to read commands from")); + app.parseCommandLine(); + + + const QStringList args = app.commandLineArguments().positionalArguments(); + if (args.isEmpty()) { + app.printUsage(); + return -1; + } + + Session session(args[0]); + QObject::connect(&session, &Session::disconnected, QCoreApplication::instance(), &QCoreApplication::quit); + QMetaObject::invokeMethod(&session, "connectToHost", Qt::QueuedConnection); + + const int result = app.exec(); + session.printStats(); + return result; +} diff --git a/src/asapcat/session.cpp b/src/asapcat/session.cpp new file mode 100644 index 0000000..a5245b5 --- /dev/null +++ b/src/asapcat/session.cpp @@ -0,0 +1,153 @@ +/*************************************************************************** + * Copyright (C) 2013 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "session.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +Session::Session(const QString &input, QObject *parent) + : QObject(parent) + , m_input(0) + , m_session(0) + , m_notifier(0) + , m_receivedBytes(0) + , m_sentBytes(0) +{ + QFile *file = new QFile(this); + if (input != QLatin1String("-")) { + file->setFileName(input); + if (!file->open(QFile::ReadOnly)) { + akFatal() << "Failed to open" << input; + } + } else { + // ### does that work on Windows? + const int flags = fcntl(0, F_GETFL); + fcntl(0, F_SETFL, flags | O_NONBLOCK); + + if (!file->open(stdin, QFile::ReadOnly | QFile::Unbuffered)) { + akFatal() << "Failed to open stdin!"; + } + m_notifier = new QSocketNotifier(0, QSocketNotifier::Read, this); + connect(m_notifier, &QSocketNotifier::activated, this, &Session::inputAvailable); + } + m_input = file; +} + +Session::~Session() +{ +} + +void Session::connectToHost() +{ + const QSettings connectionSettings(Akonadi::StandardDirs::connectionConfigFile(), QSettings::IniFormat); + + QString serverAddress; +#ifdef Q_OS_WIN + serverAddress = connectionSettings.value(QStringLiteral("Data/NamedPipe"), QString()).toString(); +#else + serverAddress = connectionSettings.value(QStringLiteral("Data/UnixPath"), QString()).toString(); +#endif + if (serverAddress.isEmpty()) { + akFatal() << "Unable to determine server address."; + } + + QLocalSocket *socket = new QLocalSocket(this); + connect(socket, SIGNAL(error(QLocalSocket::LocalSocketError)), SLOT(serverError(QLocalSocket::LocalSocketError))); + connect(socket, &QLocalSocket::disconnected, this, &Session::serverDisconnected); + connect(socket, &QIODevice::readyRead, this, &Session::serverRead); + connect(socket, &QLocalSocket::connected, this, &Session::inputAvailable); + + m_session = socket; + socket->connectToServer(serverAddress); + + m_connectionTime.start(); +} + +void Session::inputAvailable() +{ + if (!m_session->isOpen()) { + return; + } + + if (m_notifier) { + m_notifier->setEnabled(false); + } + + if (m_input->atEnd()) { + return; + } + + QByteArray buffer(1024, Qt::Uninitialized); + qint64 readSize = 0; + + while ((readSize = m_input->read(buffer.data(), buffer.size())) > 0) { + m_session->write(buffer.constData(), readSize); + m_sentBytes += readSize; + } + + if (m_notifier) { + m_notifier->setEnabled(true); + } +} + +void Session::serverDisconnected() +{ + QCoreApplication::exit(0); +} + +void Session::serverError(QLocalSocket::LocalSocketError socketError) +{ + if (socketError == QLocalSocket::PeerClosedError) { + QCoreApplication::exit(0); + return; + } + + std::cerr << qPrintable(m_session->errorString()); + QCoreApplication::exit(1); +} + +void Session::serverRead() +{ + QByteArray buffer(1024, Qt::Uninitialized); + qint64 readSize = 0; + + while ((readSize = m_session->read(buffer.data(), buffer.size())) > 0) { + write(1, buffer.data(), readSize); + m_receivedBytes += readSize; + } +} + +void Session::printStats() const +{ + std::cerr << "Connection time: " << m_connectionTime.elapsed() << " ms" << std::endl; + std::cerr << "Sent: " << m_sentBytes << " bytes" << std::endl; + std::cerr << "Received: " << m_receivedBytes << " bytes" << std::endl; +} diff --git a/src/asapcat/session.h b/src/asapcat/session.h new file mode 100644 index 0000000..72d4ad9 --- /dev/null +++ b/src/asapcat/session.h @@ -0,0 +1,62 @@ +/*************************************************************************** + * Copyright (C) 2013 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef SESSION_H +#define SESSION_H + +#include +#include +#include + +class QIODevice; +class QSocketNotifier; + +/** ASAP CLI session. */ +class Session : public QObject +{ + Q_OBJECT +public: + explicit Session(const QString &input, QObject *parent = 0); + ~Session(); + + void printStats() const; + +public Q_SLOTS: + void connectToHost(); + +Q_SIGNALS: + void disconnected(); + +private Q_SLOTS: + void inputAvailable(); + void serverDisconnected(); + void serverError(QLocalSocket::LocalSocketError socketError); + void serverRead(); + +private: + QIODevice *m_input; + QIODevice *m_session; + QSocketNotifier *m_notifier; + + QTime m_connectionTime; + qint64 m_receivedBytes; + qint64 m_sentBytes; +}; + +#endif // SESSION_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..e2378ba --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,352 @@ + +set(akonadicore_base_SRCS + agentinstance.cpp + agentmanager.cpp + agenttype.cpp + asyncselectionhandler.cpp + attribute.cpp + attributefactory.cpp + cachepolicy.cpp + changemediator_p.cpp + changenotificationdependenciesfactory.cpp + changerecorder.cpp + changerecorder_p.cpp + connectionthread.cpp + collection.cpp + collectionfetchscope.cpp + collectionpathresolver.cpp + collectionquotaattribute.cpp + collectionquotaattribute.cpp + collectionrightsattribute.cpp + collectionstatistics.cpp + collectionsync.cpp + conflicthandler.cpp + collectionidentificationattribute.cpp + control.cpp + entityannotationsattribute.cpp + entitycache.cpp + entitydeletedattribute.cpp + entitydeletedattribute.cpp + entitydisplayattribute.cpp + entityhiddenattribute.cpp + exception.cpp + firstrun.cpp + gidextractor.cpp + indexpolicyattribute.cpp + item.cpp + itemchangelog.cpp + itemfetchscope.cpp + itemmonitor.cpp + itemserializer.cpp + itemserializerplugin.cpp + itemsync.cpp + kdsignalblocker.cpp + mimetypechecker.cpp + monitor.cpp + monitor_p.cpp + newmailnotifierattribute.cpp + notificationbus_p.cpp + notificationsource_p.cpp + partfetcher.cpp + pastehelper.cpp + persistentsearchattribute.cpp + pluginloader.cpp + pop3resourceattribute.cpp + protocolhelper.cpp + relation.cpp + relationsync.cpp + searchquery.cpp + servermanager.cpp + session.cpp + specialcollectionattribute.cpp + specialcollections.cpp + tag.cpp + tagattribute.cpp + tagfetchscope.cpp + tagsync.cpp + trashsettings.cpp + typepluginloader.cpp +) + +ecm_generate_headers(AkonadiCore_base_HEADERS + HEADER_NAMES + AbstractDifferencesReporter + AgentInstance + AgentManager + AgentType + Attribute + AttributeFactory + CachePolicy + ChangeRecorder + Collection + CollectionFetchScope + CollectionQuotaAttribute + CollectionStatistics + CollectionUtils + CollectionIdentificationAttribute + Control + DifferencesAlgorithmInterface + EntityAnnotationsAttribute + EntityDeletedAttribute + EntityDisplayAttribute + EntityHiddenAttribute + Exception + GidExtractorInterface + IndexPolicyAttribute + Item + ItemFetchScope + ItemMonitor + ItemSerializerPlugin + ItemSync + KDSignalBlocker + MimeTypeChecker + NewMailNotifierAttribute + Monitor + PartFetcher + PersistentSearchAttribute + Pop3ResourceAttribute + Relation + SearchQuery + ServerManager + Session + SpecialCollections + SpecialCollectionAttribute + Supertrait + Tag + TagAttribute + TagFetchScope + TrashSettings + CollectionPathResolver + VectorHelper + REQUIRED_HEADERS AkonadiCore_base_HEADERS +) + +set(akonadicore_models_SRCS + models/agentfilterproxymodel.cpp + models/agentinstancemodel.cpp + models/agenttypemodel.cpp + models/collectionfilterproxymodel.cpp + models/collectionmodel.cpp + models/collectionmodel_p.cpp + models/entitymimetypefiltermodel.cpp + models/entityorderproxymodel.cpp + models/entityrightsfiltermodel.cpp + models/entitytreemodel.cpp + models/entitytreemodel_p.cpp + models/favoritecollectionsmodel.cpp + models/itemmodel.cpp + models/quotacolorproxymodel.cpp + models/recursivecollectionfilterproxymodel.cpp + models/selectionproxymodel.cpp + models/statisticsproxymodel.cpp + models/subscriptionmodel.cpp + models/tagmodel.cpp + models/tagmodel_p.cpp + models/trashfilterproxymodel.cpp +) + +ecm_generate_headers(AkonadiCore_models_HEADERS + HEADER_NAMES + AgentFilterProxyModel + AgentInstanceModel + AgentTypeModel + CollectionFilterProxyModel + EntityMimeTypeFilterModel + EntityOrderProxyModel + EntityRightsFilterModel + EntityTreeModel + FavoriteCollectionsModel + ItemModel + QuotaColorProxyModel + RecursiveCollectionFilterProxyModel + SelectionProxyModel + StatisticsProxyModel + TagModel + TrashFilterProxyModel + REQUIRED_HEADERS AkonadiCore_models_HEADERS + RELATIVE models +) + +set(akonadicore_jobs_SRCS + jobs/agentinstancecreatejob.cpp + jobs/collectionattributessynchronizationjob.cpp + jobs/collectioncopyjob.cpp + jobs/collectioncreatejob.cpp + jobs/collectiondeletejob.cpp + jobs/collectionfetchjob.cpp + jobs/collectionmodifyjob.cpp + jobs/collectionmovejob.cpp + jobs/collectionstatisticsjob.cpp + jobs/invalidatecachejob.cpp + jobs/itemcopyjob.cpp + jobs/itemcreatejob.cpp + jobs/itemdeletejob.cpp + jobs/itemfetchjob.cpp + jobs/itemmodifyjob.cpp + jobs/itemmovejob.cpp + jobs/itemsearchjob.cpp + jobs/job.cpp + jobs/kjobprivatebase.cpp + jobs/linkjob.cpp + jobs/recursiveitemfetchjob.cpp + jobs/resourceselectjob.cpp + jobs/resourcesynchronizationjob.cpp + jobs/relationfetchjob.cpp + jobs/relationcreatejob.cpp + jobs/relationdeletejob.cpp + jobs/searchcreatejob.cpp + jobs/searchresultjob.cpp + jobs/specialcollectionsdiscoveryjob.cpp + jobs/specialcollectionshelperjobs.cpp + jobs/specialcollectionsrequestjob.cpp + jobs/subscriptionjob.cpp + jobs/tagcreatejob.cpp + jobs/tagdeletejob.cpp + jobs/tagfetchjob.cpp + jobs/tagmodifyjob.cpp + jobs/transactionjobs.cpp + jobs/transactionsequence.cpp + jobs/trashjob.cpp + jobs/trashrestorejob.cpp + jobs/unlinkjob.cpp +) + +ecm_generate_headers(AkonadiCore_jobs_HEADERS + HEADER_NAMES + AgentInstanceCreateJob + CollectionAttributesSynchronizationJob + CollectionCopyJob + CollectionCreateJob + CollectionDeleteJob + CollectionFetchJob + CollectionModifyJob + CollectionMoveJob + CollectionStatisticsJob + ItemCopyJob + ItemCreateJob + ItemDeleteJob + ItemFetchJob + ItemModifyJob + ItemMoveJob + ItemSearchJob + Job + LinkJob + RecursiveItemFetchJob + ResourceSynchronizationJob + RelationFetchJob + RelationCreateJob + RelationDeleteJob + SearchCreateJob + SpecialCollectionsDiscoveryJob + SpecialCollectionsRequestJob + TagCreateJob + TagDeleteJob + TagFetchJob + TagModifyJob + TransactionJobs + TransactionSequence + TrashJob + TrashRestoreJob + UnlinkJob + REQUIRED_HEADERS AkonadiCore_jobs_HEADERS + RELATIVE jobs +) + +# This is a workaround for conflict between our "Exception" fancy header and +# C++ stdlib's "exception" header which occurs in case-insensitive systems. +# For that reason we generate std_exception.h file, which contains an absolute +# path to the stdlib's exception header file, which resolves the ambiguity +# when including from within Akonadi. +include(FindStdlibInclude) +findStdlibInclude("exception" std_exception_file) +if (NOT "${std_exception_file}" STREQUAL "") + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/std_exception.h.in + ${CMAKE_CURRENT_BINARY_DIR}/std_exception.h + ) +endif() + +set(akonadicore_dbus_xml ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml) +qt5_add_dbus_interface(akonadicore_dbus_SRCS ${akonadicore_dbus_xml} notificationmanagerinterface) + +set(akonadicore_dbus_xml ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml) +set_source_files_properties(${akonadicore_dbus_xml} PROPERTIES INCLUDE "${Akonadi_SOURCE_DIR}/src/private/protocol_p.h" ) +qt5_add_dbus_interface(akonadicore_dbus_SRCS ${akonadicore_dbus_xml} notificationsourceinterface) + +qt5_add_dbus_interfaces(akonadicore_dbus_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml) +qt5_add_dbus_interfaces(akonadicore_dbus_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Tracer.xml) +qt5_add_dbus_interfaces(akonadicore_dbus_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml) + +set(akonadicore_SRCS + ${akonadicore_base_SRCS} + ${akonadicore_jobs_SRCS} + ${akonadicore_models_SRCS} + ${akonadicore_dbus_SRCS} +) + +ecm_qt_declare_logging_category(akonadicore_SRCS HEADER akonadicore_debug.h IDENTIFIER AKONADICORE_LOG CATEGORY_NAME akonadicore_log) + +add_library(KF5AkonadiCore ${akonadicore_SRCS}) + +generate_export_header(KF5AkonadiCore BASE_NAME akonadicore) + +add_library(KF5::AkonadiCore ALIAS KF5AkonadiCore) +target_include_directories(KF5AkonadiCore INTERFACE "$") +target_include_directories(KF5AkonadiCore PUBLIC "$") +target_include_directories(KF5AkonadiCore PUBLIC "$") +target_include_directories(KF5AkonadiCore PUBLIC "$") + +kde_target_enable_exceptions(KF5AkonadiCore PUBLIC) + +target_link_libraries(KF5AkonadiCore +PUBLIC + KF5::CoreAddons # for KJob + KF5::ItemModels + Qt5::Gui # for QColor +PRIVATE + Qt5::Widgets + Qt5::Network + KF5::AkonadiPrivate + KF5::DBusAddons + KF5::I18n + KF5::IconThemes + KF5::ConfigCore + KF5AkonadiPrivate +) + +set_target_properties(KF5AkonadiCore PROPERTIES + VERSION ${AKONADI_VERSION_STRING} + SOVERSION ${AKONADI_SOVERSION} + EXPORT_NAME AkonadiCore +) + +ecm_generate_pri_file(BASE_NAME AkonadiCore + LIB_NAME KF5AkonadiCore + DEPS "ItemModels CoreAddons" FILENAME_VAR PRI_FILENAME +) + +install(TARGETS + KF5AkonadiCore + EXPORT KF5AkonadiTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadicore_export.h + ${CMAKE_CURRENT_BINARY_DIR}/std_exception.h + ${AkonadiCore_base_HEADERS} + ${AkonadiCore_models_HEADERS} + ${AkonadiCore_jobs_HEADERS} + ${AkonadiCore_HEADERS} + qtest_akonadi.h + itempayloadinternals_p.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/AkonadiCore COMPONENT Devel +) + +install(FILES + ${PRI_FILENAME} + DESTINATION ${ECM_MKSPECS_INSTALL_DIR} +) + +install( FILES + kcfg2dbus.xsl + DESTINATION ${KDE_INSTALL_DATADIR_KF5}/akonadi +) diff --git a/src/core/abstractdifferencesreporter.h b/src/core/abstractdifferencesreporter.h new file mode 100644 index 0000000..e4a2af0 --- /dev/null +++ b/src/core/abstractdifferencesreporter.h @@ -0,0 +1,144 @@ +/* + Copyright (c) 2010 KDAB + Author: 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 ABSTRACTDIFFERENCESREPORTER_P_H +#define ABSTRACTDIFFERENCESREPORTER_P_H + +namespace Akonadi +{ + +/** + * @short An interface to report differences between two arbitrary objects. + * + * This interface can be used to report differences between two arbitrary objects + * by describing a virtual table with three columns. The first column contains the name + * of the property that is compared, the second column the property value of the one + * object and the third column the property of the other object. + * + * The rows of this table can have different modes: + * @li NormalMode The left and right columns show the same property values. + * @li ConflictMode The left and right columns show conflicting property values. + * @li AdditionalLeftMode The left column contains a property value that is not available in the right column. + * @li AdditionalRightMode The right column contains a property value that is not available in the left column. + * + * Example: + * + * @code + * // add differences of a contact + * const KContacts::Addressee contact1 = ... + * const KContacts::Addressee contact2 = ... + * + * AbstractDifferencesReporter *reporter = ... + * reporter->setPropertyNameTitle( i18n( "Contact fields" ) ); + * reporter->setLeftPropertyValueTitle( i18n( "Changed Contact" ) ); + * reporter->setRightPropertyValueTitle( i18n( "Conflicting Contact" ) ); + * + * // check given name + * if ( contact1.givenName() != contact2.givenName() ) + * reporter->addProperty( AbstractDifferencesReporter::ConflictMode, i18n( "Given Name" ), + * contact1.givenName(), contact2.givenName() ); + * else + * reporter->addProperty( AbstractDifferencesReporter::NormalMode, i18n( "Given Name" ), + * contact1.givenName(), contact2.givenName() ); + * + * // check family name + * if ( contact1.familyName() != contact2.familyName() ) + * reporter->addProperty( AbstractDifferencesReporter::ConflictMode, i18n( "Family Name" ), + * contact1.givenName(), contact2.givenName() ); + * else + * reporter->addProperty( AbstractDifferencesReporter::NormalMode, i18n( "Family Name" ), + * contact1.givenName(), contact2.givenName() ); + * + * // check emails + * const QStringList leftEmails = contact1.emails(); + * const QStringList rightEmails = contact2.emails(); + * + * foreach ( const QString &leftEmail, leftEmails ) { + * if ( rightEmails.contains( leftEmail ) ) + * reporter->addProperty( AbstractDifferencesReporter::NormalMode, i18n( "Email" ), + * leftEmail, leftEmail ); + * else + * reporter->addProperty( AbstractDifferencesReporter::AdditionalLeftMode, i18n( "Email" ), + * leftEmail, QString() ); + * } + * + * foreach ( const QString &rightEmail, rightEmails ) { + * if ( !leftEmails.contains( rightEmail ) ) + * reporter->addProperty( AbstractDifferencesReporter::AdditionalRightMode, i18n( "Email" ), + * QString(), rightEmail ); + * } + * + * @endcode + * + * @author Tobias Koenig + * @since 4.6 + */ +class AbstractDifferencesReporter +{ +public: + /** + * Describes the property modes. + */ + enum Mode { + NormalMode, ///< The left and right column show the same property values. + ConflictMode, ///< The left and right column show conflicting property values. + AdditionalLeftMode, ///< The left column contains a property value that is not available in the right column. + AdditionalRightMode ///< The right column contains a property value that is not available in the left column. + }; + + /** + * Destroys the abstract differences reporter. + */ + virtual ~AbstractDifferencesReporter() + { + } + + /** + * Sets the @p title of the property name column. + */ + virtual void setPropertyNameTitle(const QString &title) = 0; + + /** + * Sets the @p title of the column that shows the property values + * of the left object. + */ + virtual void setLeftPropertyValueTitle(const QString &title) = 0; + + /** + * Sets the @p title of the column that shows the property values + * of the right object. + */ + virtual void setRightPropertyValueTitle(const QString &title) = 0; + + /** + * Adds a new property entry to the table. + * + * @param mode Describes the mode of the property. If mode is AdditionalLeftMode or AdditionalRightMode, rightValue resp. leftValue + * should be QString(). + * @param name The user visible name of the property. + * @param leftValue The user visible property value of the left object. + * @param rightValue The user visible property value of the right object. + */ + virtual void addProperty(Mode mode, const QString &name, const QString &leftValue, const QString &rightValue) = 0; +}; + +} + +#endif diff --git a/src/core/agentinstance.cpp b/src/core/agentinstance.cpp new file mode 100644 index 0000000..e4a753b --- /dev/null +++ b/src/core/agentinstance.cpp @@ -0,0 +1,177 @@ +/* + 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. +*/ + +#include "agentinstance.h" +#include "agentinstance_p.h" + +#include "agentmanager.h" +#include "agentmanager_p.h" +#include "servermanager.h" + +#include "akonadicore_debug.h" + +using namespace Akonadi; + +AgentInstance::AgentInstance() + : d(new Private) +{ +} + +AgentInstance::AgentInstance(const AgentInstance &other) + : d(other.d) +{ +} + +AgentInstance::~AgentInstance() +{ +} + +bool AgentInstance::isValid() const +{ + return !d->mIdentifier.isEmpty(); +} + +AgentType AgentInstance::type() const +{ + return d->mType; +} + +QString AgentInstance::identifier() const +{ + return d->mIdentifier; +} + +void AgentInstance::setName(const QString &name) +{ + AgentManager::self()->d->setName(*this, name); +} + +QString AgentInstance::name() const +{ + return d->mName; +} + +AgentInstance::Status AgentInstance::status() const +{ + switch (d->mStatus) { + case 0: + return Idle; + case 1: + return Running; + case 2: + default: + return Broken; + case 3: + return NotConfigured; + } +} + +QString AgentInstance::statusMessage() const +{ + return d->mStatusMessage; +} + +int AgentInstance::progress() const +{ + return d->mProgress; +} + +bool AgentInstance::isOnline() const +{ + return d->mIsOnline; +} + +void AgentInstance::setIsOnline(bool online) +{ + AgentManager::self()->d->setOnline(*this, online); +} + +void AgentInstance::configure(QWidget *parent) +{ + AgentManager::self()->d->configure(*this, parent); +} + +void AgentInstance::synchronize() +{ + AgentManager::self()->d->synchronize(*this); +} + +void AgentInstance::synchronizeCollectionTree() +{ + AgentManager::self()->d->synchronizeCollectionTree(*this); +} + +AgentInstance &AgentInstance::operator=(const AgentInstance &other) +{ + if (this != &other) { + d = other.d; + } + + return *this; +} + +bool AgentInstance::operator==(const AgentInstance &other) const +{ + return (d->mIdentifier == other.d->mIdentifier); +} + +void AgentInstance::abortCurrentTask() const +{ + QDBusInterface iface(ServerManager::agentServiceName(ServerManager::Agent, identifier()), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.Akonadi.Agent.Control")); + if (iface.isValid()) { + QDBusReply reply = iface.call(QStringLiteral("abort")); + if (!reply.isValid()) { + qCWarning(AKONADICORE_LOG) << "Failed to place D-Bus call."; + } + } else { + qCWarning(AKONADICORE_LOG) << "Unable to obtain agent interface"; + } +} + +void AgentInstance::reconfigure() const +{ + QDBusInterface iface(ServerManager::agentServiceName(ServerManager::Agent, identifier()), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.Akonadi.Agent.Control")); + if (iface.isValid()) { + QDBusReply reply = iface.call(QStringLiteral("reconfigure")); + if (!reply.isValid()) { + qCWarning(AKONADICORE_LOG) << "Failed to place D-Bus call."; + } + } else { + qCWarning(AKONADICORE_LOG) << "Unable to obtain agent interface"; + } +} + +void Akonadi::AgentInstance::restart() const +{ + QDBusInterface iface(ServerManager::serviceName(Akonadi::ServerManager::Control), + QStringLiteral("/AgentManager"), + QStringLiteral("org.freedesktop.Akonadi.AgentManager")); + if (iface.isValid()) { + QDBusReply reply = iface.call(QStringLiteral("restartAgentInstance"), identifier()); + if (!reply.isValid()) { + qCWarning(AKONADICORE_LOG) << "Failed to place D-Bus call."; + } + } else { + qCWarning(AKONADICORE_LOG) << "Unable to obtain control interface" << iface.lastError().message(); + } +} diff --git a/src/core/agentinstance.h b/src/core/agentinstance.h new file mode 100644 index 0000000..afe25a9 --- /dev/null +++ b/src/core/agentinstance.h @@ -0,0 +1,206 @@ +/* + 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_AGENTINSTANCE_H +#define AKONADI_AGENTINSTANCE_H + +#include "akonadicore_export.h" + +#include +#include +#include + +class QString; +class QWidget; + +namespace Akonadi +{ + +class AgentType; + +/** + * @short A representation of an agent instance. + * + * The agent instance is a representation of a running agent process. + * It provides information about the instance and a reference to the + * AgentType of that instance. + * + * All available agent instances can be retrieved from the AgentManager. + * + * @code + * + * Akonadi::AgentInstance::List instances = Akonadi::AgentManager::self()->instances(); + * foreach ( const Akonadi::AgentInstance &instance, instances ) { + * qDebug() << "Name:" << instance.name() << "(" << instance.identifier() << ")"; + * } + * + * @endcode + * + * @note To find the collections belonging to an AgentInstance, use + * CollectionFetchJob and supply AgentInstance::identifier() as the parameter + * to CollectionFetchScope::setResource(). + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentInstance +{ + friend class AgentManager; + friend class AgentManagerPrivate; + +public: + /** + * Describes a list of agent instances. + */ + typedef QVector List; + + /** + * Describes the status of the agent instance. + */ + enum Status { + Idle = 0, ///< The agent instance does currently nothing. + Running, ///< The agent instance is working on something. + Broken, ///< The agent instance encountered an error state. + NotConfigured ///< The agent is lacking required configuration + }; + + /** + * Creates a new agent instance object. + */ + AgentInstance(); + + /** + * Creates an agent instance from an @p other agent instance. + */ + AgentInstance(const AgentInstance &other); + + /** + * Destroys the agent instance object. + */ + ~AgentInstance(); + + /** + * Returns whether the agent instance object is valid. + */ + bool isValid() const; + + /** + * Returns the agent type of this instance. + */ + AgentType type() const; + + /** + * Returns the unique identifier of the agent instance. + */ + QString identifier() const; + + /** + * Returns the user visible name of the agent instance. + */ + QString name() const; + + /** + * Sets the user visible @p name of the agent instance. + */ + void setName(const QString &name); + + /** + * Returns the status of the agent instance. + */ + Status status() const; + + /** + * Returns a textual presentation of the status of the agent instance. + */ + QString statusMessage() const; + + /** + * Returns the progress of the agent instance in percent, or -1 if no + * progress information are available. + */ + int progress() const; + + /** + * Returns whether the agent instance is online currently. + */ + bool isOnline() const; + + /** + * Sets @p online status of the agent instance. + */ + void setIsOnline(bool online); + + /** + * Triggers the agent instance to show its configuration dialog. + * + * @param parent Parent window for the configuration dialog. + */ + void configure(QWidget *parent = Q_NULLPTR); + + /** + * Triggers the agent instance to start synchronization. + */ + void synchronize(); + + /** + * Triggers a synchronization of the collection tree by the given agent instance. + */ + void synchronizeCollectionTree(); + + /** + * @internal + * @param other other agent instance + */ + AgentInstance &operator=(const AgentInstance &other); + + /** + * @internal + * @param other other agent instance + */ + bool operator==(const AgentInstance &other) const; + + /** + * Tell the agent to abort its current operation. + * @since 4.4 + */ + void abortCurrentTask() const; + + /** + * Tell the agent that its configuration has been changed remotely via D-Bus + */ + void reconfigure() const; + + /** + * Restart the agent process. + */ + void restart() const; + +private: + //@cond PRIVATE + class Private; + QSharedDataPointer d; + //@endcond +}; + +} + +Q_DECLARE_TYPEINFO(Akonadi::AgentInstance, Q_MOVABLE_TYPE); + +Q_DECLARE_METATYPE(Akonadi::AgentInstance) + +#endif diff --git a/src/core/agentinstance_p.h b/src/core/agentinstance_p.h new file mode 100644 index 0000000..3410372 --- /dev/null +++ b/src/core/agentinstance_p.h @@ -0,0 +1,67 @@ +/* + 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_AGENTINSTANCE_P_H +#define AKONADI_AGENTINSTANCE_P_H + +#include "agenttype.h" + +#include +#include + +namespace Akonadi +{ + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentInstance::Private : public QSharedData +{ +public: + Private() + : mStatus(0) + , mProgress(0) + , mIsOnline(false) + { + } + + Private(const Private &other) + : QSharedData(other) + { + mType = other.mType; + mIdentifier = other.mIdentifier; + mName = other.mName; + mStatus = other.mStatus; + mStatusMessage = other.mStatusMessage; + mProgress = other.mProgress; + mIsOnline = other.mIsOnline; + } + + AgentType mType; + QString mIdentifier; + QString mName; + int mStatus; + QString mStatusMessage; + int mProgress; + bool mIsOnline; +}; + +} + +#endif diff --git a/src/core/agentmanager.cpp b/src/core/agentmanager.cpp new file mode 100644 index 0000000..d1d1b68 --- /dev/null +++ b/src/core/agentmanager.cpp @@ -0,0 +1,421 @@ +/* + Copyright (c) 2006-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. +*/ + +#include "agentmanager.h" +#include "agentmanager_p.h" +#include "vectorhelper.h" + +#include "agenttype_p.h" +#include "agentinstance_p.h" +#include "KDBusConnectionPool" +#include "servermanager.h" + +#include "collection.h" + +#include +#include + + +using namespace Akonadi; + +// @cond PRIVATE + +AgentInstance AgentManagerPrivate::createInstance(const AgentType &type) +{ + const QString &identifier = mManager->createAgentInstance(type.identifier()); + if (identifier.isEmpty()) { + return AgentInstance(); + } + + return fillAgentInstanceLight(identifier); +} + +void AgentManagerPrivate::agentTypeAdded(const QString &identifier) +{ + // Ignore agent types we already know about, for example because we called + // readAgentTypes before. + if (mTypes.contains(identifier)) { + return; + } + + if (mTypes.isEmpty()) { + // The Akonadi ServerManager assumes that the server is up and running as soon + // as it knows about at least one agent type. + // If we emit the typeAdded() signal here, it therefore thinks the server is + // running. However, the AgentManager does not know about all agent types yet, + // as the server might still have pending agentTypeAdded() signals, even though + // it internally knows all agent types already. + // This can cause situations where the client gets told by the ServerManager that + // the server is running, yet the client will encounter an error because the + // AgentManager doesn't know all types yet. + // + // Therefore, we read all agent types from the server here so they are known. + readAgentTypes(); + } + + const AgentType type = fillAgentType(identifier); + if (type.isValid()) { + mTypes.insert(identifier, type); + + emit mParent->typeAdded(type); + } +} + +void AgentManagerPrivate::agentTypeRemoved(const QString &identifier) +{ + if (!mTypes.contains(identifier)) { + return; + } + + const AgentType type = mTypes.take(identifier); + emit mParent->typeRemoved(type); +} + +void AgentManagerPrivate::agentInstanceAdded(const QString &identifier) +{ + const AgentInstance instance = fillAgentInstance(identifier); + if (instance.isValid()) { + + // It is possible that this function is called when the instance is already + // in our list we filled initially in the constructor. + // This happens when the constructor is called during Akonadi startup, when + // the agent processes are not fully loaded and have no D-Bus interface yet. + // The server-side agent manager then emits the instance added signal when + // the D-Bus interface for the agent comes up. + // In this case, we simply notify that the instance status has changed. + const bool newAgentInstance = !mInstances.contains(identifier); + if (newAgentInstance) { + mInstances.insert(identifier, instance); + emit mParent->instanceAdded(instance); + } else { + mInstances.remove(identifier); + mInstances.insert(identifier, instance); + emit mParent->instanceStatusChanged(instance); + } + } +} + +void AgentManagerPrivate::agentInstanceRemoved(const QString &identifier) +{ + if (!mInstances.contains(identifier)) { + return; + } + + const AgentInstance instance = mInstances.take(identifier); + emit mParent->instanceRemoved(instance); +} + +void AgentManagerPrivate::agentInstanceStatusChanged(const QString &identifier, int status, const QString &msg) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + instance.d->mStatus = status; + instance.d->mStatusMessage = msg; + + emit mParent->instanceStatusChanged(instance); +} + +void AgentManagerPrivate::agentInstanceProgressChanged(const QString &identifier, uint progress, const QString &msg) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + instance.d->mProgress = progress; + if (!msg.isEmpty()) { + instance.d->mStatusMessage = msg; + } + + emit mParent->instanceProgressChanged(instance); +} + +void AgentManagerPrivate::agentInstanceWarning(const QString &identifier, const QString &msg) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + emit mParent->instanceWarning(instance, msg); +} + +void AgentManagerPrivate::agentInstanceError(const QString &identifier, const QString &msg) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + emit mParent->instanceError(instance, msg); +} + +void AgentManagerPrivate::agentInstanceOnlineChanged(const QString &identifier, bool state) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + instance.d->mIsOnline = state; + emit mParent->instanceOnline(instance, state); +} + +void AgentManagerPrivate::agentInstanceNameChanged(const QString &identifier, const QString &name) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + instance.d->mName = name; + + emit mParent->instanceNameChanged(instance); +} + +void AgentManagerPrivate::readAgentTypes() +{ + const QDBusReply types = mManager->agentTypes(); + if (types.isValid()) { + foreach (const QString &type, types.value()) { + const AgentType agentType = fillAgentType(type); + if (agentType.isValid()) { + mTypes.insert(type, agentType); + emit mParent->typeAdded(agentType); + } + } + } +} + +void AgentManagerPrivate::readAgentInstances() +{ + const QDBusReply instances = mManager->agentInstances(); + if (instances.isValid()) { + foreach (const QString &instance, instances.value()) { + const AgentInstance agentInstance = fillAgentInstance(instance); + if (agentInstance.isValid()) { + mInstances.insert(instance, agentInstance); + emit mParent->instanceAdded(agentInstance); + } + } + } +} + +AgentType AgentManagerPrivate::fillAgentType(const QString &identifier) const +{ + AgentType type; + type.d->mIdentifier = identifier; + type.d->mName = mManager->agentName(identifier); + type.d->mDescription = mManager->agentComment(identifier); + type.d->mIconName = mManager->agentIcon(identifier); + type.d->mMimeTypes = mManager->agentMimeTypes(identifier); + type.d->mCapabilities = mManager->agentCapabilities(identifier); + type.d->mCustomProperties = mManager->agentCustomProperties(identifier); + + return type; +} + +void AgentManagerPrivate::setName(const AgentInstance &instance, const QString &name) +{ + mManager->setAgentInstanceName(instance.identifier(), name); +} + +void AgentManagerPrivate::setOnline(const AgentInstance &instance, bool state) +{ + mManager->setAgentInstanceOnline(instance.identifier(), state); +} + +void AgentManagerPrivate::configure(const AgentInstance &instance, QWidget *parent) +{ + qlonglong winId = 0; + if (parent) { + winId = (qlonglong)(parent->window()->winId()); + } + + mManager->agentInstanceConfigure(instance.identifier(), winId); +} + +void AgentManagerPrivate::synchronize(const AgentInstance &instance) +{ + mManager->agentInstanceSynchronize(instance.identifier()); +} + +void AgentManagerPrivate::synchronizeCollectionTree(const AgentInstance &instance) +{ + mManager->agentInstanceSynchronizeCollectionTree(instance.identifier()); +} + +AgentInstance AgentManagerPrivate::fillAgentInstance(const QString &identifier) const +{ + AgentInstance instance; + + const QString agentTypeIdentifier = mManager->agentInstanceType(identifier); + if (!mTypes.contains(agentTypeIdentifier)) { + return instance; + } + + instance.d->mType = mTypes.value(agentTypeIdentifier); + instance.d->mIdentifier = identifier; + instance.d->mName = mManager->agentInstanceName(identifier); + instance.d->mStatus = mManager->agentInstanceStatus(identifier); + instance.d->mStatusMessage = mManager->agentInstanceStatusMessage(identifier); + instance.d->mProgress = mManager->agentInstanceProgress(identifier); + instance.d->mIsOnline = mManager->agentInstanceOnline(identifier); + + return instance; +} + +AgentInstance AgentManagerPrivate::fillAgentInstanceLight(const QString &identifier) const +{ + AgentInstance instance; + + const QString agentTypeIdentifier = mManager->agentInstanceType(identifier); + Q_ASSERT_X(mTypes.contains(agentTypeIdentifier), "fillAgentInstanceLight", "Requests non-existing agent type"); + + instance.d->mType = mTypes.value(agentTypeIdentifier); + instance.d->mIdentifier = identifier; + + return instance; +} + +void AgentManagerPrivate::serviceOwnerChanged(const QString &, const QString &oldOwner, const QString &) +{ + if (oldOwner.isEmpty()) { + if (mTypes.isEmpty()) { // just to be safe + readAgentTypes(); + } + if (mInstances.isEmpty()) { + readAgentInstances(); + } + } +} + +void AgentManagerPrivate::createDBusInterface() +{ + mTypes.clear(); + mInstances.clear(); + delete mManager; + + mManager = new org::freedesktop::Akonadi::AgentManager(ServerManager::serviceName(ServerManager::Control), + QStringLiteral("/AgentManager"), + KDBusConnectionPool::threadConnection(), mParent); + + QObject::connect(mManager, SIGNAL(agentTypeAdded(QString)), + mParent, SLOT(agentTypeAdded(QString))); + QObject::connect(mManager, SIGNAL(agentTypeRemoved(QString)), + mParent, SLOT(agentTypeRemoved(QString))); + QObject::connect(mManager, SIGNAL(agentInstanceAdded(QString)), + mParent, SLOT(agentInstanceAdded(QString))); + QObject::connect(mManager, SIGNAL(agentInstanceRemoved(QString)), + mParent, SLOT(agentInstanceRemoved(QString))); + QObject::connect(mManager, SIGNAL(agentInstanceStatusChanged(QString,int,QString)), + mParent, SLOT(agentInstanceStatusChanged(QString,int,QString))); + QObject::connect(mManager, SIGNAL(agentInstanceProgressChanged(QString,uint,QString)), + mParent, SLOT(agentInstanceProgressChanged(QString,uint,QString))); + QObject::connect(mManager, SIGNAL(agentInstanceNameChanged(QString,QString)), + mParent, SLOT(agentInstanceNameChanged(QString,QString))); + QObject::connect(mManager, SIGNAL(agentInstanceWarning(QString,QString)), + mParent, SLOT(agentInstanceWarning(QString,QString))); + QObject::connect(mManager, SIGNAL(agentInstanceError(QString,QString)), + mParent, SLOT(agentInstanceError(QString,QString))); + QObject::connect(mManager, SIGNAL(agentInstanceOnlineChanged(QString,bool)), + mParent, SLOT(agentInstanceOnlineChanged(QString,bool))); + + if (mManager->isValid()) { + readAgentTypes(); + readAgentInstances(); + } +} + +AgentManager *AgentManagerPrivate::mSelf = Q_NULLPTR; + +AgentManager::AgentManager() + : QObject(0) + , d(new AgentManagerPrivate(this)) +{ + // needed for queued connections on our signals + qRegisterMetaType(); + qRegisterMetaType(); + + d->createDBusInterface(); + + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(ServerManager::serviceName(ServerManager::Control), + KDBusConnectionPool::threadConnection(), + QDBusServiceWatcher::WatchForOwnerChange, this); + connect(watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)), + this, SLOT(serviceOwnerChanged(QString,QString,QString))); +} + +// @endcond + +AgentManager::~AgentManager() +{ + delete d; +} + +AgentManager *AgentManager::self() +{ + if (!AgentManagerPrivate::mSelf) { + AgentManagerPrivate::mSelf = new AgentManager(); + } + + return AgentManagerPrivate::mSelf; +} + +AgentType::List AgentManager::types() const +{ + return Akonadi::valuesToVector(d->mTypes); +} + +AgentType AgentManager::type(const QString &identifier) const +{ + return d->mTypes.value(identifier); +} + +AgentInstance::List AgentManager::instances() const +{ + return Akonadi::valuesToVector(d->mInstances); +} + +AgentInstance AgentManager::instance(const QString &identifier) const +{ + return d->mInstances.value(identifier); +} + +void AgentManager::removeInstance(const AgentInstance &instance) +{ + d->mManager->removeAgentInstance(instance.identifier()); +} + +void AgentManager::synchronizeCollection(const Collection &collection) +{ + synchronizeCollection(collection, false); +} + +void AgentManager::synchronizeCollection(const Collection &collection, bool recursive) +{ + const QString resId = collection.resource(); + Q_ASSERT(!resId.isEmpty()); + d->mManager->agentInstanceSynchronizeCollection(resId, collection.id(), recursive); +} + +#include "moc_agentmanager.cpp" diff --git a/src/core/agentmanager.h b/src/core/agentmanager.h new file mode 100644 index 0000000..16a10ba --- /dev/null +++ b/src/core/agentmanager.h @@ -0,0 +1,222 @@ +/* + Copyright (c) 2006-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_AGENTMANAGER_H +#define AKONADI_AGENTMANAGER_H + +#include "akonadicore_export.h" + +#include "agenttype.h" +#include "agentinstance.h" + +#include + +namespace Akonadi +{ + +class AgentManagerPrivate; +class Collection; + +/** + * @short Provides an interface to retrieve agent types and manage agent instances. + * + * This singleton class can be used to create or remove agent instances or trigger + * synchronization of collections. Furthermore it provides information about status + * changes of the agents. + * + * @code + * + * Akonadi::AgentManager *manager = Akonadi::AgentManager::self(); + * + * Akonadi::AgentType::List types = manager->types(); + * foreach ( const Akonadi::AgentType& type, types ) { + * qDebug() << "Type:" << type.name() << type.description(); + * } + * + * @endcode + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentManager : public QObject +{ + friend class AgentInstance; + friend class AgentInstanceCreateJobPrivate; + friend class AgentManagerPrivate; + + Q_OBJECT + +public: + /** + * Returns the global instance of the agent manager. + */ + static AgentManager *self(); + + /** + * Destroys the agent manager. + */ + ~AgentManager(); + + /** + * Returns the list of all available agent types. + */ + AgentType::List types() const; + + /** + * Returns the agent type with the given @p identifier or + * an invalid agent type if the identifier does not exist. + */ + AgentType type(const QString &identifier) const; + + /** + * Returns the list of all available agent instances. + */ + AgentInstance::List instances() const; + + /** + * Returns the agent instance with the given @p identifier or + * an invalid agent instance if the identifier does not exist. + * + * Note that because a resource is a special case of an agent, the + * identifier of a resource is the same as that of its agent instance. + * @param identifier identifier to choose the agent instance + */ + AgentInstance instance(const QString &identifier) const; + + /** + * Removes the given agent @p instance. + */ + void removeInstance(const AgentInstance &instance); + + /** + * Trigger a synchronization of the given collection by its owning resource agent. + * + * @param collection The collection to synchronize. + */ + void synchronizeCollection(const Collection &collection); + + /** + * Trigger a synchronization of the given collection by its owning resource agent. + * + * @param collection The collection to synchronize. + * @param recursive If true, the sub-collections are also synchronized + * + * @since 4.6 + */ + void synchronizeCollection(const Collection &collection, bool recursive); + +Q_SIGNALS: + /** + * This signal is emitted whenever a new agent type was installed on the system. + * + * @param type The new agent type. + */ + void typeAdded(const Akonadi::AgentType &type); + + /** + * This signal is emitted whenever an agent type was removed from the system. + * + * @param type The removed agent type. + */ + void typeRemoved(const Akonadi::AgentType &type); + + /** + * This signal is emitted whenever a new agent instance was created. + * + * @param instance The new agent instance. + */ + void instanceAdded(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever an agent instance was removed. + * + * @param instance The removed agent instance. + */ + void instanceRemoved(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever the status of an agent instance has + * changed. + * + * @param instance The agent instance that status has changed. + */ + void instanceStatusChanged(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever the progress of an agent instance has + * changed. + * + * @param instance The agent instance that progress has changed. + */ + void instanceProgressChanged(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever the name of the agent instance has changed. + * + * @param instance The agent instance that name has changed. + */ + void instanceNameChanged(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever the agent instance raised an error. + * + * @param instance The agent instance that raised the error. + * @param message The i18n'ed error message. + */ + void instanceError(const Akonadi::AgentInstance &instance, const QString &message); + + /** + * This signal is emitted whenever the agent instance raised a warning. + * + * @param instance The agent instance that raised the warning. + * @param message The i18n'ed warning message. + */ + void instanceWarning(const Akonadi::AgentInstance &instance, const QString &message); + + /** + * This signal is emitted whenever the online state of an agent changed. + * + * @param instance The agent instance that changed its online state. + * @param online The new online state. + * @since 4.2 + */ + void instanceOnline(const Akonadi::AgentInstance &instance, bool online); + +private: + //@cond PRIVATE + AgentManager(); + + AgentManagerPrivate *const d; + + Q_PRIVATE_SLOT(d, void agentTypeAdded(const QString &)) + Q_PRIVATE_SLOT(d, void agentTypeRemoved(const QString &)) + Q_PRIVATE_SLOT(d, void agentInstanceAdded(const QString &)) + Q_PRIVATE_SLOT(d, void agentInstanceRemoved(const QString &)) + Q_PRIVATE_SLOT(d, void agentInstanceStatusChanged(const QString &, int, const QString &)) + Q_PRIVATE_SLOT(d, void agentInstanceProgressChanged(const QString &, uint, const QString &)) + Q_PRIVATE_SLOT(d, void agentInstanceNameChanged(const QString &, const QString &)) + Q_PRIVATE_SLOT(d, void agentInstanceWarning(const QString &, const QString &)) + Q_PRIVATE_SLOT(d, void agentInstanceError(const QString &, const QString &)) + Q_PRIVATE_SLOT(d, void agentInstanceOnlineChanged(const QString &, bool)) + Q_PRIVATE_SLOT(d, void serviceOwnerChanged(const QString &, const QString &, const QString &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/agentmanager_p.h b/src/core/agentmanager_p.h new file mode 100644 index 0000000..2ed3c68 --- /dev/null +++ b/src/core/agentmanager_p.h @@ -0,0 +1,105 @@ +/* + Copyright (c) 2006-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_AGENTMANAGER_P_H +#define AKONADI_AGENTMANAGER_P_H + +#include "agentmanagerinterface.h" + +#include "agenttype.h" +#include "agentinstance.h" + +#include + +namespace Akonadi +{ + +class AgentManager; + +/** + * @internal + */ +class AgentManagerPrivate +{ + friend class AgentManager; + +public: + AgentManagerPrivate(AgentManager *parent) + : mParent(parent) + , mManager(0) + { + } + + /* + * Used by AgentInstanceCreateJob + */ + AgentInstance createInstance(const AgentType &type); + + void agentTypeAdded(const QString &identifier); + void agentTypeRemoved(const QString &identifier); + void agentInstanceAdded(const QString &identifier); + void agentInstanceRemoved(const QString &identifier); + void agentInstanceStatusChanged(const QString &identifier, int status, const QString &msg); + void agentInstanceProgressChanged(const QString &identifier, uint progress, const QString &msg); + void agentInstanceNameChanged(const QString &identifier, const QString &name); + void agentInstanceWarning(const QString &identifier, const QString &msg); + void agentInstanceError(const QString &identifier, const QString &msg); + void agentInstanceOnlineChanged(const QString &identifier, bool state); + + /** + * Reads the information about all known agent types from the serverside + * agent manager and updates mTypes, like agentTypeAdded() does. + * + * This will not remove agents from the internal map that are no longer on + * the server. + */ + void readAgentTypes(); + + /** + * Reads the information about all known agent instances from the server. If AgentManager + * is created before the Akonadi.Control interface is registered, the agent + * instances aren't immediately found then. + */ + void readAgentInstances(); + + void setName(const AgentInstance &instance, const QString &name); + void setOnline(const AgentInstance &instance, bool state); + void configure(const AgentInstance &instance, QWidget *parent); + void synchronize(const AgentInstance &instance); + void synchronizeCollectionTree(const AgentInstance &instance); + + void serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner); + void createDBusInterface(); + + AgentType fillAgentType(const QString &identifier) const; + AgentInstance fillAgentInstance(const QString &identifier) const; + AgentInstance fillAgentInstanceLight(const QString &identifier) const; + + static AgentManager *mSelf; + + AgentManager *mParent; + org::freedesktop::Akonadi::AgentManager *mManager; + + QHash mTypes; + QHash mInstances; +}; + +} + +#endif diff --git a/src/core/agenttype.cpp b/src/core/agenttype.cpp new file mode 100644 index 0000000..b00b0ba --- /dev/null +++ b/src/core/agenttype.cpp @@ -0,0 +1,98 @@ +/* + 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. +*/ + +#include "agenttype.h" +#include "agenttype_p.h" + +#include + +using namespace Akonadi; + +AgentType::AgentType() + : d(new Private) +{ +} + +AgentType::AgentType(const AgentType &other) + : d(other.d) +{ +} + +AgentType::~AgentType() +{ +} + +bool AgentType::isValid() const +{ + return !d->mIdentifier.isEmpty(); +} + +QString AgentType::identifier() const +{ + return d->mIdentifier; +} + +QString AgentType::name() const +{ + return d->mName; +} + +QString AgentType::description() const +{ + return d->mDescription; +} + +QString AgentType::iconName() const +{ + return d->mIconName; +} + +QIcon AgentType::icon() const +{ + return QIcon::fromTheme(d->mIconName); +} + +QStringList AgentType::mimeTypes() const +{ + return d->mMimeTypes; +} + +QStringList AgentType::capabilities() const +{ + return d->mCapabilities; +} + +QVariantMap AgentType::customProperties() const +{ + return d->mCustomProperties; +} + +AgentType &AgentType::operator=(const AgentType &other) +{ + if (this != &other) { + d = other.d; + } + + return *this; +} + +bool AgentType::operator==(const AgentType &other) const +{ + return (d->mIdentifier == other.d->mIdentifier); +} diff --git a/src/core/agenttype.h b/src/core/agenttype.h new file mode 100644 index 0000000..893a9a1 --- /dev/null +++ b/src/core/agenttype.h @@ -0,0 +1,155 @@ +/* + 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_AGENTTYPE_H +#define AKONADI_AGENTTYPE_H + +#include "akonadicore_export.h" + +#include +#include +#include + +class QIcon; +class QString; +class QStringList; +class QVariant; +typedef QMap QVariantMap; + +namespace Akonadi +{ + +/** + * @short A representation of an agent type. + * + * The agent type is a representation of an available agent, that can + * be started as an agent instance. + * It provides all information about the type. + * + * All available agent types can be retrieved from the AgentManager. + * + * @code + * + * Akonadi::AgentType::List types = Akonadi::AgentManager::self()->types(); + * foreach ( const Akonadi::AgentType &type, types ) { + * qDebug() << "Name:" << type.name() << "(" << type.identifier() << ")"; + * } + * + * @endcode + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentType +{ + friend class AgentManager; + friend class AgentManagerPrivate; + +public: + /** + * Describes a list of agent types. + */ + typedef QVector List; + + /** + * Creates a new agent type. + */ + AgentType(); + + /** + * Creates an agent type from an @p other agent type. + */ + AgentType(const AgentType &other); + + /** + * Destroys the agent type. + */ + ~AgentType(); + + /** + * Returns whether the agent type is valid. + */ + bool isValid() const; + + /** + * Returns the unique identifier of the agent type. + */ + QString identifier() const; + + /** + * Returns the i18n'ed name of the agent type. + */ + QString name() const; + + /** + * Returns the description of the agent type. + */ + QString description() const; + + /** + * Returns the name of the icon of the agent type. + */ + QString iconName() const; + + /** + * Returns the icon of the agent type. + */ + QIcon icon() const; + + /** + * Returns the list of supported mime types of the agent type. + */ + QStringList mimeTypes() const; + + /** + * Returns the list of supported capabilities of the agent type. + */ + QStringList capabilities() const; + + /** + * Returns a Map of custom properties of the agent type. + * @since 4.12 + */ + QVariantMap customProperties() const; + + /** + * @internal + * @param other other agent type + */ + AgentType &operator=(const AgentType &other); + + /** + * @internal + * @param other other agent type + */ + bool operator==(const AgentType &other) const; + +private: + //@cond PRIVATE + class Private; + QSharedDataPointer d; + //@endcond +}; + +} + +Q_DECLARE_TYPEINFO(Akonadi::AgentType, Q_MOVABLE_TYPE); + +Q_DECLARE_METATYPE(Akonadi::AgentType) + +#endif diff --git a/src/core/agenttype_p.h b/src/core/agenttype_p.h new file mode 100644 index 0000000..d4d8da0 --- /dev/null +++ b/src/core/agenttype_p.h @@ -0,0 +1,63 @@ +/* + 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_AGENTTYPE_P_H +#define AKONADI_AGENTTYPE_P_H + +#include +#include +#include + +namespace Akonadi +{ + +/** + * @internal + */ +class AgentType::Private : public QSharedData +{ +public: + Private() + { + } + + Private(const Private &other) + : QSharedData(other) + { + mIdentifier = other.mIdentifier; + mName = other.mName; + mDescription = other.mDescription; + mIconName = other.mIconName; + mMimeTypes = other.mMimeTypes; + mCapabilities = other.mCapabilities; + mCustomProperties = other.mCustomProperties; + } + + QString mIdentifier; + QString mName; + QString mDescription; + QString mIconName; + QStringList mMimeTypes; + QStringList mCapabilities; + QVariantMap mCustomProperties; +}; + +} + +#endif diff --git a/src/core/asyncselectionhandler.cpp b/src/core/asyncselectionhandler.cpp new file mode 100644 index 0000000..528a1c7 --- /dev/null +++ b/src/core/asyncselectionhandler.cpp @@ -0,0 +1,95 @@ +/* + Copyright (c) 2009 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. +*/ + +#include "asyncselectionhandler_p.h" +#include "models/entitytreemodel.h" +#include "akonadicore_debug.h" + +using namespace Akonadi; + +AsyncSelectionHandler::AsyncSelectionHandler(QAbstractItemModel *model, QObject *parent) + : QObject(parent) + , mModel(model) +{ + Q_ASSERT(mModel); + + connect(mModel, &QAbstractItemModel::rowsInserted, this, &AsyncSelectionHandler::rowsInserted); +} + +AsyncSelectionHandler::~AsyncSelectionHandler() +{ +} + +bool AsyncSelectionHandler::scanSubTree(const QModelIndex &index, bool searchForItem) +{ + if (searchForItem) { + const Item::Id id = index.data(EntityTreeModel::ItemIdRole).toLongLong(); + + if (mItem.id() == id) { + emit itemAvailable(index); + return true; + } + } else { + const Collection::Id id = index.data(EntityTreeModel::CollectionIdRole).toLongLong(); + + if (mCollection.id() == id) { + emit collectionAvailable(index); + return true; + } + } + + for (int row = 0; row < mModel->rowCount(index); ++row) { + const QModelIndex childIndex = mModel->index(row, 0, index); + //This should not normally happen, but if it does we end up in an endless loop + if (!childIndex.isValid()) { + qCWarning(AKONADICORE_LOG) << "Invalid child detected: " << index.data().toString(); + Q_ASSERT(false); + return false; + } + if (scanSubTree(childIndex, searchForItem)) { + return true; + } + } + + return false; +} + +void AsyncSelectionHandler::waitForCollection(const Collection &collection) +{ + mCollection = collection; + + scanSubTree(QModelIndex(), false); +} + +void AsyncSelectionHandler::waitForItem(const Item &item) +{ + mItem = item; + + scanSubTree(QModelIndex(), true); +} + +void AsyncSelectionHandler::rowsInserted(const QModelIndex &parent, int start, int end) +{ + for (int i = start; i <= end; ++i) { + scanSubTree(mModel->index(i, 0, parent), false); + scanSubTree(mModel->index(i, 0, parent), true); + } +} + +#include "moc_asyncselectionhandler_p.cpp" diff --git a/src/core/asyncselectionhandler_p.h b/src/core/asyncselectionhandler_p.h new file mode 100644 index 0000000..a3dfc58 --- /dev/null +++ b/src/core/asyncselectionhandler_p.h @@ -0,0 +1,74 @@ +/* + Copyright (c) 2009 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_ASYNCSELECTIONHANDLER_P_H +#define AKONADI_ASYNCSELECTIONHANDLER_P_H + +#include + +#include "akonadicore_export.h" +#include "collection.h" +#include "item.h" + +class QAbstractItemModel; +class QModelIndex; + +namespace Akonadi +{ + +/** + * @internal + * + * A helper class to set a current index on a widget with + * delayed model loading. + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AsyncSelectionHandler : public QObject +{ + Q_OBJECT + +public: + /** + */ + explicit AsyncSelectionHandler(QAbstractItemModel *model, QObject *parent = 0); + + ~AsyncSelectionHandler(); + + void waitForCollection(const Collection &collection); + void waitForItem(const Item &item); + +Q_SIGNALS: + void collectionAvailable(const QModelIndex &index); + void itemAvailable(const QModelIndex &index); + +private Q_SLOTS: + void rowsInserted(const QModelIndex &parent, int start, int end); + +private: + bool scanSubTree(const QModelIndex &index, bool searchForItem); + + QAbstractItemModel *mModel; + Collection mCollection; + Item mItem; +}; + +} + +#endif diff --git a/src/core/attribute.cpp b/src/core/attribute.cpp new file mode 100644 index 0000000..c829170 --- /dev/null +++ b/src/core/attribute.cpp @@ -0,0 +1,26 @@ +/* + Copyright (c) 2006 - 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 "attribute.h" + +using namespace Akonadi; + +Attribute::~ Attribute() +{ +} diff --git a/src/core/attribute.h b/src/core/attribute.h new file mode 100644 index 0000000..831eb25 --- /dev/null +++ b/src/core/attribute.h @@ -0,0 +1,179 @@ +/* + Copyright (c) 2006 - 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_ATTRIBUTE_H +#define AKONADI_ATTRIBUTE_H + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ + +/** + * @short Provides interface for custom attributes for Entity. + * + * This class is an interface for custom attributes, that can be stored + * in an entity. Attributes should be meta data, e.g. ACLs, quotas etc. + * that are not part of the entities' data itself. + * + * Note that attributes are per user, i.e. when an attribute is added to + * an entity, it only applies to the current user. + * + * To provide custom attributes, you have to subclass from this interface + * and reimplement the pure virtual methods. + * + * @code + * + * class SecrecyAttribute : public Akonadi::Attribute + * { + * public: + * enum Secrecy + * { + * Public, + * Private, + * Confidential + * }; + * + * SecrecyAttribute( Secrecy secrecy = Public ) + * : mSecrecy( secrecy ) + * { + * } + * + * void setSecrecy( Secrecy secrecy ) + * { + * mSecrecy = secrecy; + * } + * + * Secrecy secrecy() const + * { + * return mSecrecy; + * } + * + * virtual QByteArray type() const + * { + * return "secrecy"; + * } + * + * virtual Attribute* clone() const + * { + * return new SecrecyAttribute( mSecrecy ); + * } + * + * virtual QByteArray serialized() const + * { + * switch ( mSecrecy ) { + * case Public: return "public"; break; + * case Private: return "private"; break; + * case Confidential: return "confidential"; break; + * } + * } + * + * virtual void deserialize( const QByteArray &data ) + * { + * if ( data == "public" ) + * mSecrecy = Public; + * else if ( data == "private" ) + * mSecrecy = Private; + * else if ( data == "confidential" ) + * mSecrecy = Confidential; + * } + * } + * + * @endcode + * + * Additionally, you need to register your attribute with Akonadi::AttributeFactory + * for automatic deserialization during retrieving of collecitons or items: + * + * @code + * AttributeFactory::registerAttribute(); + * @endcode + * + * Third party attributes need to be registered once by each application that uses + * them. So the above snippet needs to be in the resource that adds the attribute, + * and each application that uses the resource. This may be simplified in the future. + * + * The custom attributes can be used in the following way: + * + * @code + * + * Akonadi::Item item( "text/directory" ); + * SecrecyAttribute* attr = item.attribute( Item::AddIfMissing ); + * attr->setSecrecy( SecrecyAttribute::Confidential ); + * + * @endcode + * + * and + * + * @code + * + * Akonadi::Item item = ... + * + * if ( item.hasAttribute() ) { + * SecrecyAttribute *attr = item.attribute(); + * + * SecrecyAttribute::Secrecy secrecy = attr->secrecy(); + * ... + * } + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT Attribute +{ +public: + /** + * Describes a list of attributes. + */ + typedef QList List; + + /** + * Returns the type of the attribute. + */ + virtual QByteArray type() const = 0; + + /** + * Destroys this attribute. + */ + virtual ~Attribute(); + + /** + * Creates a copy of this attribute. + */ + virtual Attribute *clone() const = 0; + + /** + * Returns a QByteArray representation of the attribute which will be + * storaged. This can be raw binary data, no encoding needs to be applied. + */ + virtual QByteArray serialized() const = 0; + + /** + * Sets the data of this attribute, using the same encoding + * as returned by toByteArray(). + * + * @param data The encoded attribute data. + */ + virtual void deserialize(const QByteArray &data) = 0; +}; + +} + +#endif diff --git a/src/core/attributefactory.cpp b/src/core/attributefactory.cpp new file mode 100644 index 0000000..b2777f4 --- /dev/null +++ b/src/core/attributefactory.cpp @@ -0,0 +1,159 @@ +/* + Copyright (c) 2007 - 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 "attributefactory.h" + +#include "collectionquotaattribute.h" +#include "collectionrightsattribute_p.h" +#include "entitydisplayattribute.h" +#include "entityhiddenattribute.h" +#include "indexpolicyattribute.h" +#include "persistentsearchattribute.h" +#include "entitydeletedattribute.h" +#include "tagattribute.h" +#include "entityannotationsattribute.h" + +#include + +using namespace Akonadi; + +namespace Akonadi +{ +namespace Internal +{ + +/** + * @internal + */ +class DefaultAttribute : public Attribute +{ +public: + explicit DefaultAttribute(const QByteArray &type, const QByteArray &value = QByteArray()) + : mType(type) + , mValue(value) + { + } + + QByteArray type() const Q_DECL_OVERRIDE + { + return mType; + } + Attribute *clone() const Q_DECL_OVERRIDE + { + return new DefaultAttribute(mType, mValue); + } + + QByteArray serialized() const Q_DECL_OVERRIDE + { + return mValue; + } + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE { + mValue = data; + } + +private: + QByteArray mType, mValue; +}; + +/** + * @internal + */ +class StaticAttributeFactory : public AttributeFactory +{ +public: + StaticAttributeFactory() + : AttributeFactory() + , initialized(false) + { + } + void init() + { + if (initialized) { + return; + } + initialized = true; + + // Register built-in attributes + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + } + bool initialized; +}; + +Q_GLOBAL_STATIC(StaticAttributeFactory, s_attributeInstance) + +} + +using Akonadi::Internal::s_attributeInstance; + +/** + * @internal + */ +class Q_DECL_HIDDEN AttributeFactory::Private +{ +public: + QHash attributes; +}; + +AttributeFactory *AttributeFactory::self() +{ + s_attributeInstance->init(); + return s_attributeInstance; +} + +AttributeFactory::AttributeFactory() + : d(new Private) +{ +} + +AttributeFactory::~ AttributeFactory() +{ + qDeleteAll(d->attributes); + delete d; +} + +void AttributeFactory::registerAttribute(Attribute *attr) +{ + Q_ASSERT(attr); + Q_ASSERT(!attr->type().contains(' ') && !attr->type().contains('\'') && !attr->type().contains('"')); + QHash::Iterator it = d->attributes.find(attr->type()); + if (it != d->attributes.end()) { + delete *it; + d->attributes.erase(it); + } + d->attributes.insert(attr->type(), attr); +} + +Attribute *AttributeFactory::createAttribute(const QByteArray &type) +{ + Attribute *attr = self()->d->attributes.value(type); + if (attr) { + return attr->clone(); + } + return new Internal::DefaultAttribute(type); +} + +} diff --git a/src/core/attributefactory.h b/src/core/attributefactory.h new file mode 100644 index 0000000..0eea4dd --- /dev/null +++ b/src/core/attributefactory.h @@ -0,0 +1,90 @@ +/* + Copyright (c) 2007 - 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_ATTRIBUTEFACTORY_H +#define AKONADI_ATTRIBUTEFACTORY_H + +#include "akonadicore_export.h" +#include "attribute.h" + +namespace Akonadi +{ + +/** + * @short Provides the functionality of registering and creating arbitrary + * entity attributes. + * + * This class provides the functionality of registering and creating arbitrary Attributes for Entity + * and its subclasses (e.g. Item and Collection). + * + * @code + * + * // register the type first + * Akonadi::AttributeFactory::registerAttribute(); + * + * ... + * + * // use it anywhere else in the application + * SecrecyAttribute *attr = Akonadi::AttributeFactory::createAttribute( "secrecy" ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT AttributeFactory +{ +public: + //@cond PRIVATE + ~AttributeFactory(); + //@endcond + + /** + * Registers a custom attribute of type T. + * The same attribute cannot be registered more than once. + */ + template inline static void registerAttribute() + { + AttributeFactory::self()->registerAttribute(new T); + } + + /** + * Creates an entity attribute object of the given type. + * If the type has not been registered, creates a DefaultAttribute. + * + * @param type The attribute type. + */ + static Attribute *createAttribute(const QByteArray &type); + +protected: + //@cond PRIVATE + AttributeFactory(); + +private: + Q_DISABLE_COPY(AttributeFactory) + static AttributeFactory *self(); + void registerAttribute(Attribute *attribute); + + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/cachepolicy.cpp b/src/core/cachepolicy.cpp new file mode 100644 index 0000000..5020ad1 --- /dev/null +++ b/src/core/cachepolicy.cpp @@ -0,0 +1,147 @@ +/* + 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 "cachepolicy.h" +#include "collection.h" + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN CachePolicy::Private : public QSharedData +{ +public: + Private() + : QSharedData() + , inherit(true) + , timeout(-1) + , interval(-1) + , syncOnDemand(false) + { + } + + Private(const Private &other) + : QSharedData(other) + { + inherit = other.inherit; + localParts = other.localParts; + timeout = other.timeout; + interval = other.interval; + syncOnDemand = other.syncOnDemand; + } + + bool inherit; + QStringList localParts; + int timeout; + int interval; + bool syncOnDemand; +}; + +CachePolicy::CachePolicy() +{ + static QSharedDataPointer sharedPrivate(new Private); + d = sharedPrivate; +} + +CachePolicy::CachePolicy(const CachePolicy &other) + : d(other.d) +{ +} + +CachePolicy::~ CachePolicy() +{ +} + +CachePolicy &CachePolicy::operator =(const CachePolicy &other) +{ + d = other.d; + return *this; +} + +bool Akonadi::CachePolicy::operator ==(const CachePolicy &other) const +{ + if (!d->inherit && !other.d->inherit) { + return d->localParts == other.d->localParts + && d->timeout == other.d->timeout + && d->interval == other.d->interval + && d->syncOnDemand == other.d->syncOnDemand; + } + return d->inherit == other.d->inherit; +} + +bool CachePolicy::inheritFromParent() const +{ + return d->inherit; +} + +void CachePolicy::setInheritFromParent(bool inherit) +{ + d->inherit = inherit; +} + +QStringList CachePolicy::localParts() const +{ + return d->localParts; +} + +void CachePolicy::setLocalParts(const QStringList &parts) +{ + d->localParts = parts; +} + +int CachePolicy::cacheTimeout() const +{ + return d->timeout; +} + +void CachePolicy::setCacheTimeout(int timeout) +{ + d->timeout = timeout; +} + +int CachePolicy::intervalCheckTime() const +{ + return d->interval; +} + +void CachePolicy::setIntervalCheckTime(int time) +{ + d->interval = time; +} + +bool CachePolicy::syncOnDemand() const +{ + return d->syncOnDemand; +} + +void CachePolicy::setSyncOnDemand(bool enable) +{ + d->syncOnDemand = enable; +} + +QDebug operator<<(QDebug d, const CachePolicy &c) +{ + return d << "CachePolicy: " << endl + << " inherit:" << c.inheritFromParent() << endl + << " interval:" << c.intervalCheckTime() << endl + << " timeout:" << c.cacheTimeout() << endl + << " sync on demand:" << c.syncOnDemand() << endl + << " local parts:" << c.localParts(); +} diff --git a/src/core/cachepolicy.h b/src/core/cachepolicy.h new file mode 100644 index 0000000..cb3cdcb --- /dev/null +++ b/src/core/cachepolicy.h @@ -0,0 +1,172 @@ +/* + 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_CACHEPOLICY_H +#define AKONADI_CACHEPOLICY_H + +#include "akonadicore_export.h" + +#include +#include + +namespace Akonadi +{ + +/** + * @short Represents the caching policy for a collection. + * + * There is one cache policy per collection. It can either specify that all + * properties of the policy of the parent collection will be inherited (the + * default) or specify the following values: + * + * - The item parts that should be permanently kept locally and are downloaded + * during a collection sync (e.g. full mail vs. just the headers). + * - A minimum time for which non-permanently cached item parts have to be kept + * (0 - infinity). + * - Whether or not a collection sync is triggered on demand, i.e. as soon + * as it is accessed by a client. + * - An optional time interval for regular collection sync (aka interval + * mail check). + * + * Syncing means fetching updates from the Akonadi database. The cache policy + * does not affect updates of the Akonadi database from the backend, since + * backend updates will normally immediately trigger the resource to update the + * Akonadi database. + * + * The cache policy applies only to reading from the collection. Writing to the + * collection is independent of cache policy - all updates are written to the + * backend as soon as the resource can schedule this. + * + * @code + * + * Akonadi::CachePolicy policy; + * policy.setCacheTimeout( 30 ); + * policy.setIntervalCheckTime( 20 ); + * + * Akonadi::Collection collection = ... + * collection.setCachePolicy( policy ); + * + * @endcode + * + * @todo Do we also need a size limit for the cache as well? + * @todo on a POP3 account, is should not be possible to change locally cached parts, find a solution for that + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CachePolicy +{ +public: + /** + * Creates an empty cache policy. + */ + CachePolicy(); + + /** + * Creates a cache policy from an @p other cache policy. + */ + CachePolicy(const CachePolicy &other); + + /** + * Destroys the cache policy. + */ + ~CachePolicy(); + + /** + * Returns whether it inherits cache policy from the parent collection. + */ + bool inheritFromParent() const; + + /** + * Sets whether the cache policy should be inherited from the parent collection. + */ + void setInheritFromParent(bool inherit); + + /** + * Returns the parts to permanently cache locally. + */ + QStringList localParts() const; + + /** + * Specifies the parts to permanently cache locally. + */ + void setLocalParts(const QStringList &parts); + + /** + * Returns the cache timeout for non-permanently cached parts in minutes; + * -1 means indefinitely. + */ + int cacheTimeout() const; + + /** + * Sets cache timeout for non-permanently cached parts. + * @param timeout Timeout in minutes, -1 for indefinitely. + */ + void setCacheTimeout(int timeout); + + /** + * Returns the interval check time in minutes, -1 for never. + */ + int intervalCheckTime() const; + + /** + * Sets interval check time. + * @param time Check time interval in minutes, -1 for never. + */ + void setIntervalCheckTime(int time); + + /** + * Returns whether the collection will be synced automatically when necessary, + * i.e. as soon as it is accessed by a client. + */ + bool syncOnDemand() const; + + /** + * Sets whether the collection shall be synced automatically when necessary, + * i.e. as soon as it is accessed by a client. + * @param enable If @c true the collection is synced. + */ + void setSyncOnDemand(bool enable); + + /** + * @internal. + * @param other other cache policy + */ + CachePolicy &operator=(const CachePolicy &other); + + /** + * @internal + * @param other other cache policy + */ + bool operator==(const CachePolicy &other) const; + +private: + //@cond PRIVATE + class Private; + QSharedDataPointer d; + //@endcond +}; + +} + +/** + * Allows a cache policy to be output for debugging purposes. + */ +AKONADICORE_EXPORT QDebug operator<<(QDebug, const Akonadi::CachePolicy &); + +#endif diff --git a/src/core/changemediator_p.cpp b/src/core/changemediator_p.cpp new file mode 100644 index 0000000..925430e --- /dev/null +++ b/src/core/changemediator_p.cpp @@ -0,0 +1,114 @@ +/* + Copyright (c) 2011 Tobias Koenig + Copyright (c) 2011 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 "changemediator_p.h" + +#include + +#include "changenotificationdependenciesfactory_p.h" +#include "notificationsourceinterface.h" +#include "job_p.h" +#include "itemmovejob.h" +#include "collection.h" +#include "item.h" + +//static const char mediatorSessionId[] = "MediatorSession"; TODO: remove? + +using namespace Akonadi; + +Q_GLOBAL_STATIC(ChangeMediator, s_globalChangeMediator) + +ChangeMediator *ChangeMediator::instance() +{ + if (s_globalChangeMediator.isDestroyed()) { + return 0; + } else { + return s_globalChangeMediator; + } +} + +ChangeMediator::ChangeMediator(QObject *parent) + : QObject(parent) +{ + if (qApp) { + this->moveToThread(qApp->thread()); + } +} + +/* static */ +void ChangeMediator::registerMonitor(QObject *monitor) +{ + QMetaObject::invokeMethod(instance(), "do_registerMonitor", Q_ARG(QObject *, monitor)); +} + +/* static */ +void ChangeMediator::unregisterMonitor(QObject *monitor) +{ + QMetaObject::invokeMethod(instance(), "do_unregisterMonitor", Q_ARG(QObject *, monitor)); +} + +/* static */ +void ChangeMediator::invalidateCollection(const Akonadi::Collection &collection) +{ + QMetaObject::invokeMethod(instance(), "do_invalidateCollection", Q_ARG(Akonadi::Collection, collection)); +} + +/* static */ +void ChangeMediator::invalidateItem(const Akonadi::Item &item) +{ + QMetaObject::invokeMethod(instance(), "do_invalidateItem", Q_ARG(Akonadi::Item, item)); +} + +/* static */ +void ChangeMediator::invalidateTag(const Tag &tag) +{ + QMetaObject::invokeMethod(instance(), "do_invalidateTag", Q_ARG(Akonadi::Tag, tag)); +} + +void ChangeMediator::do_registerMonitor(QObject *monitor) +{ + m_monitors.append(monitor); +} + +void ChangeMediator::do_unregisterMonitor(QObject *monitor) +{ + m_monitors.removeAll(monitor); +} + +void ChangeMediator::do_invalidateCollection(const Akonadi::Collection &collection) +{ + foreach (QObject *monitor, m_monitors) { + QMetaObject::invokeMethod(monitor, "invalidateCollectionCache", Qt::AutoConnection, Q_ARG(qint64, collection.id())); + } +} + +void ChangeMediator::do_invalidateItem(const Akonadi::Item &item) +{ + foreach (QObject *monitor, m_monitors) { + QMetaObject::invokeMethod(monitor, "invalidateItemCache", Qt::AutoConnection, Q_ARG(qint64, item.id())); + } +} + +void ChangeMediator::do_invalidateTag(const Tag &tag) +{ + foreach (QObject *monitor, m_monitors) { + QMetaObject::invokeMethod(monitor, "invalidateTagCache", Qt::AutoConnection, Q_ARG(qint64, tag.id())); + } +} diff --git a/src/core/changemediator_p.h b/src/core/changemediator_p.h new file mode 100644 index 0000000..eca0dfc --- /dev/null +++ b/src/core/changemediator_p.h @@ -0,0 +1,68 @@ +/* + Copyright (c) 2011 Tobias Koenig + Copyright (c) 2011 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_CHANGEMEDIATOR_P_H +#define AKONADI_CHANGEMEDIATOR_P_H + +#include +#include +#include + +#include "item.h" + +namespace Akonadi +{ + +class Job; +class JobPrivate; + +class Collection; +class Item; + +class ChangeMediator : public QObject +{ + Q_OBJECT +public: + explicit ChangeMediator(QObject *parent = Q_NULLPTR); + + static ChangeMediator *instance(); + + static void registerMonitor(QObject *monitor); + static void unregisterMonitor(QObject *monitor); + + static void invalidateCollection(const Akonadi::Collection &collection); + static void invalidateItem(const Akonadi::Item &item); + static void invalidateTag(const Akonadi::Tag &tag); + +private Q_SLOTS: + void do_registerMonitor(QObject *monitor); + void do_unregisterMonitor(QObject *monitor); + + void do_invalidateCollection(const Akonadi::Collection &collection); + void do_invalidateItem(const Akonadi::Item &item); + void do_invalidateTag(const Akonadi::Tag &tag); + +private: + QList m_monitors; +}; + +} + +#endif diff --git a/src/core/changenotificationdependenciesfactory.cpp b/src/core/changenotificationdependenciesfactory.cpp new file mode 100644 index 0000000..d9baf40 --- /dev/null +++ b/src/core/changenotificationdependenciesfactory.cpp @@ -0,0 +1,111 @@ +/* + Copyright (c) 2011 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 "changenotificationdependenciesfactory_p.h" +#include "KDBusConnectionPool" +#include "notificationsource_p.h" +#include "notificationbus_p.h" +#include "notificationsourceinterface.h" +#include "notificationmanagerinterface.h" +#include "changemediator_p.h" +#include "servermanager.h" +#include "akonadicore_debug.h" + +#include +#include + +using namespace Akonadi; + +NotificationSource *ChangeNotificationDependenciesFactory::createNotificationSource(QObject *parent) +{ + if (!Akonadi::ServerManager::self()->isRunning()) { + return 0; + } + + org::freedesktop::Akonadi::NotificationManager *manager = + new org::freedesktop::Akonadi::NotificationManager( + ServerManager::serviceName(Akonadi::ServerManager::Server), + QStringLiteral("/notifications"), + KDBusConnectionPool::threadConnection()); + + if (!manager) { + // :TODO: error handling + return 0; + } + + const QString name = + QStringLiteral("%1_%2_%3").arg( + QCoreApplication::applicationName(), + QString::number(QCoreApplication::applicationPid()), + KRandom::randomString(6)); + QDBusObjectPath p = manager->subscribe(name, false); + const bool validError = manager->lastError().isValid(); + if (validError) { + qCWarning(AKONADICORE_LOG) << manager->lastError().name() << manager->lastError().message(); + // :TODO: What to do? + delete manager; + return 0; + } + delete manager; + org::freedesktop::Akonadi::NotificationSource *notificationSource = + new org::freedesktop::Akonadi::NotificationSource( + ServerManager::serviceName(Akonadi::ServerManager::Server), + p.path(), + KDBusConnectionPool::threadConnection(), parent); + + if (!notificationSource) { + // :TODO: error handling + return 0; + } + return new NotificationSource(notificationSource); +} + +QObject *ChangeNotificationDependenciesFactory::createNotificationBus(QObject *parent, NotificationSource *source) +{ + NotificationBusPrivate *priv = new NotificationBusPrivate; + Session *session = new Session(priv, source->identifier().toLatin1(), parent); + priv->setParent(session); + return priv; +} + +QObject *ChangeNotificationDependenciesFactory::createChangeMediator(QObject *parent) +{ + Q_UNUSED(parent); + return ChangeMediator::instance(); +} + +CollectionCache *ChangeNotificationDependenciesFactory::createCollectionCache(int maxCapacity, Session *session) +{ + return new CollectionCache(maxCapacity, session); +} + +ItemCache *ChangeNotificationDependenciesFactory::createItemCache(int maxCapacity, Session *session) +{ + return new ItemCache(maxCapacity, session); +} + +ItemListCache *ChangeNotificationDependenciesFactory::createItemListCache(int maxCapacity, Session *session) +{ + return new ItemListCache(maxCapacity, session); +} + +TagListCache *ChangeNotificationDependenciesFactory::createTagListCache(int maxCapacity, Session *session) +{ + return new TagListCache(maxCapacity, session); +} diff --git a/src/core/changenotificationdependenciesfactory_p.h b/src/core/changenotificationdependenciesfactory_p.h new file mode 100644 index 0000000..414984f --- /dev/null +++ b/src/core/changenotificationdependenciesfactory_p.h @@ -0,0 +1,53 @@ +/* + Copyright (c) 2011 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 CHANGENOTIFICATIONDEPENDENCIESFACTORY_P_H +#define CHANGENOTIFICATIONDEPENDENCIESFACTORY_P_H + +#include "session.h" +#include "entitycache_p.h" + +namespace Akonadi +{ + +class NotificationSource; + +/** + * This class exists so that we can create a fake notification source in + * unit tests. + */ +class AKONADI_TESTS_EXPORT ChangeNotificationDependenciesFactory +{ +public: + virtual ~ChangeNotificationDependenciesFactory() + { + } + virtual NotificationSource *createNotificationSource(QObject *parent); + virtual QObject *createNotificationBus(QObject *parent, NotificationSource *source); + virtual QObject *createChangeMediator(QObject *parent); + + virtual Akonadi::CollectionCache *createCollectionCache(int maxCapacity, Session *session); + virtual Akonadi::ItemCache *createItemCache(int maxCapacity, Session *session); + virtual Akonadi::ItemListCache *createItemListCache(int maxCapacity, Session *session); + virtual Akonadi::TagListCache *createTagListCache(int maxCapacity, Session *session); +}; + +} + +#endif diff --git a/src/core/changerecorder.cpp b/src/core/changerecorder.cpp new file mode 100644 index 0000000..17b2e18 --- /dev/null +++ b/src/core/changerecorder.cpp @@ -0,0 +1,128 @@ +/* + 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 "changerecorder.h" +#include "changerecorder_p.h" + +#include +#include + +using namespace Akonadi; + +ChangeRecorder::ChangeRecorder(QObject *parent) + : Monitor(new ChangeRecorderPrivate(0, this), parent) +{ +} + +ChangeRecorder::ChangeRecorder(ChangeRecorderPrivate *privateclass, QObject *parent) + : Monitor(privateclass, parent) +{ +} + +ChangeRecorder::~ChangeRecorder() +{ +} + +void ChangeRecorder::setConfig(QSettings *settings) +{ + Q_D(ChangeRecorder); + if (settings) { + d->settings = settings; + Q_ASSERT(d->pendingNotifications.isEmpty()); + d->loadNotifications(); + } else if (d->settings) { + if (d->enableChangeRecording) { + d->saveNotifications(); + } + d->settings = settings; + } +} + +void ChangeRecorder::replayNext() +{ + Q_D(ChangeRecorder); + + if (!d->enableChangeRecording) { + return; + } + + if (!d->pendingNotifications.isEmpty()) { + const Protocol::ChangeNotification msg = d->pendingNotifications.head(); + if (d->ensureDataAvailable(msg)) { + d->emitNotification(msg); + } else if (d->translateAndCompress(d->pipeline, msg)) { + // The msg is now in both pipeline and pendingNotifications. + // When data is available, MonitorPrivate::flushPipeline will emitNotification. + // When changeProcessed is called, we'll finally remove it from pendingNotifications. + } else { + // In the case of a move where both source and destination are + // ignored, we ignore the message and process the next one. + d->dequeueNotification(); + return replayNext(); + } + } else { + // This is necessary when none of the notifications were accepted / processed + // above, and so there is no one to call changeProcessed() and the ChangeReplay task + // will be stuck forever in the ResourceScheduler. + emit nothingToReplay(); + } +} + +bool ChangeRecorder::isEmpty() const +{ + Q_D(const ChangeRecorder); + return d->pendingNotifications.isEmpty(); +} + +void ChangeRecorder::changeProcessed() +{ + Q_D(ChangeRecorder); + + if (!d->enableChangeRecording) { + return; + } + + // changerecordertest.cpp calls changeProcessed after receiving nothingToReplay, + // so test for emptiness. Not sure real code does this though. + // Q_ASSERT( !d->pendingNotifications.isEmpty() ) + if (!d->pendingNotifications.isEmpty()) { + d->dequeueNotification(); + } +} + +void ChangeRecorder::setChangeRecordingEnabled(bool enable) +{ + Q_D(ChangeRecorder); + if (d->enableChangeRecording == enable) { + return; + } + d->enableChangeRecording = enable; + if (enable) { + d->m_needFullSave = true; + d->notificationsLoaded(); + } else { + d->dispatchNotifications(); + } +} + +QString Akonadi::ChangeRecorder::dumpNotificationListToString() const +{ + Q_D(const ChangeRecorder); + return d->dumpNotificationListToString(); +} diff --git a/src/core/changerecorder.h b/src/core/changerecorder.h new file mode 100644 index 0000000..2743bf0 --- /dev/null +++ b/src/core/changerecorder.h @@ -0,0 +1,125 @@ +/* + 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_CHANGERECORDER_H +#define AKONADI_CHANGERECORDER_H + +#include "akonadicore_export.h" +#include "monitor.h" + +class QSettings; + +namespace Akonadi +{ + +class ChangeRecorderPrivate; + +/** + * @short Records and replays change notification. + * + * This class is responsible for recording change notifications while + * an agent is not online and replaying the notifications when the agent + * is online again. Therefore the agent doesn't have to care about + * online/offline mode in its synchronization algorithm. + * + * Unlike Akonadi::Monitor this class only emits one change signal at a + * time. To receive the next one you need to explicitly call replayNext(). + * If a signal is emitted that has no receivers, it's automatically skipped, + * which means you only need to connect to signals you are actually interested + * in. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ChangeRecorder : public Monitor +{ + Q_OBJECT +public: + /** + * Creates a new change recorder. + */ + explicit ChangeRecorder(QObject *parent = Q_NULLPTR); + + /** + * Destroys the change recorder. + * All not yet processed changes are written back to the config file. + */ + ~ChangeRecorder(); + + /** + * Sets the QSettings object used for persistent recorded changes. + */ + void setConfig(QSettings *settings); + + /** + * Returns whether there are recorded changes. + */ + bool isEmpty() const; + + /** + * Removes the previously emitted change from the records. + */ + void changeProcessed(); + + /** + * Enables change recording. If change recording is disabled, this class + * behaves exactly like Akonadi::Monitor. + * Change recording is enabled by default. + * @param enable @c false to disable change recording. @c true by default + */ + void setChangeRecordingEnabled(bool enable); + + /** + * Debugging: dump current list of notifications, as saved on disk. + */ + QString dumpNotificationListToString() const; + +public Q_SLOTS: + /** + * Replay the next change notification and erase the previous one from the record. + */ + void replayNext(); + +Q_SIGNALS: + /** + * Emitted when new changes are recorded. + */ + void changesAdded(); + + /** + * Emitted when replayNext() was called, but there was no valid change to replay. + * This can happen when all pending changes have been filtered out, for example. + * You only need to connect to this signal if you rely on one signal being emitted + * as a result of calling replayNext(). + */ + void nothingToReplay(); + +protected: + //@cond PRIVATE + explicit ChangeRecorder(ChangeRecorderPrivate *d, QObject *parent = Q_NULLPTR); + //@endcond + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(ChangeRecorder) + //@endcond +}; + +} + +#endif diff --git a/src/core/changerecorder_p.cpp b/src/core/changerecorder_p.cpp new file mode 100644 index 0000000..e3ce060 --- /dev/null +++ b/src/core/changerecorder_p.cpp @@ -0,0 +1,507 @@ +/* + 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 "changerecorder_p.h" +#include "akonadicore_debug.h" + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +ChangeRecorderPrivate::ChangeRecorderPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, + ChangeRecorder *parent) + : MonitorPrivate(dependenciesFactory_, parent) + , settings(0) + , enableChangeRecording(true) + , m_lastKnownNotificationsCount(0) + , m_startOffset(0) + , m_needFullSave(true) +{ +} + +int ChangeRecorderPrivate::pipelineSize() const +{ + if (enableChangeRecording) { + return 0; // we fill the pipeline ourselves when using change recording + } + return MonitorPrivate::pipelineSize(); +} + +void ChangeRecorderPrivate::slotNotify(const Akonadi::Protocol::ChangeNotification &msg) +{ + Q_Q(ChangeRecorder); + const int oldChanges = pendingNotifications.size(); + // with change recording disabled this will automatically take care of dispatching notification messages and saving + MonitorPrivate::slotNotify(msg); + if (enableChangeRecording && pendingNotifications.size() != oldChanges) { + emit q->changesAdded(); + } +} + +// The QSettings object isn't actually used anymore, except for migrating old data +// and it gives us the base of the filename to use. This is all historical. +QString ChangeRecorderPrivate::notificationsFileName() const +{ + return settings->fileName() + QStringLiteral("_changes.dat"); +} + +void ChangeRecorderPrivate::loadNotifications() +{ + pendingNotifications.clear(); + Q_ASSERT(pipeline.isEmpty()); + pipeline.clear(); + + const QString changesFileName = notificationsFileName(); + + /** + * In an older version we recorded changes inside the settings object, however + * for performance reasons we changed that to store them in a separated file. + * If this file doesn't exists, it means we run the new version the first time, + * so we have to read in the legacy list of changes first. + */ + if (!QFile::exists(changesFileName)) { + QStringList list; + settings->beginGroup(QStringLiteral("ChangeRecorder")); + const int size = settings->beginReadArray(QStringLiteral("change")); + + for (int i = 0; i < size; ++i) { + settings->setArrayIndex(i); + Protocol::ChangeNotification msg; + msg.setSessionId(settings->value(QStringLiteral("sessionId")).toByteArray()); + msg.setType((Protocol::ChangeNotification::Type)settings->value(QStringLiteral("type")).toInt()); + msg.setOperation((Protocol::ChangeNotification::Operation)settings->value(QStringLiteral("op")).toInt()); + msg.addEntity(settings->value(QStringLiteral("uid")).toLongLong(), + settings->value(QStringLiteral("rid")).toString(), + QString(), + settings->value(QStringLiteral("mimeType")).toString()); + msg.setResource(settings->value(QStringLiteral("resource")).toByteArray()); + msg.setParentCollection(settings->value(QStringLiteral("parentCol")).toLongLong()); + msg.setParentDestCollection(settings->value(QStringLiteral("parentDestCol")).toLongLong()); + list = settings->value(QStringLiteral("itemParts")).toStringList(); + QSet itemParts; + Q_FOREACH (const QString &entry, list) { + itemParts.insert(entry.toLatin1()); + } + msg.setItemParts(itemParts); + pendingNotifications << msg; + } + + settings->endArray(); + + // save notifications to the new file... + saveNotifications(); + + // ...delete the legacy list... + settings->remove(QString()); + settings->endGroup(); + + // ...and continue as usually + } + + QFile file(changesFileName); + if (file.open(QIODevice::ReadOnly)) { + m_needFullSave = false; + pendingNotifications = loadFrom(&file, m_needFullSave); + } else { + m_needFullSave = true; + } + notificationsLoaded(); +} + +static const quint64 s_currentVersion = Q_UINT64_C(0x000400000000); +static const quint64 s_versionMask = Q_UINT64_C(0xFFFF00000000); +static const quint64 s_sizeMask = Q_UINT64_C(0x0000FFFFFFFF); + +QQueue ChangeRecorderPrivate::loadFrom(QIODevice *device, bool &needsFullSave) const +{ + QDataStream stream(device); + stream.setVersion(QDataStream::Qt_4_6); + + QByteArray sessionId, resource, destinationResource; + int type, operation, entityCnt; + quint64 uid, parentCollection, parentDestCollection; + QString remoteId, mimeType, remoteRevision; + QSet itemParts, addedFlags, removedFlags; + QSet addedTags, removedTags; + + QQueue list; + + quint64 sizeAndVersion; + stream >> sizeAndVersion; + + const quint64 size = sizeAndVersion & s_sizeMask; + const quint64 version = (sizeAndVersion & s_versionMask) >> 32; + + quint64 startOffset = 0; + if (version >= 1) { + stream >> startOffset; + } + + // If we skip the first N items, then we'll need to rewrite the file on saving. + // Also, if the file is old, it needs to be rewritten. + needsFullSave = startOffset > 0 || version == 0; + + for (quint64 i = 0; i < size && !stream.atEnd(); ++i) { + Protocol::ChangeNotification msg; + + if (version == 1) { + stream >> sessionId; + stream >> type; + stream >> operation; + stream >> uid; + stream >> remoteId; + stream >> resource; + stream >> parentCollection; + stream >> parentDestCollection; + stream >> mimeType; + stream >> itemParts; + + if (i < startOffset) { + continue; + } + + msg.setSessionId(sessionId); + msg.setType(static_cast(type)); + msg.setOperation(static_cast(operation)); + msg.addEntity(uid, remoteId, QString(), mimeType); + msg.setResource(resource); + msg.setParentCollection(parentCollection); + msg.setParentDestCollection(parentDestCollection); + msg.setItemParts(itemParts); + + } else if (version >= 2) { + + Protocol::ChangeNotification msg; + + stream >> sessionId; + stream >> type; + stream >> operation; + stream >> entityCnt; + for (int j = 0; j < entityCnt; ++j) { + stream >> uid; + stream >> remoteId; + stream >> remoteRevision; + stream >> mimeType; + msg.addEntity(uid, remoteId, remoteRevision, mimeType); + } + stream >> resource; + stream >> destinationResource; + stream >> parentCollection; + stream >> parentDestCollection; + stream >> itemParts; + stream >> addedFlags; + stream >> removedFlags; + if (version >= 3) { + stream >> addedTags; + stream >> removedTags; + } + + if (i < startOffset) { + continue; + } + + msg.setSessionId(sessionId); + msg.setType(static_cast(type)); + msg.setOperation(static_cast(operation)); + msg.setResource(resource); + msg.setDestinationResource(destinationResource); + msg.setParentCollection(parentCollection); + msg.setParentDestCollection(parentDestCollection); + msg.setItemParts(itemParts); + msg.setAddedFlags(addedFlags); + msg.setRemovedFlags(removedFlags); + msg.setAddedTags(addedTags); + msg.setRemovedTags(removedTags); + + list << msg; + } + } + + return list; +} + +static QString join(const QSet &set) +{ + QString string; + Q_FOREACH (const QByteArray &b, set) { + string += QString::fromLatin1(b) + QLatin1String(", "); + } + return string; +} + +static QString join(const QList &set) +{ + QString string; + Q_FOREACH (qint64 b, set) { + string += QString::number(b) + QLatin1String(", "); + } + return string; +} + +QString ChangeRecorderPrivate::dumpNotificationListToString() const +{ + if (!settings) { + return QStringLiteral("No settings set in ChangeRecorder yet."); + } + const QString changesFileName = notificationsFileName(); + QFile file(changesFileName); + + if (!file.open(QIODevice::ReadOnly)) { + return QLatin1String("Error reading ") + changesFileName; + } + + QString result; + bool dummy; + const QQueue notifications = loadFrom(&file, dummy); + Q_FOREACH (const Protocol::ChangeNotification &n, notifications) { + QString typeString; + switch (n.type()) { + case Protocol::ChangeNotification::Collections: + typeString = QStringLiteral("Collections"); + break; + case Protocol::ChangeNotification::Items: + typeString = QStringLiteral("Items"); + break; + case Protocol::ChangeNotification::Tags: + typeString = QStringLiteral("Tags"); + break; + default: + typeString = QStringLiteral("InvalidType"); + break; + }; + + QString operationString; + switch (n.operation()) { + case Protocol::ChangeNotification::Add: + operationString = QStringLiteral("Add"); + break; + case Protocol::ChangeNotification::Modify: + operationString = QStringLiteral("Modify"); + break; + case Protocol::ChangeNotification::ModifyFlags: + operationString = QStringLiteral("ModifyFlags"); + break; + case Protocol::ChangeNotification::ModifyTags: + operationString = QStringLiteral("ModifyTags"); + break; + case Protocol::ChangeNotification::Move: + operationString = QStringLiteral("Move"); + break; + case Protocol::ChangeNotification::Remove: + operationString = QStringLiteral("Remove"); + break; + case Protocol::ChangeNotification::Link: + operationString = QStringLiteral("Link"); + break; + case Protocol::ChangeNotification::Unlink: + operationString = QStringLiteral("Unlink"); + break; + case Protocol::ChangeNotification::Subscribe: + operationString = QStringLiteral("Subscribe"); + break; + case Protocol::ChangeNotification::Unsubscribe: + operationString = QStringLiteral("Unsubscribe"); + break; + default: + operationString = QStringLiteral("InvalidOp"); + break; + }; + + const QString entities = join(n.entities().keys()); + const QString addedTags = join(n.addedTags().toList()); + const QString removedTags = join(n.removedTags().toList()); + + const QString entry = QStringLiteral("session=%1 type=%2 operation=%3 items=%4 resource=%5 destResource=%6 parentCollection=%7 parentDestCollection=%8 itemParts=%9 addedFlags=%10 removedFlags=%11 addedTags=%12 removedTags=%13") + .arg(QString::fromLatin1(n.sessionId())) + .arg(typeString) + .arg(operationString) + .arg(entities) + .arg(QString::fromLatin1(n.resource())) + .arg(QString::fromLatin1(n.destinationResource())) + .arg(n.parentCollection()) + .arg(n.parentDestCollection()) + .arg(join(n.itemParts())) + .arg(join(n.addedFlags())) + .arg(join(n.removedFlags())) + .arg(addedTags) + .arg(removedTags); + result += entry + QLatin1Char('\n'); + } + return result; +} + +void ChangeRecorderPrivate::addToStream(QDataStream &stream, const Protocol::ChangeNotification &msg) +{ + // We deliberately don't use Factory::serialize(), because the internal + // serialization format could change at any point + + stream << msg.sessionId(); + stream << int(msg.type()); + stream << int(msg.operation()); + stream << msg.entities().count(); + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + stream << quint64(entity.id); + stream << entity.remoteId; + stream << entity.remoteRevision; + stream << entity.mimeType; + } + stream << msg.resource(); + stream << msg.destinationResource(); + stream << quint64(msg.parentCollection()); + stream << quint64(msg.parentDestCollection()); + stream << msg.itemParts(); + stream << msg.addedFlags(); + stream << msg.removedFlags(); + stream << msg.addedTags(); + stream << msg.removedTags(); +} + +void ChangeRecorderPrivate::writeStartOffset() +{ + if (!settings) { + return; + } + + QFile file(notificationsFileName()); + if (!file.open(QIODevice::ReadWrite)) { + qCWarning(AKONADICORE_LOG) << "Could not update notifications in file" << file.fileName(); + return; + } + + // Skip "countAndVersion" + file.seek(8); + + //qCDebug(AKONADICORE_LOG) << "Writing start offset=" << m_startOffset; + + QDataStream stream(&file); + stream.setVersion(QDataStream::Qt_4_6); + stream << static_cast(m_startOffset); + + // Everything else stays unchanged +} + +void ChangeRecorderPrivate::saveNotifications() +{ + if (!settings) { + return; + } + + QFile file(notificationsFileName()); + QFileInfo info(file); + if (!QFile::exists(info.absolutePath())) { + QDir dir; + dir.mkpath(info.absolutePath()); + } + if (!file.open(QIODevice::WriteOnly)) { + qCWarning(AKONADICORE_LOG) << "Could not save notifications to file" << file.fileName(); + return; + } + saveTo(&file); + m_needFullSave = false; + m_startOffset = 0; +} + +void ChangeRecorderPrivate::saveTo(QIODevice *device) +{ + // Version 0 of this file format was writing a quint64 count, followed by the notifications. + // Version 1 bundles a version number into that quint64, to be able to detect a version number at load time. + + const quint64 countAndVersion = static_cast(pendingNotifications.count()) | s_currentVersion; + + QDataStream stream(device); + stream.setVersion(QDataStream::Qt_4_6); + + stream << countAndVersion; + stream << quint64(0); // no start offset + + //qCDebug(AKONADICORE_LOG) << "Saving" << pendingNotifications.count() << "notifications (full save)"; + + for (int i = 0; i < pendingNotifications.count(); ++i) { + const Protocol::ChangeNotification msg = pendingNotifications.at(i); + addToStream(stream, msg); + } +} + +void ChangeRecorderPrivate::notificationsEnqueued(int count) +{ + // Just to ensure the contract is kept, and these two methods are always properly called. + if (enableChangeRecording) { + m_lastKnownNotificationsCount += count; + if (m_lastKnownNotificationsCount != pendingNotifications.count()) { + qCWarning(AKONADICORE_LOG) << this << "The number of pending notifications changed without telling us! Expected" + << m_lastKnownNotificationsCount << "but got" << pendingNotifications.count() + << "Caller just added" << count; + Q_ASSERT(pendingNotifications.count() == m_lastKnownNotificationsCount); + } + + saveNotifications(); + } +} + +void ChangeRecorderPrivate::dequeueNotification() +{ + if (pendingNotifications.isEmpty()) { + return; + } + + pendingNotifications.dequeue(); + if (enableChangeRecording) { + + Q_ASSERT(pendingNotifications.count() == m_lastKnownNotificationsCount - 1); + --m_lastKnownNotificationsCount; + + if (m_needFullSave || pendingNotifications.isEmpty()) { + saveNotifications(); + } else { + ++m_startOffset; + writeStartOffset(); + } + } +} + +void ChangeRecorderPrivate::notificationsErased() +{ + if (enableChangeRecording) { + m_lastKnownNotificationsCount = pendingNotifications.count(); + m_needFullSave = true; + saveNotifications(); + } +} + +void ChangeRecorderPrivate::notificationsLoaded() +{ + m_lastKnownNotificationsCount = pendingNotifications.count(); + m_startOffset = 0; +} + +bool ChangeRecorderPrivate::emitNotification(const Protocol::ChangeNotification &msg) +{ + const bool someoneWasListening = MonitorPrivate::emitNotification(msg); + if (!someoneWasListening && enableChangeRecording) { + //If no signal was emitted (e.g. because no one was connected to it), no one is going to call changeProcessed, so we help ourselves. + dequeueNotification(); + QMetaObject::invokeMethod(q_ptr, "replayNext", Qt::QueuedConnection); + } + return someoneWasListening; +} + diff --git a/src/core/changerecorder_p.h b/src/core/changerecorder_p.h new file mode 100644 index 0000000..e6c3010 --- /dev/null +++ b/src/core/changerecorder_p.h @@ -0,0 +1,69 @@ +/* + 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_CHANGERECORDER_P_H +#define AKONADI_CHANGERECORDER_P_H + +#include "akonadiprivate_export.h" +#include "changerecorder.h" +#include "monitor_p.h" + +namespace Akonadi +{ + +class ChangeRecorder; +class ChangeNotificationDependenciesFactory; + +class AKONADI_TESTS_EXPORT ChangeRecorderPrivate : public Akonadi::MonitorPrivate +{ +public: + ChangeRecorderPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, ChangeRecorder *parent); + + Q_DECLARE_PUBLIC(ChangeRecorder) + QSettings *settings; + bool enableChangeRecording; + + int pipelineSize() const Q_DECL_OVERRIDE; + void notificationsEnqueued(int count) Q_DECL_OVERRIDE; + void notificationsErased() Q_DECL_OVERRIDE; + + void slotNotify(const Protocol::ChangeNotification &msg) Q_DECL_OVERRIDE; + bool emitNotification(const Akonadi::Protocol::ChangeNotification &msg) Q_DECL_OVERRIDE; + + QString notificationsFileName() const; + + void loadNotifications(); + QQueue loadFrom(QIODevice *device, bool &needsFullSave) const; + QString dumpNotificationListToString() const; + void addToStream(QDataStream &stream, const Protocol::ChangeNotification &msg); + void saveNotifications(); + void saveTo(QIODevice *device); +private: + void dequeueNotification(); + void notificationsLoaded(); + void writeStartOffset(); + + int m_lastKnownNotificationsCount; // just for invariant checking + int m_startOffset; // number of saved notifications to skip + bool m_needFullSave; +}; + +} // namespace Akonadi + +#endif diff --git a/src/core/collection.cpp b/src/core/collection.cpp new file mode 100644 index 0000000..0e95b78 --- /dev/null +++ b/src/core/collection.cpp @@ -0,0 +1,461 @@ +/* + 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 "collection.h" +#include "collection_p.h" + +#include "attributefactory.h" +#include "cachepolicy.h" +#include "collectionrightsattribute_p.h" +#include "collectionstatistics.h" +#include "entitydisplayattribute.h" + +#include +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +Q_GLOBAL_STATIC(Akonadi::Collection, s_defaultParentCollection) + +uint Akonadi::qHash(const Akonadi::Collection &collection) +{ + return ::qHash(collection.id()); +} + +/** + * Helper method for assignment operator and copy constructor. + */ +static void assignCollectionPrivate(QSharedDataPointer &one, + const QSharedDataPointer &other) +{ + // We can't simply do one = other here, we have to use a temp. + // Otherwise ProtocolHelperTest::testParentCollectionAfterCollectionParsing() + // will break. + // + // The reason are assignments like + // col = col.parentCollection() + // + // Here, parentCollection() actually returns a reference to a pointer owned + // by col. So when col (or rather, it's private class) is deleted, the pointer + // to the parent collection and therefore the reference becomes invalid. + // + // With a single-line assignment here, the parent collection would be deleted + // before it is assigned, and therefore the resulting object would point to + // uninitalized memory. + QSharedDataPointer temp = other; + one = temp; +} + +class CollectionRoot : public Collection +{ +public: + CollectionRoot() + : Collection(0) + { + setContentMimeTypes({ Collection::mimeType() }); + + // The root collection is read-only for the users + setRights(Collection::ReadOnly); + } +}; + +Q_GLOBAL_STATIC(CollectionRoot, s_root) + +Collection::Collection() + : d_ptr(new CollectionPrivate) +{ + static int lastId = -1; + d_ptr->mId = lastId--; +} + +Collection::Collection(Id id) + : d_ptr(new CollectionPrivate(id)) +{ +} + +Collection::Collection(const Collection &other) +{ + assignCollectionPrivate(d_ptr, other.d_ptr); +} + +Collection::~Collection() +{ +} + +void Collection::setId(Collection::Id identifier) +{ + d_ptr->mId = identifier; +} + +Collection::Id Collection::id() const +{ + return d_ptr->mId; +} + +void Collection::setRemoteId(const QString &id) +{ + d_ptr->mRemoteId = id; +} + +QString Collection::remoteId() const +{ + return d_ptr->mRemoteId; +} + +void Collection::setRemoteRevision(const QString &revision) +{ + d_ptr->mRemoteRevision = revision; +} + +QString Collection::remoteRevision() const +{ + return d_ptr->mRemoteRevision; +} + +bool Collection::isValid() const +{ + return (d_ptr->mId >= 0); +} + +bool Collection::operator==(const Collection &other) const +{ + // Invalid collections are the same, no matter what their internal ID is + return (!isValid() && !other.isValid()) || (d_ptr->mId == other.d_ptr->mId); +} + +bool Akonadi::Collection::operator!=(const Collection &other) const +{ + return (isValid() || other.isValid()) && (d_ptr->mId != other.d_ptr->mId); +} + +Collection &Collection ::operator=(const Collection &other) +{ + if (this != &other) { + assignCollectionPrivate(d_ptr, other.d_ptr); + } + + return *this; +} + +bool Akonadi::Collection::operator<(const Collection &other) const +{ + return d_ptr->mId < other.d_ptr->mId; +} + +void Collection::addAttribute(Attribute *attr) +{ + Q_ASSERT(attr); + Attribute *existing = d_ptr->mAttributes.value(attr->type()); + if (existing) { + if (attr == existing) { + return; + } + d_ptr->mAttributes.remove(attr->type()); + delete existing; + } + d_ptr->mAttributes.insert(attr->type(), attr); + d_ptr->mDeletedAttributes.remove(attr->type()); +} + +void Collection::removeAttribute(const QByteArray &type) +{ + d_ptr->mDeletedAttributes.insert(type); + delete d_ptr->mAttributes.take(type); +} + +bool Collection::hasAttribute(const QByteArray &type) const +{ + return d_ptr->mAttributes.contains(type); +} + +Attribute::List Collection::attributes() const +{ + return d_ptr->mAttributes.values(); +} + +void Akonadi::Collection::clearAttributes() +{ + Q_FOREACH (Attribute *attr, d_ptr->mAttributes) { + d_ptr->mDeletedAttributes.insert(attr->type()); + delete attr; + } + d_ptr->mAttributes.clear(); +} + +Attribute *Collection::attribute(const QByteArray &type) const +{ + return d_ptr->mAttributes.value(type); +} + +Collection &Collection::parentCollection() +{ + if (!d_ptr->mParent) { + d_ptr->mParent = new Collection(); + } + return *(d_ptr->mParent); +} + +Collection Collection::parentCollection() const +{ + if (!d_ptr->mParent) { + return *(s_defaultParentCollection); + } else { + return *(d_ptr->mParent); + } +} + +void Collection::setParentCollection(const Collection &parent) +{ + delete d_ptr->mParent; + d_ptr->mParent = new Collection(parent); +} + +QString Collection::name() const +{ + return d_ptr->name; +} + +QString Collection::displayName() const +{ + const EntityDisplayAttribute *const attr = attribute(); + const QString displayName = attr ? attr->displayName() : QString(); + return !displayName.isEmpty() ? displayName : d_ptr->name; +} + +void Collection::setName(const QString &name) +{ + d_ptr->name = name; +} + +Collection::Rights Collection::rights() const +{ + CollectionRightsAttribute *attr = attribute(); + if (attr) { + return attr->rights(); + } else { + return AllRights; + } +} + +void Collection::setRights(Rights rights) +{ + CollectionRightsAttribute *attr = attribute(AddIfMissing); + attr->setRights(rights); +} + +QStringList Collection::contentMimeTypes() const +{ + return d_ptr->contentTypes; +} + +void Collection::setContentMimeTypes(const QStringList &types) +{ + if (d_ptr->contentTypes != types) { + d_ptr->contentTypes = types; + d_ptr->contentTypesChanged = true; + } +} + +QUrl Collection::url(UrlType type) const +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("collection"), QString::number(id())); + if (type == UrlWithName) { + query.addQueryItem(QStringLiteral("name"), name()); + } + + QUrl url; + url.setScheme(QStringLiteral("akonadi")); + url.setQuery(query); + return url; +} + +Collection Collection::fromUrl(const QUrl &url) +{ + if (url.scheme() != QLatin1String("akonadi")) { + return Collection(); + } + + const QString colStr = QUrlQuery(url).queryItemValue(QStringLiteral("collection")); + bool ok = false; + Collection::Id colId = colStr.toLongLong(&ok); + if (!ok) { + return Collection(); + } + + if (colId == 0) { + return Collection::root(); + } + + return Collection(colId); +} + +Collection Collection::root() +{ + return *s_root; +} + +QString Collection::mimeType() +{ + return QStringLiteral("inode/directory"); +} + +QString Akonadi::Collection::virtualMimeType() +{ + return QStringLiteral("application/x-vnd.akonadi.collection.virtual"); +} + +QString Collection::resource() const +{ + return d_ptr->resource; +} + +void Collection::setResource(const QString &resource) +{ + d_ptr->resource = resource; +} + +QDebug operator <<(QDebug d, const Akonadi::Collection &collection) +{ + return d << "Collection ID:" << collection.id() + << " remote ID:" << collection.remoteId() << endl + << " name:" << collection.name() << endl + << " url:" << collection.url() << endl + << " parent:" << collection.parentCollection().id() << collection.parentCollection().remoteId() << endl + << " resource:" << collection.resource() << endl + << " rights:" << collection.rights() << endl + << " contents mime type:" << collection.contentMimeTypes() << endl + << " isVirtual:" << collection.isVirtual() << endl + << " " << collection.cachePolicy() << endl + << " " << collection.statistics(); +} + +CollectionStatistics Collection::statistics() const +{ + return d_ptr->statistics; +} + +void Collection::setStatistics(const CollectionStatistics &statistics) +{ + d_ptr->statistics = statistics; +} + +CachePolicy Collection::cachePolicy() const +{ + return d_ptr->cachePolicy; +} + +void Collection::setCachePolicy(const CachePolicy &cachePolicy) +{ + d_ptr->cachePolicy = cachePolicy; + d_ptr->cachePolicyChanged = true; +} + +bool Collection::isVirtual() const +{ + return d_ptr->isVirtual; +} + +void Akonadi::Collection::setVirtual(bool isVirtual) +{ + d_ptr->isVirtual = isVirtual; +} + +void Collection::setEnabled(bool enabled) +{ + d_ptr->enabledChanged = true; + d_ptr->enabled = enabled; +} + +bool Collection::enabled() const +{ + return d_ptr->enabled; +} + +void Collection::setLocalListPreference(Collection::ListPurpose purpose, Collection::ListPreference preference) +{ + switch (purpose) { + case ListDisplay: + d_ptr->displayPreference = preference; + break; + case ListSync: + d_ptr->syncPreference = preference; + break; + case ListIndex: + d_ptr->indexPreference = preference; + break; + } + d_ptr->listPreferenceChanged = true; +} + +Collection::ListPreference Collection::localListPreference(Collection::ListPurpose purpose) const +{ + switch (purpose) { + case ListDisplay: + return d_ptr->displayPreference; + case ListSync: + return d_ptr->syncPreference; + case ListIndex: + return d_ptr->indexPreference; + } + return ListDefault; +} + +bool Collection::shouldList(Collection::ListPurpose purpose) const +{ + if (localListPreference(purpose) == ListDefault) { + return enabled() || referenced(); + } + return (localListPreference(purpose) == ListEnabled); +} + +void Collection::setShouldList(ListPurpose purpose, bool list) +{ + if (localListPreference(purpose) == ListDefault) { + setEnabled(list); + } else { + setLocalListPreference(purpose, list ? ListEnabled : ListDisabled); + } +} + +void Collection::setReferenced(bool referenced) +{ + d_ptr->referencedChanged = true; + d_ptr->referenced = referenced; +} + +bool Collection::referenced() const +{ + return d_ptr->referenced; +} + +void Collection::setKeepLocalChanges(const QSet &parts) +{ + d_ptr->keepLocalChanges = parts; +} + +QSet Collection::keepLocalChanges() const +{ + return d_ptr->keepLocalChanges; +} diff --git a/src/core/collection.h b/src/core/collection.h new file mode 100644 index 0000000..4bd50b5 --- /dev/null +++ b/src/core/collection.h @@ -0,0 +1,624 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_COLLECTION_H +#define AKONADI_COLLECTION_H + +#include "akonadicore_export.h" +#include "attribute.h" + +#include +#include +#include + +class QUrl; + +namespace Akonadi +{ + +class CachePolicy; +class CollectionPrivate; +class CollectionStatistics; + +/** + * @short Represents a collection of PIM items. + * + * This class represents a collection of PIM items, such as a folder on a mail- or + * groupware-server. + * + * Collections are hierarchical, i.e., they may have a parent collection. + * + * @code + * + * using namespace Akonadi; + * + * // fetching all collections recursive, starting at the root collection + * CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); + * connect( job, SIGNAL(result(KJob*)), SLOT(fetchFinished(KJob*)) ); + * + * ... + * + * MyClass::fetchFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * CollectionFetchJob *fetchJob = qobject_cast( job ); + * + * const Collection::List collections = fetchJob->collections(); + * foreach ( const Collection &collection, collections ) { + * qDebug() << "Name:" << collection.name(); + * } + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT Collection +{ +public: + /** + * Describes the unique id type. + */ + typedef qint64 Id; + + /** + * Describes a list of collections. + */ + typedef QVector List; + + /** + * Describes rights of a collection. + */ + enum Right { + ReadOnly = 0x0, ///< Can only read items or subcollection of this collection + CanChangeItem = 0x1, ///< Can change items in this collection + CanCreateItem = 0x2, ///< Can create new items in this collection + CanDeleteItem = 0x4, ///< Can delete items in this collection + CanChangeCollection = 0x8, ///< Can change this collection + CanCreateCollection = 0x10, ///< Can create new subcollections in this collection + CanDeleteCollection = 0x20, ///< Can delete this collection + CanLinkItem = 0x40, ///< Can create links to existing items in this virtual collection @since 4.4 + CanUnlinkItem = 0x80, ///< Can remove links to items in this virtual collection @since 4.4 + AllRights = (CanChangeItem | CanCreateItem | CanDeleteItem | + CanChangeCollection | CanCreateCollection | CanDeleteCollection) ///< Has all rights on this storage collection + }; + Q_DECLARE_FLAGS(Rights, Right) + + /** + * Creates an invalid collection. + */ + Collection(); + + /** + * Create a new collection. + * + * @param id The unique identifier of the collection. + */ + explicit Collection(Id id); + + /** + * Destroys the collection. + */ + ~Collection(); + + /** + * Creates a collection from an @p other collection. + */ + Collection(const Collection &other); + + /** + * Creates a collection from the given @p url. + */ + static Collection fromUrl(const QUrl &url); + + /** + * Sets the unique @p identifier of the collection. + */ + void setId(Id identifier); + + /** + * Returns the unique identifier of the collection. + */ + Id id() const; + + /** + * Sets the remote @p id of the collection. + */ + void setRemoteId(const QString &id); + + /** + * Returns the remote id of the collection. + */ + QString remoteId() const; + + /** + * Sets the remote @p revision of the collection. + * @param revision the collections's remote revision + * The remote revision can be used by resources to store some + * revision information of the backend to detect changes there. + * + * @note This method is supposed to be used by resources only. + * @since 4.5 + */ + void setRemoteRevision(const QString &revision); + + /** + * Returns the remote revision of the collection. + * + * @note This method is supposed to be used by resources only. + * @since 4.5 + */ + QString remoteRevision() const; + + /** + * Returns whether the collection is valid. + */ + bool isValid() const; + + /** + * Returns whether this collections's id equals the + * id of the @p other collection. + */ + bool operator==(const Collection &other) const; + + /** + * Returns whether the collection's id does not equal the id + * of the @p other collection. + */ + bool operator!=(const Collection &other) const; + + /** + * Assigns the @p other to this collection and returns a reference to this + * collection. + * @param other the collection to assign + */ + Collection &operator=(const Collection &other); + + /** + * @internal For use with containers only. + * + * @since 4.8 + */ + bool operator<(const Collection &other) const; + + /** + * Returns the parent collection of this object. + * @note This will of course only return a useful value if it was explictly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Collection parentCollection() const; + + /** + * Returns a reference to the parent collection of this object. + * @note This will of course only return a useful value if it was explictly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Collection &parentCollection(); + + /** + * Set the parent collection of this object. + * @note Calling this method has no immediate effect for the object itself, + * such as being moved to another collection. + * It is mainly relevant to provide a context for RID-based operations + * inside resources. + * @param parent The parent collection. + * @since 4.4 + */ + void setParentCollection(const Collection &parent); + + /** + * Adds an attribute to the collection. + * + * If an attribute of the same type name already exists, it is deleted and + * replaced with the new one. + * + * @param attribute The new attribute. + * + * @note The collection takes the ownership of the attribute. + */ + void addAttribute(Attribute *attribute); + + /** + * Removes and deletes the attribute of the given type @p name. + */ + void removeAttribute(const QByteArray &name); + + /** + * Returns @c true if the collection has an attribute of the given type @p name, + * false otherwise. + */ + bool hasAttribute(const QByteArray &name) const; + + /** + * Returns a list of all attributes of the collection. + */ + Attribute::List attributes() const; + + /** + * Removes and deletes all attributes of the collection. + */ + void clearAttributes(); + + /** + * Returns the attribute of the given type @p name if available, 0 otherwise. + */ + Attribute *attribute(const QByteArray &name) const; + + /** + * Describes the options that can be passed to access attributes. + */ + enum CreateOption { + AddIfMissing ///< Creates the attribute if it is missing + }; + + /** + * Returns the attribute of the requested type. + * If the collection has no attribute of that type yet, a new one + * is created and added to the entity. + * + * @param option The create options. + */ + template + inline T *attribute(CreateOption option); + + /** + * Returns the attribute of the requested type or 0 if it is not available. + */ + template + inline T *attribute() const; + + /** + * Removes and deletes the attribute of the requested type. + */ + template + inline void removeAttribute(); + + /** + * Returns whether the collection has an attribute of the requested type. + */ + template + inline bool hasAttribute() const; + + /** + * Returns the i18n'ed name of the collection. + */ + QString name() const; + + /** + * Returns the display name (EntityDisplayAttribute::displayName()) if set, + * and Collection::name() otherwise. For human-readable strings this is preferred + * over Collection::name(). + * + * @since 4.11 + */ + QString displayName() const; + + /** + * Sets the i18n'ed name of the collection. + * + * @param name The new collection name. + */ + void setName(const QString &name); + + /** + * Returns the rights the user has on the collection. + */ + Rights rights() const; + + /** + * Sets the @p rights the user has on the collection. + */ + void setRights(Rights rights); + + /** + * Returns a list of possible content mimetypes, + * e.g. message/rfc822, x-akonadi/collection for a mail folder that + * supports sub-folders. + */ + QStringList contentMimeTypes() const; + + /** + * Sets the list of possible content mime @p types. + */ + void setContentMimeTypes(const QStringList &types); + + /** + * Returns the root collection. + */ + static Collection root(); + + /** + * Returns the mimetype used for collections. + */ + static QString mimeType(); + + /** + * Returns the mimetype used for virtual collections + * + * @since 4.11 + */ + static QString virtualMimeType(); + + /** + * Returns the identifier of the resource owning the collection. + */ + QString resource() const; + + /** + * Sets the @p identifier of the resource owning the collection. + */ + void setResource(const QString &identifier); + + /** + * Returns the cache policy of the collection. + */ + CachePolicy cachePolicy() const; + + /** + * Sets the cache @p policy of the collection. + */ + void setCachePolicy(const CachePolicy &policy); + + /** + * Returns the collection statistics of the collection. + */ + CollectionStatistics statistics() const; + + /** + * Sets the collection @p statistics for the collection. + */ + void setStatistics(const CollectionStatistics &statistics); + + /** + * Describes the type of url which is returned in url(). + * + * @since 4.7 + */ + enum UrlType { + UrlShort = 0, ///< A short url which contains the identifier only (equivalent to url()) + UrlWithName = 1 ///< A url with identifier and name + }; + + /** + * Returns the url of the collection. + * @param type the type of url + * @since 4.7 + */ + QUrl url(UrlType type = UrlShort) const; + + /** + * Returns whether the collection is virtual, for example a search collection. + * + * @since 4.6 + */ + bool isVirtual() const; + + /** + * Sets whether the collection is virtual or not. + * Virtual collections can't be converted to non-virtual and vice versa. + * @param isVirtual virtual collection if @c true, otherwise a normal collection + * @since 4.10 + */ + void setVirtual(bool isVirtual); + + /** + * Sets the collection's enabled state. + * + * Use this mechanism to set if a collection should be available + * to the user or not. + * + * This can be used in conjunction with the local list preference for finer grained control + * to define if a collection should be included depending on the purpose. + * + * For example: A collection is by default enabled, meaning it is displayed to the user, synchronized by the resource, + * and indexed by the indexer. A disabled collection on the other hand is not displayed, sychronized or indexed. + * The local list preference allows to locally override that default value for each purpose individually. + * + * The enabled state can be synchronized by backends. + * E.g. an imap resource may synchronize this with the subscription state. + * + * @since 4.14 + * @see setLocalListPreference, setShouldList + */ + void setEnabled(bool enabled); + + /** + * Returns the collection's enabled state. + * @since 4.14 + * @see localListPreference + */ + bool enabled() const; + + /** + * Describes the list preference value + * + * @since 4.14 + */ + enum ListPreference { + ListEnabled, ///< Enable collection for specified purpose + ListDisabled, ///< Disable collectoin for specified purpose + ListDefault ///< Fallback to enabled state + }; + + /** + * Describes the purpose of the listing + * + * @since 4.14 + */ + enum ListPurpose { + ListSync, ///< Listing for synchronization + ListDisplay, ///< Listing for display to the user + ListIndex ///< Listing for indexing the content + }; + + /** + * Sets the local list preference for the specified purpose. + * + * The local list preference overrides the enabled state unless set to ListDefault. + * In case of ListDefault the enabled state should be taken as fallback (shouldList() implements this logic). + * + * The default value is ListDefault. + * + * @since 4.14 + * @see shouldList, setEnabled + */ + void setLocalListPreference(ListPurpose purpose, ListPreference preference); + + /** + * Returns the local list preference for the specified purpose. + * @since 4.14 + * @see setLocalListPreference + */ + ListPreference localListPreference(ListPurpose purpose) const; + + /** + * Returns whether the collection should be listed or not for the specified purpose + * Takes enabled state and local preference into account. + * + * @since 4.14 + * @see setLocalListPreference, setEnabled + */ + bool shouldList(ListPurpose purpose) const; + + /** + * Sets whether the collection should be listed or not for the specified purpose. + * Takes enabled state and local preference into account. + * + * Use this instead of sestEnabled and setLocalListPreference to automatically set + * the right setting. + * + * @since 4.14 + * @see setLocalListPreference, setEnabled + */ + void setShouldList(ListPurpose purpose, bool shouldList); + + /** + * Sets a collection to be referenced. + * + * A referenced collection is temporarily shown and synchronized even when disabled. + * A reference is only valid for the duration of a session, and is automatically removed afterwards. + * + * Referenced collections are only visible if explicitly monitored in the ETM. + * + * @since 4.14 + */ + void setReferenced(bool referenced); + + /** + * Returns the referenced state of the collection. + * @since 4.14 + */ + bool referenced() const; + + /** + * Set during sync to indicate that the provided parts are only default values; + * @since 4.15 + */ + void setKeepLocalChanges(const QSet &parts); + + /** + * Returns what parts are only default values. + */ + QSet keepLocalChanges() const; + +private: + friend class CollectionCreateJob; + friend class CollectionFetchJob; + friend class CollectionModifyJob; + friend class ProtocolHelper; + + //@cond PRIVATE + QSharedDataPointer d_ptr; + friend class CollectionPrivate; + //@endcond +}; + +AKONADICORE_EXPORT uint qHash(const Akonadi::Collection &collection); + +template +inline T *Akonadi::Collection::attribute(Collection::CreateOption option) +{ + Q_UNUSED(option); + + const T dummy; + if (hasAttribute(dummy.type())) { + T *attr = dynamic_cast(attribute(dummy.type())); + if (attr) { + return attr; + } + //Reuse 5250 + qWarning() << "Found attribute of unknown type" << dummy.type() + << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } + + T *attr = new T(); + addAttribute(attr); + return attr; +} + +template +inline T *Akonadi::Collection::attribute() const +{ + const T dummy; + if (hasAttribute(dummy.type())) { + T *attr = dynamic_cast(attribute(dummy.type())); + if (attr) { + return attr; + } + //reuse 5250 + qWarning() << "Found attribute of unknown type" << dummy.type() + << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } + + return 0; +} + +template +inline void Akonadi::Collection::removeAttribute() +{ + const T dummy; + removeAttribute(dummy.type()); +} + +template +inline bool Akonadi::Collection::hasAttribute() const +{ + const T dummy; + return hasAttribute(dummy.type()); +} + +} // namespace Akonadi + +/** + * Allows to output a collection for debugging purposes. + */ +AKONADICORE_EXPORT QDebug operator<<(QDebug d, const Akonadi::Collection &collection); + +Q_DECLARE_METATYPE(Akonadi::Collection) +Q_DECLARE_METATYPE(Akonadi::Collection::List) +Q_DECLARE_OPERATORS_FOR_FLAGS(Akonadi::Collection::Rights) +Q_DECLARE_TYPEINFO(Akonadi::Collection, Q_MOVABLE_TYPE); + +#endif diff --git a/src/core/collection_p.h b/src/core/collection_p.h new file mode 100644 index 0000000..716fb65 --- /dev/null +++ b/src/core/collection_p.h @@ -0,0 +1,143 @@ +/* + Copyright (c) 2006 - 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_COLLECTION_P_H +#define AKONADI_COLLECTION_P_H + +#include "collection.h" +#include "cachepolicy.h" +#include "collectionstatistics.h" + +#include "qstringlist.h" + +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Akonadi::CollectionPrivate : public QSharedData +{ +public: + CollectionPrivate(Collection::Id id = -1) + : QSharedData() + , displayPreference(Collection::ListDefault) + , syncPreference(Collection::ListDefault) + , indexPreference(Collection::ListDefault) + , listPreferenceChanged(false) + , enabled(true) + , enabledChanged(false) + , referenced(false) + , referencedChanged(false) + , contentTypesChanged(false) + , cachePolicyChanged(false) + , isVirtual(false) + , mId(id) + , mParent(Q_NULLPTR) + { + } + + CollectionPrivate(const CollectionPrivate &other) + : QSharedData(other) + , mParent(Q_NULLPTR) + { + mId = other.mId; + mRemoteId = other.mRemoteId; + mRemoteRevision = other.mRemoteRevision; + Q_FOREACH (Attribute *attr, other.mAttributes) { + mAttributes.insert(attr->type(), attr->clone()); + } + mDeletedAttributes = other.mDeletedAttributes; + if (other.mParent) { + mParent = new Collection(*(other.mParent)); + } + name = other.name; + resource = other.resource; + statistics = other.statistics; + contentTypes = other.contentTypes; + cachePolicy = other.cachePolicy; + contentTypesChanged = other.contentTypesChanged; + cachePolicyChanged = other.cachePolicyChanged; + isVirtual = other.isVirtual; + enabled = other.enabled; + enabledChanged = other.enabledChanged; + displayPreference = other.displayPreference; + syncPreference = other.syncPreference; + indexPreference = other.indexPreference; + listPreferenceChanged = other.listPreferenceChanged; + referenced = other.referenced; + referencedChanged = other.referencedChanged; + keepLocalChanges = other.keepLocalChanges; + } + + ~CollectionPrivate() + { + qDeleteAll(mAttributes); + delete mParent; + } + + void resetChangeLog() + { + contentTypesChanged = false; + cachePolicyChanged = false; + enabledChanged = false; + listPreferenceChanged = false; + referencedChanged = false; + mDeletedAttributes.clear(); + } + + static Collection newRoot() + { + Collection rootCollection(0); + rootCollection.setContentMimeTypes({ Collection::mimeType() }); + return rootCollection; + } + + // Make use of the 4-bytes padding from QSharedData + Collection::ListPreference displayPreference: 2; + Collection::ListPreference syncPreference: 2; + Collection::ListPreference indexPreference: 2; + bool listPreferenceChanged: 1; + bool enabled: 1; + bool enabledChanged: 1; + bool referenced: 1; + bool referencedChanged: 1; + bool contentTypesChanged: 1; + bool cachePolicyChanged: 1; + bool isVirtual: 1; + // 2 bytes padding here + + Collection::Id mId; + QString mRemoteId; + QString mRemoteRevision; + QHash mAttributes; + QSet mDeletedAttributes; + mutable Collection *mParent; + QString name; + QString resource; + CollectionStatistics statistics; + QStringList contentTypes; + static const Collection root; + CachePolicy cachePolicy; + QSet keepLocalChanges; + +}; + +#endif diff --git a/src/core/collectionfetchscope.cpp b/src/core/collectionfetchscope.cpp new file mode 100644 index 0000000..da2beff --- /dev/null +++ b/src/core/collectionfetchscope.cpp @@ -0,0 +1,210 @@ +/* + Copyright (c) 2008 Kevin Krammer + 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 "collectionfetchscope.h" + +#include +#include +#include + +namespace Akonadi +{ + +class CollectionFetchScopePrivate : public QSharedData +{ +public: + CollectionFetchScopePrivate() + : ancestorDepth(CollectionFetchScope::None) + , statistics(false) + , listFilter(CollectionFetchScope::Enabled) + , fetchAllAttributes(false) + , fetchIdOnly(true) + , mIgnoreRetrievalErrors(false) + { + } + + CollectionFetchScopePrivate(const CollectionFetchScopePrivate &other) + : QSharedData(other) + { + resource = other.resource; + contentMimeTypes = other.contentMimeTypes; + ancestorDepth = other.ancestorDepth; + statistics = other.statistics; + listFilter = other.listFilter; + attributes = other.attributes; + if (!ancestorFetchScope && other.ancestorFetchScope) { + ancestorFetchScope.reset(new CollectionFetchScope()); + *ancestorFetchScope = *other.ancestorFetchScope; + } else if (ancestorFetchScope && !other.ancestorFetchScope) { + ancestorFetchScope.reset(0); + } + fetchAllAttributes = other.fetchAllAttributes; + fetchIdOnly = other.fetchIdOnly; + mIgnoreRetrievalErrors = other.mIgnoreRetrievalErrors; + } + +public: + QString resource; + QStringList contentMimeTypes; + CollectionFetchScope::AncestorRetrieval ancestorDepth; + bool statistics; + CollectionFetchScope::ListFilter listFilter; + QSet attributes; + QScopedPointer ancestorFetchScope; + bool fetchAllAttributes; + bool fetchIdOnly; + bool mIgnoreRetrievalErrors; +}; + +CollectionFetchScope::CollectionFetchScope() +{ + d = new CollectionFetchScopePrivate(); +} + +CollectionFetchScope::CollectionFetchScope(const CollectionFetchScope &other) + : d(other.d) +{ +} + +CollectionFetchScope::~CollectionFetchScope() +{ +} + +CollectionFetchScope &CollectionFetchScope::operator=(const CollectionFetchScope &other) +{ + if (&other != this) { + d = other.d; + } + + return *this; +} + +bool CollectionFetchScope::isEmpty() const +{ + return d->resource.isEmpty() && d->contentMimeTypes.isEmpty() && !d->statistics && d->ancestorDepth == None && d->listFilter == Enabled; +} + +bool CollectionFetchScope::includeStatistics() const +{ + return d->statistics; +} + +void CollectionFetchScope::setIncludeStatistics(bool include) +{ + d->statistics = include; +} + +QString CollectionFetchScope::resource() const +{ + return d->resource; +} + +void CollectionFetchScope::setResource(const QString &resource) +{ + d->resource = resource; +} + +QStringList CollectionFetchScope::contentMimeTypes() const +{ + return d->contentMimeTypes; +} + +void CollectionFetchScope::setContentMimeTypes(const QStringList &mimeTypes) +{ + d->contentMimeTypes = mimeTypes; +} + +CollectionFetchScope::AncestorRetrieval CollectionFetchScope::ancestorRetrieval() const +{ + return d->ancestorDepth; +} + +void CollectionFetchScope::setAncestorRetrieval(AncestorRetrieval ancestorDepth) +{ + d->ancestorDepth = ancestorDepth; +} + +CollectionFetchScope::ListFilter CollectionFetchScope::listFilter() const +{ + return d->listFilter; +} + +void CollectionFetchScope::setListFilter(CollectionFetchScope::ListFilter listFilter) +{ + d->listFilter = listFilter; +} + +QSet CollectionFetchScope::attributes() const +{ + return d->attributes; +} + +void CollectionFetchScope::fetchAttribute(const QByteArray &type, bool fetch) +{ + d->fetchIdOnly = false; + if (fetch) { + d->attributes.insert(type); + } else { + d->attributes.remove(type); + } +} + +void CollectionFetchScope::setFetchIdOnly(bool fetchIdOnly) +{ + d->fetchIdOnly = fetchIdOnly; +} + +bool CollectionFetchScope::fetchIdOnly() const +{ + return d->fetchIdOnly; +} + +void CollectionFetchScope::setIgnoreRetrievalErrors(bool enable) +{ + d->mIgnoreRetrievalErrors = enable; +} + +bool CollectionFetchScope::ignoreRetrievalErrors() const +{ + return d->mIgnoreRetrievalErrors; +} + +void CollectionFetchScope::setAncestorFetchScope(const CollectionFetchScope &scope) +{ + *d->ancestorFetchScope = scope; +} + +CollectionFetchScope CollectionFetchScope::ancestorFetchScope() const +{ + if (!d->ancestorFetchScope) { + return CollectionFetchScope(); + } + return *d->ancestorFetchScope; +} + +CollectionFetchScope &CollectionFetchScope::ancestorFetchScope() +{ + if (!d->ancestorFetchScope) { + d->ancestorFetchScope.reset(new CollectionFetchScope()); + } + return *d->ancestorFetchScope; +} + +} diff --git a/src/core/collectionfetchscope.h b/src/core/collectionfetchscope.h new file mode 100644 index 0000000..3de2dc0 --- /dev/null +++ b/src/core/collectionfetchscope.h @@ -0,0 +1,289 @@ +/* + Copyright (c) 2008 Kevin Krammer + 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. +*/ + +#ifndef AKONADI_COLLECTIONFETCHSCOPE_H +#define AKONADI_COLLECTIONFETCHSCOPE_H + +#include "akonadicore_export.h" + +#include +#include + +class QStringList; + +namespace Akonadi +{ + +class CollectionFetchScopePrivate; + +/** + * @short Specifies which parts of a collection should be fetched from the Akonadi storage. + * + * When collections are fetched from the server either by using CollectionFetchJob + * explicitly or when it is being used internally by other classes, e.g. Akonadi::Monitor, + * the scope of the fetch operation can be tailored to the application's current needs. + * + * Note that CollectionFetchScope always includes fetching collection attributes. + * + * There are two supported ways of changing the currently active CollectionFetchScope + * of classes: + * - in-place: modify the CollectionFetchScope object the other class holds as a member + * - replace: replace the other class' member with a new scope object + * + * Example: modifying a CollectionFetchJob's scope @c in-place + * @code + * Akonadi::CollectionFetchJob *job = new Akonadi::CollectionFetchJob( collection ); + * job->fetchScope().setIncludeUnsubscribed( true ); + * @endcode + * + * Example: @c replacing a CollectionFetchJob's scope + * @code + * Akonadi::CollectionFetchScope scope; + * scope.setIncludeUnsubscribed( true ); + * + * Akonadi::CollectionFetchJob *job = new Akonadi::CollectionFetchJob( collection ); + * job->setFetchScope( scope ); + * @endcode + * + * This class is implicitly shared. + * + * @author Volker Krause + * @since 4.4 + */ +class AKONADICORE_EXPORT CollectionFetchScope +{ +public: + /** + * Describes the ancestor retrieval depth. + */ + enum AncestorRetrieval { + None, ///< No ancestor retrieval at all (the default) + Parent, ///< Only retrieve the immediate parent collection + All ///< Retrieve all ancestors, up to Collection::root() + }; + + /** + * Creates an empty collection fetch scope. + * + * Using an empty scope will only fetch the very basic meta data of collections, + * e.g. local id, remote id and content mimetypes. + */ + CollectionFetchScope(); + + /** + * Creates a new collection fetch scope from an @p other. + */ + CollectionFetchScope(const CollectionFetchScope &other); + + /** + * Destroys the collection fetch scope. + */ + ~CollectionFetchScope(); + + /** + * Assigns the @p other to this scope and returns a reference to this scope. + */ + CollectionFetchScope &operator=(const CollectionFetchScope &other); + + /** + * Describes the list filter + * + * @since 4.14 + */ + enum ListFilter { + NoFilter, ///< No filtering, retrieve all collections + Display, ///< Only retrieve collections for display, taking the local preference and enabled into account. + Sync, ///< Only retrieve collections for synchronization, taking the local preference and enabled into account. + Index, ///< Only retrieve collections for indxing, taking the local preference and enabled into account. + Enabled ///< Only retrieve enabled collections, ignoring the local preference. + }; + + /** + * Sets a filter for the collections to be listed. + * + * Note that collections that do not match the filter are included if required to complete the tree. + * + * @since 4.14 + */ + void setListFilter(ListFilter); + + /** + * Returns the list filter. + * + * @see setListFilter() + * @since 4.14 + */ + ListFilter listFilter() const; + + /** + * Returns whether collection statistics should be included in the retrieved results. + * + * @see setIncludeStatistics() + */ + bool includeStatistics() const; + + /** + * Sets whether collection statistics should be included in the retrieved results. + * + * @param include @c true to include collction statistics, @c false otherwise (the default). + */ + void setIncludeStatistics(bool include); + + /** + * Returns the resource identifier that is used as filter. + * + * @see setResource() + */ + QString resource() const; + + /** + * Sets a resource filter, that is only collections owned by the specified resource are + * retrieved. + * + * @param resource The resource identifier. + */ + void setResource(const QString &resource); + + /** + * Sets a content mimetypes filter, that is only collections that contain at least one of the + * given mimetypes (or their parents) are retrieved. + * + * @param mimeTypes A list of mime types + */ + void setContentMimeTypes(const QStringList &mimeTypes); + + /** + * Returns the content mimetypes filter. + * + * @see setContentMimeTypes() + */ + QStringList contentMimeTypes() const; + + /** + * Sets how many levels of ancestor collections should be included in the retrieval. + * + * Only the ID and the remote ID of the ancestor collections are fetched. If + * you want more information about the ancestor collections, like their name, + * you will need to do an additional CollectionFetchJob for them. + * + * @param ancestorDepth The desired ancestor retrieval depth. + */ + void setAncestorRetrieval(AncestorRetrieval ancestorDepth); + + /** + * Returns the ancestor retrieval depth. + * + * @see setAncestorRetrieval() + */ + AncestorRetrieval ancestorRetrieval() const; + + /** + * Sets the fetch scope for ancestor retrieval. + * + * @see setAncestorRetrieval() + */ + void setAncestorFetchScope(const CollectionFetchScope &scope); + + /** + * Returns the fetch scope for ancestor retrieval. + */ + CollectionFetchScope ancestorFetchScope() const; + + /** + * Returns the fetch scope for ancestor retrieval. + */ + CollectionFetchScope &ancestorFetchScope(); + + /** + * Returns all explicitly fetched attributes. + * + * Undefined if fetchAllAttributes() returns true. + * + * @see fetchAttribute() + */ + QSet attributes() const; + + /** + * Sets whether the attribute of the given @p type should be fetched. + * + * @param type The attribute type to fetch. + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + void fetchAttribute(const QByteArray &type, bool fetch = true); + + /** + * Sets whether the attribute of the requested type should be fetched. + * + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + template inline void fetchAttribute(bool fetch = true) + { + T dummy; + fetchAttribute(dummy.type(), fetch); + } + + /** + * Sets whether only the id or the complete tag should be fetched. + * + * The default is @c false. + * + * @since 4.15 + */ + void setFetchIdOnly(bool fetchIdOnly); + + /** + * Sets whether only the id of the tags should be retieved or the complete tag. + * + * @see tagFetchScope() + * @since 4.15 + */ + bool fetchIdOnly() const; + + /** + * Ignore retrieval errors while fetching collections, and always deliver what is available. + * + * This flag is useful to fetch a list of collections, where some might no longer be available. + * + * @since KF5 + */ + void setIgnoreRetrievalErrors(bool enabled); + + /** + * Returns whether retrieval errors should be ignored. + * + * @see setIgnoreRetrievalErrors() + * @since KF5 + */ + bool ignoreRetrievalErrors() const; + + /** + * Returns @c true if there is nothing to fetch. + */ + bool isEmpty() const; + +private: + //@cond PRIVATE + QSharedDataPointer d; + //@endcond +}; + +} + +#endif diff --git a/src/core/collectionidentificationattribute.cpp b/src/core/collectionidentificationattribute.cpp new file mode 100644 index 0000000..bfe184f --- /dev/null +++ b/src/core/collectionidentificationattribute.cpp @@ -0,0 +1,147 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "collectionidentificationattribute.h" + +#include + +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN CollectionIdentificationAttribute::Private +{ +public: + Private() + { + } + QByteArray mFolderNamespace; + QByteArray mIdentifier; + QByteArray mName; + QByteArray mOrganizationUnit; + QByteArray mMail; +}; + +CollectionIdentificationAttribute::CollectionIdentificationAttribute(const QByteArray &identifier, const QByteArray &folderNamespace, + const QByteArray &name, const QByteArray &organizationUnit, const QByteArray &mail) + : d(new Private) +{ + d->mIdentifier = identifier; + d->mFolderNamespace = folderNamespace; + d->mName = name; + d->mOrganizationUnit = organizationUnit; + d->mMail = mail; +} + +CollectionIdentificationAttribute::~CollectionIdentificationAttribute() +{ + delete d; +} + +void CollectionIdentificationAttribute::setIdentifier(const QByteArray &identifier) +{ + d->mIdentifier = identifier; +} + +QByteArray CollectionIdentificationAttribute::identifier() const +{ + return d->mIdentifier; +} + +void CollectionIdentificationAttribute::setMail(const QByteArray &mail) +{ + d->mMail = mail; +} + +QByteArray CollectionIdentificationAttribute::mail() const +{ + return d->mMail; +} + +void CollectionIdentificationAttribute::setOu(const QByteArray &ou) +{ + d->mOrganizationUnit = ou; +} + +QByteArray CollectionIdentificationAttribute::ou() const +{ + return d->mOrganizationUnit; +} + +void CollectionIdentificationAttribute::setName(const QByteArray &name) +{ + d->mName = name; +} + +QByteArray CollectionIdentificationAttribute::name() const +{ + return d->mName; +} + +void CollectionIdentificationAttribute::setCollectionNamespace(const QByteArray &ns) +{ + d->mFolderNamespace = ns; +} + +QByteArray CollectionIdentificationAttribute::collectionNamespace() const +{ + return d->mFolderNamespace; +} + +QByteArray CollectionIdentificationAttribute::type() const +{ + return "collectionidentification"; +} + +Akonadi::Attribute *CollectionIdentificationAttribute::clone() const +{ + return new CollectionIdentificationAttribute(d->mIdentifier, d->mFolderNamespace, d->mName, d->mOrganizationUnit, d->mMail); +} + +QByteArray CollectionIdentificationAttribute::serialized() const +{ + QList l; + l << Akonadi::ImapParser::quote(d->mIdentifier); + l << Akonadi::ImapParser::quote(d->mFolderNamespace); + l << Akonadi::ImapParser::quote(d->mName); + l << Akonadi::ImapParser::quote(d->mOrganizationUnit); + l << Akonadi::ImapParser::quote(d->mMail); + return '(' + Akonadi::ImapParser::join(l, " ") + ')'; +} + +void CollectionIdentificationAttribute::deserialize(const QByteArray &data) +{ + QList l; + Akonadi::ImapParser::parseParenthesizedList(data, l); + const int size = l.size(); + Q_ASSERT(size >= 2); + if (size < 2) { + return; + } + d->mIdentifier = l[0]; + d->mFolderNamespace = l[1]; + + if (size == 5) { + d->mName = l[2]; + d->mOrganizationUnit = l[3]; + d->mMail = l[4]; + } +} + diff --git a/src/core/collectionidentificationattribute.h b/src/core/collectionidentificationattribute.h new file mode 100644 index 0000000..91574ec --- /dev/null +++ b/src/core/collectionidentificationattribute.h @@ -0,0 +1,81 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 COLLECTIONIDENTIFICATIONATTRIBUTE_H +#define COLLECTIONIDENTIFICATIONATTRIBUTE_H + +#include +#include + +namespace Akonadi { + +/** + * @short Attribute that stores additional information on a collection that can be used for searching. + * + * Additional indexed properties that can be used for searching. + * + * @author Christian Mollekopf + * @since 4.15 + */ +class AKONADICORE_EXPORT CollectionIdentificationAttribute : public Akonadi::Attribute +{ +public: + explicit CollectionIdentificationAttribute(const QByteArray &identifier = QByteArray(), const QByteArray &folderNamespace = QByteArray(), + const QByteArray &name = QByteArray(), const QByteArray &organizationUnit = QByteArray(), const QByteArray &mail = QByteArray()); + ~CollectionIdentificationAttribute(); + + /** + * Sets an identifier for the collection. + */ + void setIdentifier(const QByteArray &identifier); + QByteArray identifier() const; + + void setMail(const QByteArray &); + QByteArray mail() const; + + void setOu(const QByteArray &); + QByteArray ou() const; + + void setName(const QByteArray &); + QByteArray name() const; + + /** + * Sets a namespace the collection is in. + * + * Initially used are: + * * "person" for a collection shared by a person. + * * "shared" for a collection shared by a person. + */ + void setCollectionNamespace(const QByteArray &ns); + QByteArray collectionNamespace() const; + QByteArray type() const Q_DECL_OVERRIDE; + Attribute *clone() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/collectionpathresolver.cpp b/src/core/collectionpathresolver.cpp new file mode 100644 index 0000000..f1d6f32 --- /dev/null +++ b/src/core/collectionpathresolver.cpp @@ -0,0 +1,229 @@ +/* + 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 "collectionpathresolver.h" + +#include "collectionfetchjob.h" +#include "job_p.h" + +#include "akonadicore_debug.h" + +#include + +#include + +using namespace Akonadi; + +//@cond PRIVATE + +class Akonadi::CollectionPathResolverPrivate : public JobPrivate +{ +public: + CollectionPathResolverPrivate(CollectionPathResolver *parent) + : JobPrivate(parent) + , mColId(-1) + { + } + + void init(const QString &path, const Collection &rootCollection) + { + Q_Q(CollectionPathResolver); + + mPathToId = true; + mPath = path; + if (mPath.startsWith(q->pathDelimiter())) { + mPath = mPath.right(mPath.length() - q->pathDelimiter().length()); + } + if (mPath.endsWith(q->pathDelimiter())) { + mPath = mPath.left(mPath.length() - q->pathDelimiter().length()); + } + + mPathParts = splitPath(mPath); + mCurrentNode = rootCollection; + } + + void jobResult(KJob *job); + + QStringList splitPath(const QString &path) + { + if (path.isEmpty()) { // path is normalized, so non-empty means at least one hit + return QStringList(); + } + + QStringList rv; + int begin = 0; + const int pathSize(path.size()); + for (int i = 0; i < pathSize; ++i) { + if (path[i] == QLatin1Char('/')) { + QString pathElement = path.mid(begin, i - begin); + pathElement = pathElement.replace(QStringLiteral("\\/"), QStringLiteral("/")); + rv.append(pathElement); + begin = i + 1; + } + if (i < path.size() - 2 && path[i] == QLatin1Char('\\') && path[i + 1] == QLatin1Char('/')) { + ++i; + } + } + QString pathElement = path.mid(begin); + pathElement = pathElement.replace(QStringLiteral("\\/"), QStringLiteral("/")); + rv.append(pathElement); + return rv; + } + + Q_DECLARE_PUBLIC(CollectionPathResolver) + + Collection::Id mColId; + QString mPath; + bool mPathToId; + QStringList mPathParts; + Collection mCurrentNode; +}; + +void CollectionPathResolverPrivate::jobResult(KJob *job) +{ + if (job->error()) { + return; + } + + Q_Q(CollectionPathResolver); + + CollectionFetchJob *list = static_cast(job); + CollectionFetchJob *nextJob = Q_NULLPTR; + const Collection::List cols = list->collections(); + if (cols.isEmpty()) { + mColId = -1; + q->setError(CollectionPathResolver::Unknown); + q->setErrorText(i18n("No such collection.")); + q->emitResult(); + return; + } + + if (mPathToId) { + const QString currentPart = mPathParts.takeFirst(); + bool found = false; + foreach (const Collection &c, cols) { + if (c.name() == currentPart) { + mCurrentNode = c; + found = true; + break; + } + } + if (!found) { + qCWarning(AKONADICORE_LOG) << "No such collection" << currentPart << "with parent" << mCurrentNode.id(); + mColId = -1; + q->setError(CollectionPathResolver::Unknown); + q->setErrorText(i18n("No such collection.")); + q->emitResult(); + return; + } + if (mPathParts.isEmpty()) { + mColId = mCurrentNode.id(); + q->emitResult(); + return; + } + nextJob = new CollectionFetchJob(mCurrentNode, CollectionFetchJob::FirstLevel, q); + } else { + Collection col = list->collections().at(0); + mCurrentNode = col.parentCollection(); + mPathParts.prepend(col.name()); + if (mCurrentNode == Collection::root()) { + q->emitResult(); + return; + } + nextJob = new CollectionFetchJob(mCurrentNode, CollectionFetchJob::Base, q); + } + q->connect(nextJob, SIGNAL(result(KJob*)), q, SLOT(jobResult(KJob*))); +} + +CollectionPathResolver::CollectionPathResolver(const QString &path, QObject *parent) + : Job(new CollectionPathResolverPrivate(this), parent) +{ + Q_D(CollectionPathResolver); + d->init(path, Collection::root()); +} + +CollectionPathResolver::CollectionPathResolver(const QString &path, const Collection &parentCollection, QObject *parent) + : Job(new CollectionPathResolverPrivate(this), parent) +{ + Q_D(CollectionPathResolver); + d->init(path, parentCollection); +} + +CollectionPathResolver::CollectionPathResolver(const Collection &collection, QObject *parent) + : Job(new CollectionPathResolverPrivate(this), parent) +{ + Q_D(CollectionPathResolver); + + d->mPathToId = false; + d->mColId = collection.id(); + d->mCurrentNode = collection; +} + +CollectionPathResolver::~CollectionPathResolver() +{ +} + +Collection::Id CollectionPathResolver::collection() const +{ + Q_D(const CollectionPathResolver); + + return d->mColId; +} + +QString CollectionPathResolver::path() const +{ + Q_D(const CollectionPathResolver); + + if (d->mPathToId) { + return d->mPath; + } + return d->mPathParts.join(pathDelimiter()); +} + +QString CollectionPathResolver::pathDelimiter() +{ + return QStringLiteral("/"); +} + +void CollectionPathResolver::doStart() +{ + Q_D(CollectionPathResolver); + + CollectionFetchJob *job = Q_NULLPTR; + if (d->mPathToId) { + if (d->mPath.isEmpty()) { + d->mColId = Collection::root().id(); + emitResult(); + return; + } + job = new CollectionFetchJob(d->mCurrentNode, CollectionFetchJob::FirstLevel, this); + } else { + if (d->mColId == 0) { + d->mColId = Collection::root().id(); + emitResult(); + return; + } + job = new CollectionFetchJob(d->mCurrentNode, CollectionFetchJob::Base, this); + } + connect(job, SIGNAL(result(KJob*)), SLOT(jobResult(KJob*))); +} + +//@endcond + +#include "moc_collectionpathresolver.cpp" diff --git a/src/core/collectionpathresolver.h b/src/core/collectionpathresolver.h new file mode 100644 index 0000000..7367510 --- /dev/null +++ b/src/core/collectionpathresolver.h @@ -0,0 +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_COLLECTIONPATHRESOLVER_P_H +#define AKONADI_COLLECTIONPATHRESOLVER_P_H + +#include "akonadicore_export.h" +#include "collection.h" +#include "job.h" + +namespace Akonadi +{ + +class CollectionPathResolverPrivate; + +/** + * @internal + * + * Converts between collection id and collection path. + * + * While it is generally recommended to use collection ids, it can + * be necessary in some cases (eg. a command line client) to use the + * collection path instead. Use this class to get a collection id + * from a collection path. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionPathResolver : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection path resolver to convert a path into a id. + * + * Equivalent to calling CollectionPathResolver(path, Collection:root(), parent) + * + * @param path The collection path. + * @param parent The parent object. + */ + explicit CollectionPathResolver(const QString &path, QObject *parent = Q_NULLPTR); + + /** + * Create a new collection path resolver to convert a path into an id. + * + * The @p path is resolved relatively to @p parentCollection. This can be + * useful for resource, which now the root collection. + * + * @param path The collection path. + * @param parentCollection Collection relatively to which the path will be resolved. + * @param parent The parent object. + * + * @since 4.14 + */ + explicit CollectionPathResolver(const QString &path, const Collection &parentCollection, QObject *parent = Q_NULLPTR); + + /** + * Creates a new collection path resolver to determine the path of + * the given collection. + * + * @param collection The collection. + * @param parent The parent object. + */ + explicit CollectionPathResolver(const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Destroys the collection path resolver. + */ + ~CollectionPathResolver(); + + /** + * Returns the collection id. Only valid after the job succeeded. + */ + Collection::Id collection() const; + + /** + * Returns the collection path. Only valid after the job succeeded. + */ + QString path() const; + + /** + * Returns the path delimiter for collections. + */ + static QString pathDelimiter(); + +protected: + void doStart() Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(CollectionPathResolver) + + //@cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void jobResult(KJob *)) + //@endcond +}; + +} + +#endif diff --git a/src/core/collectionquotaattribute.cpp b/src/core/collectionquotaattribute.cpp new file mode 100644 index 0000000..68f1ed8 --- /dev/null +++ b/src/core/collectionquotaattribute.cpp @@ -0,0 +1,110 @@ +/* + Copyright (C) 2009 Kevin Ottens + + 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 "collectionquotaattribute.h" + +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN CollectionQuotaAttribute::Private +{ +public: + Private(qint64 currentValue, qint64 maxValue) + : mCurrentValue(currentValue) + , mMaximumValue(maxValue) + { + } + + qint64 mCurrentValue; + qint64 mMaximumValue; +}; + +CollectionQuotaAttribute::CollectionQuotaAttribute() + : d(new Private(-1, -1)) +{ +} + +CollectionQuotaAttribute::CollectionQuotaAttribute(qint64 currentValue, qint64 maxValue) + : d(new Private(currentValue, maxValue)) +{ +} + +CollectionQuotaAttribute::~CollectionQuotaAttribute() +{ + delete d; +} + +void CollectionQuotaAttribute::setCurrentValue(qint64 value) +{ + d->mCurrentValue = value; +} + +void CollectionQuotaAttribute::setMaximumValue(qint64 value) +{ + d->mMaximumValue = value; +} + +qint64 CollectionQuotaAttribute::currentValue() const +{ + return d->mCurrentValue; +} + +qint64 CollectionQuotaAttribute::maximumValue() const +{ + return d->mMaximumValue; +} + +QByteArray CollectionQuotaAttribute::type() const +{ + static const QByteArray sType("collectionquota"); + return sType; +} + +Akonadi::Attribute *CollectionQuotaAttribute::clone() const +{ + return new CollectionQuotaAttribute(d->mCurrentValue, d->mMaximumValue); +} + +QByteArray CollectionQuotaAttribute::serialized() const +{ + return QByteArray::number(d->mCurrentValue) + + ' ' + + QByteArray::number(d->mMaximumValue); +} + +void CollectionQuotaAttribute::deserialize(const QByteArray &data) +{ + d->mCurrentValue = -1; + d->mMaximumValue = -1; + + const QList items = data.simplified().split(' '); + + if (items.isEmpty()) { + return; + } + + d->mCurrentValue = items[0].toLongLong(); + + if (items.size() < 2) { + return; + } + + d->mMaximumValue = items[1].toLongLong(); +} diff --git a/src/core/collectionquotaattribute.h b/src/core/collectionquotaattribute.h new file mode 100644 index 0000000..008164a --- /dev/null +++ b/src/core/collectionquotaattribute.h @@ -0,0 +1,111 @@ +/* + Copyright (C) 2009 Kevin Ottens + + 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_COLLECTIONQUOTAATTRIBUTE_H +#define AKONADI_COLLECTIONQUOTAATTRIBUTE_H + +#include "akonadicore_export.h" +#include "attribute.h" + +namespace Akonadi +{ + +/** + * @short Attribute that provides quota information for a collection. + * + * This attribute class provides quota information (e.g. current fill value + * and maximum fill value) for an Akonadi collection. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * const Collection collection = collectionFetchJob->collections().at(0); + * if ( collection.hasAttribute() ) { + * const CollectionQuotaAttribute *attribute = collection.attribute(); + * qDebug() << "current value" << attribute->currentValue(); + * } + * + * @endcode + * + * @author Kevin Ottens + * @since 4.4 + */ +class AKONADICORE_EXPORT CollectionQuotaAttribute : public Akonadi::Attribute +{ +public: + /** + * Creates a new collection quota attribute. + */ + CollectionQuotaAttribute(); + + /** + * Creates a new collection quota attribute with initial values. + * + * @param currentValue The current quota value in bytes. + * @param maxValue The maximum quota value in bytes. + */ + CollectionQuotaAttribute(qint64 currentValue, qint64 maxValue); + + /** + * Destroys the collection quota attribute. + */ + ~CollectionQuotaAttribute(); + + /** + * Sets the current quota @p value for the collection. + * + * @param value The current quota value in bytes. + */ + void setCurrentValue(qint64 value); + + /** + * Sets the maximum quota @p value for the collection. + * + * @param value The maximum quota value in bytes. + */ + void setMaximumValue(qint64 value); + + /** + * Returns the current quota value in bytes. + */ + qint64 currentValue() const; + + /** + * Returns the maximum quota value in bytes. + */ + qint64 maximumValue() const; + + QByteArray type() const Q_DECL_OVERRIDE; + Attribute *clone() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/collectionrightsattribute.cpp b/src/core/collectionrightsattribute.cpp new file mode 100644 index 0000000..1ab5594 --- /dev/null +++ b/src/core/collectionrightsattribute.cpp @@ -0,0 +1,155 @@ +/* + Copyright (c) 2007 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. +*/ + +#include "collectionrightsattribute_p.h" + +using namespace Akonadi; + +static Collection::Rights dataToRights(const QByteArray &data) +{ + Collection::Rights rights = Collection::ReadOnly; + + if (data.isEmpty()) { + return Collection::ReadOnly; + } + + if (data.at(0) == 'a') { + return Collection::AllRights; + } + + for (int i = 0; i < data.count(); ++i) { + switch (data.at(i)) { + case 'w': + rights |= Collection::CanChangeItem; + break; + case 'c': + rights |= Collection::CanCreateItem; + break; + case 'd': + rights |= Collection::CanDeleteItem; + break; + case 'l': + rights |= Collection::CanLinkItem; + break; + case 'u': + rights |= Collection::CanUnlinkItem; + break; + case 'W': + rights |= Collection::CanChangeCollection; + break; + case 'C': + rights |= Collection::CanCreateCollection; + break; + case 'D': + rights |= Collection::CanDeleteCollection; + break; + } + } + + return rights; +} + +static QByteArray rightsToData(Collection::Rights &rights) +{ + if (rights == Collection::AllRights) { + return QByteArray("a"); + } + + QByteArray data; + if (rights & Collection::CanChangeItem) { + data.append('w'); + } + if (rights & Collection::CanCreateItem) { + data.append('c'); + } + if (rights & Collection::CanDeleteItem) { + data.append('d'); + } + if (rights & Collection::CanChangeCollection) { + data.append('W'); + } + if (rights & Collection::CanCreateCollection) { + data.append('C'); + } + if (rights & Collection::CanDeleteCollection) { + data.append('D'); + } + if (rights & Collection::CanLinkItem) { + data.append('l'); + } + if (rights & Collection::CanUnlinkItem) { + data.append('u'); + } + + return data; +} + +/** + * @internal + */ +class CollectionRightsAttribute::Private +{ +public: + QByteArray mData; +}; + +CollectionRightsAttribute::CollectionRightsAttribute() + : Attribute() + , d(new Private) +{ +} + +CollectionRightsAttribute::~CollectionRightsAttribute() +{ + delete d; +} + +void CollectionRightsAttribute::setRights(Collection::Rights rights) +{ + d->mData = rightsToData(rights); +} + +Collection::Rights CollectionRightsAttribute::rights() const +{ + return dataToRights(d->mData); +} + +CollectionRightsAttribute *CollectionRightsAttribute::clone() const +{ + CollectionRightsAttribute *attr = new CollectionRightsAttribute(); + attr->d->mData = d->mData; + + return attr; +} + +QByteArray CollectionRightsAttribute::type() const +{ + static const QByteArray s_accessRightsIdentifier("AccessRights"); + return s_accessRightsIdentifier; +} + +QByteArray CollectionRightsAttribute::serialized() const +{ + return d->mData; +} + +void CollectionRightsAttribute::deserialize(const QByteArray &data) +{ + d->mData = data; +} diff --git a/src/core/collectionrightsattribute_p.h b/src/core/collectionrightsattribute_p.h new file mode 100644 index 0000000..158a7b1 --- /dev/null +++ b/src/core/collectionrightsattribute_p.h @@ -0,0 +1,85 @@ +/* + Copyright (c) 2007 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_COLLECTIONRIGHTSATTRIBUTE_P_H +#define AKONADI_COLLECTIONRIGHTSATTRIBUTE_P_H + +#include "akonadicore_export.h" + +#include "collection.h" +#include "attribute.h" + +namespace Akonadi +{ + +/** + * @internal + * + * @short Attribute that stores the rights of a collection. + * + * Every collection can have rights set which describes whether + * the collection is readable or writable. That information is stored + * in this custom attribute. + * + * @note You shouldn't use this class directly but the convenience methods + * Collection::rights() and Collection::setRights() instead. + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT CollectionRightsAttribute : public Attribute +{ +public: + /** + * Creates a new collection rights attribute. + */ + CollectionRightsAttribute(); + + /** + * Destroys the collection rights attribute. + */ + ~CollectionRightsAttribute(); + + /** + * Sets the @p rights of the collection. + */ + void setRights(Collection::Rights rights); + + /** + * Returns the rights of the collection. + */ + Collection::Rights rights() const; + + QByteArray type() const Q_DECL_OVERRIDE; + + CollectionRightsAttribute *clone() const Q_DECL_OVERRIDE; + + QByteArray serialized() const Q_DECL_OVERRIDE; + + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/collectionstatistics.cpp b/src/core/collectionstatistics.cpp new file mode 100644 index 0000000..51f2379 --- /dev/null +++ b/src/core/collectionstatistics.cpp @@ -0,0 +1,110 @@ +/* + 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 "collectionstatistics.h" + +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN CollectionStatistics::Private : public QSharedData +{ +public: + Private() + : QSharedData() + , count(-1) + , unreadCount(-1) + , size(-1) + { + } + + Private(const Private &other) + : QSharedData(other) + { + count = other.count; + unreadCount = other.unreadCount; + size = other.size; + } + + qint64 count; + qint64 unreadCount; + qint64 size; +}; + +CollectionStatistics::CollectionStatistics() + : d(new Private) +{ +} + +CollectionStatistics::CollectionStatistics(const CollectionStatistics &other) + : d(other.d) +{ +} + +CollectionStatistics::~CollectionStatistics() +{ +} + +qint64 CollectionStatistics::count() const +{ + return d->count; +} + +void CollectionStatistics::setCount(qint64 count) +{ + d->count = count; +} + +qint64 CollectionStatistics::unreadCount() const +{ + return d->unreadCount; +} + +void CollectionStatistics::setUnreadCount(qint64 count) +{ + d->unreadCount = count; +} + +qint64 CollectionStatistics::size() const +{ + return d->size; +} + +void CollectionStatistics::setSize(qint64 size) +{ + d->size = size; +} + +CollectionStatistics &CollectionStatistics::operator =(const CollectionStatistics &other) +{ + d = other.d; + return *this; +} + +QDebug operator<<(QDebug d, const CollectionStatistics &s) +{ + return d << "CollectionStatistics:" << endl + << " count:" << s.count() << endl + << " unread count:" << s.unreadCount() << endl + << " size:" << s.size(); +} diff --git a/src/core/collectionstatistics.h b/src/core/collectionstatistics.h new file mode 100644 index 0000000..7119a61 --- /dev/null +++ b/src/core/collectionstatistics.h @@ -0,0 +1,162 @@ +/* + 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. +*/ + +#ifndef AKONADI_COLLECTIONSTATISTICS_H +#define AKONADI_COLLECTIONSTATISTICS_H + +#include "akonadicore_export.h" + +#include +#include + +namespace Akonadi +{ + +/** + * @short Provides statistics information of a Collection. + * + * This class contains information such as total number of items, + * number of new and unread items, etc. + * + * This information might be expensive to obtain and is thus + * not included when fetching collections with a CollectionFetchJob. + * It can be retrieved separately using CollectionStatisticsJob. + * + * Example: + * + * @code + * + * Akonadi::Collection collection = ... + * + * Akonadi::CollectionStatisticsJob *job = new Akonadi::CollectionStatisticsJob( collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * CollectionStatisticsJob *statisticsJob = qobject_cast( job ); + * + * const Akonadi::CollectionStatistics statistics = statisticsJob->statistics(); + * qDebug() << "Unread items:" << statistics.unreadCount(); + * } + * + * @endcode + * + * This class is implicitly shared. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionStatistics +{ +public: + /** + * Creates a new collection statistics object. + */ + CollectionStatistics(); + + /** + * Creates a collection statistics object from an @p other one. + */ + CollectionStatistics(const CollectionStatistics &other); + + /** + * Destroys the collection statistics object. + */ + ~CollectionStatistics(); + + /** + * Returns the number of items in this collection or @c -1 if + * this information is not available. + * + * @see setCount() + * @see unreadCount() + */ + qint64 count() const; + + /** + * Sets the number of items in this collection. + * + * @param count The number of items. + * @see count() + */ + void setCount(qint64 count); + + /** + * Returns the number of unread items in this collection or @c -1 if + * this information is not available. + * + * @see setUnreadCount() + * @see count() + */ + qint64 unreadCount() const; + + /** + * Sets the number of unread items in this collection. + * + * @param count The number of unread messages. + * @see unreadCount() + */ + void setUnreadCount(qint64 count); + + /** + * Returns the total size of the items in this collection or @c -1 if + * this information is not available. + * + * @see setSize() + * @since 4.3 + */ + qint64 size() const; + + /** + * Sets the total size of the items in this collection. + * + * @param size The total size of the items + * @see size() + * @since 4.3 + */ + void setSize(qint64 size); + + /** + * Assigns @p other to this statistics object and returns a reference to this one. + */ + CollectionStatistics &operator=(const CollectionStatistics &other); + +private: + //@cond PRIVATE + class Private; + QSharedDataPointer d; + //@endcond +}; + +} + +/** + * Allows to output the collection statistics for debugging purposes. + */ +AKONADICORE_EXPORT QDebug operator<<(QDebug d, const Akonadi::CollectionStatistics &); + +Q_DECLARE_METATYPE(Akonadi::CollectionStatistics) + +#endif diff --git a/src/core/collectionsync.cpp b/src/core/collectionsync.cpp new file mode 100644 index 0000000..d4c9e34 --- /dev/null +++ b/src/core/collectionsync.cpp @@ -0,0 +1,837 @@ +/* + Copyright (c) 2007, 2009 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#include "collectionsync_p.h" +#include "collection.h" +#include "akonadicore_debug.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionmodifyjob.h" +#include "collectionfetchscope.h" +#include "collectionmovejob.h" + +#include "cachepolicy.h" + +#include +#include +#include +#include + +using namespace Akonadi; + +static const char CONTENTMIMETYPES[] = "CONTENTMIMETYPES"; + +static const char ROOTPARENTRID[] = "AKONADI_ROOT_COLLECTION"; + + +class RemoteId +{ +public: + explicit RemoteId() + { + } + + explicit inline RemoteId(const QStringList &ridChain): + ridChain(ridChain) + { + } + + explicit inline RemoteId(const QString &rid) + { + ridChain.append(rid); + } + + inline bool isAbsolute() const + { + return ridChain.last() == QString::fromLatin1(ROOTPARENTRID); + } + + inline bool isEmpty() const + { + return ridChain.isEmpty(); + } + + inline bool operator==(const RemoteId &other) const + { + return ridChain == other.ridChain; + } + + QStringList ridChain; + + static RemoteId rootRid; +}; + +RemoteId RemoteId::rootRid = RemoteId(QStringList() << QString::fromLatin1(ROOTPARENTRID)); + +Q_DECLARE_METATYPE(RemoteId) + +uint qHash(const RemoteId &rid) +{ + uint hash = 0; + for (QStringList::ConstIterator iter = rid.ridChain.constBegin(), + end = rid.ridChain.constEnd(); + iter != end; + ++iter) { + hash += qHash(*iter); + } + return hash; +} + +inline bool operator<(const RemoteId &r1, const RemoteId &r2) +{ + if (r1.ridChain.length() == r2.ridChain.length()) { + QStringList::ConstIterator it1 = r1.ridChain.constBegin(), + end1 = r1.ridChain.constEnd(), + it2 = r2.ridChain.constBegin(); + while (it1 != end1) { + if ((*it1) == (*it2)) { + ++it1; + ++it2; + continue; + } + return (*it1) < (*it2); + } + } else { + return r1.ridChain.length() < r2.ridChain.length(); + } + return false; +} + +QDebug operator<<(QDebug s, const RemoteId &rid) +{ + s.nospace() << "RemoteId(" << rid.ridChain << ")"; + return s; +} + +/** + * @internal + */ +class CollectionSync::Private +{ +public: + Private(CollectionSync *parent) + : q(parent) + , pendingJobs(0) + , progress(0) + , currentTransaction(0) + , incremental(false) + , streaming(false) + , hierarchicalRIDs(false) + , localListDone(false) + , deliveryDone(false) + , akonadiRootCollection(Collection::root()) + , resultEmitted(false) + { + } + + ~Private() + { + } + + RemoteId remoteIdForCollection(const Collection &collection) const + { + if (collection == Collection::root()) { + return RemoteId::rootRid; + } + + if (!hierarchicalRIDs) { + return RemoteId(collection.remoteId()); + } + + RemoteId rid; + Collection parent = collection; + while (parent.isValid() || !parent.remoteId().isEmpty()) { + QString prid = parent.remoteId(); + if (prid.isEmpty() && parent.isValid()) { + prid = uidRidMap.value(parent.id()); + } + if (prid.isEmpty()) { + break; + } + rid.ridChain.append(prid); + parent = parent.parentCollection(); + if (parent == akonadiRootCollection) { + rid.ridChain.append(QString::fromLatin1(ROOTPARENTRID)); + break; + } + } + return rid; + } + + void addRemoteColection(const Collection &collection, bool removed = false) + { + QHash &map = (removed ? removedRemoteCollections : remoteCollections); + const Collection parentCollection = collection.parentCollection(); + if (parentCollection.remoteId() == akonadiRootCollection.remoteId() || parentCollection.id() == akonadiRootCollection.id()) { + Collection c2(collection); + c2.setParentCollection(akonadiRootCollection); + map[RemoteId::rootRid].append(c2); + } else { + Q_ASSERT(!parentCollection.remoteId().isEmpty()); + map[remoteIdForCollection(parentCollection)].append(collection); + } + } + + /* Compares collections by remoteId and falls back to name comparison in case + * local collection does not have remoteId (which can happen in some cases) + */ + bool matchLocalAndRemoteCollection(const Collection &local, const Collection &remote) + { + if (!local.remoteId().isEmpty()) { + return local.remoteId() == remote.remoteId(); + } else { + return local.name() == remote.name(); + } + } + + void localCollectionsReceived(const Akonadi::Collection::List &localCols) + { + Q_FOREACH (const Akonadi::Collection &collection, localCols) { + const RemoteId parentRid = remoteIdForCollection(collection.parentCollection()); + localCollections[parentRid] += collection; + } + } + + void processCollections(const RemoteId &parentRid) + { + Collection::List remoteChildren = remoteCollections.value(parentRid); + Collection::List removedChildren = removedRemoteCollections.value(parentRid); + Collection::List localChildren = localCollections.value(parentRid); + + // Iterate over the list of local children of localParent + Collection::List::Iterator localIter, localEnd, + removedIter, removedEnd, + remoteIter, remoteEnd; + + for (localIter = localChildren.begin(), localEnd = localChildren.end(); localIter != localEnd;) { + const Collection localCollection = *localIter; + bool matched = false; + uidRidMap.insert(localIter->id(), localIter->remoteId()); + + // Try to map removed remote collections (from incremental sync) to local collections + for (removedIter = removedChildren.begin(), removedEnd = removedChildren.end(); removedIter != removedEnd;) { + Collection removedCollection = *removedIter; + + if (matchLocalAndRemoteCollection(localCollection, removedCollection)) { + matched = true; + if (!localCollection.remoteId().isEmpty()) + localCollectionsToRemove.append(localCollection); + // Remove the matched removed collection from the list so that + // we don't have to iterate over it again next time. + removedIter = removedChildren.erase(removedIter); + removedEnd = removedChildren.end(); + break; + } else { + // Keep looking + ++removedIter; + } + } + + if (matched) { + // Remove the matched local collection from the list, because we + // have already put it into localCollectionsToRemove + localIter = localChildren.erase(localIter); + localEnd = localChildren.end(); + continue; + } + + // Try to find a matching collection in the list of remote children + for (remoteIter = remoteChildren.begin(), remoteEnd = remoteChildren.end(); !matched && remoteIter != remoteEnd;) { + Collection remoteCollection = *remoteIter; + + // Yay, we found a match! + if (matchLocalAndRemoteCollection(localCollection, remoteCollection)) { + matched = true; + + // Check if the local and remote collections differ and thus if + // we need to update it + if (collectionNeedsUpdate(localCollection, remoteCollection)) { + // We need to store both local and remote collections, so that + // we can copy over attributes to be preserved + remoteCollectionsToUpdate.append(qMakePair(localCollection, remoteCollection)); + } else { + // Collections are the same, no need to update anything + } + + // Remove the matched remote collection from the list so that + // in the end we are left with list of collections that don't + // exist locally (i.e. new collections) + remoteIter = remoteChildren.erase(remoteIter); + remoteEnd = remoteChildren.end(); + break; + } else { + // Keep looking + ++remoteIter; + } + } + + if (matched) { + // Remove the matched local collection from the list so that + // in the end we are left with list of collections that don't + // exist remotely (i.e. removed collections) + localIter = localChildren.erase(localIter); + localEnd = localChildren.end(); + } else { + ++localIter; + } + } + + if (!removedChildren.isEmpty()) { + removedRemoteCollections[parentRid] = removedChildren; + } else { + removedRemoteCollections.remove(parentRid); + } + + if (!remoteChildren.isEmpty()) { + remoteCollections[parentRid] = remoteChildren; + } else { + remoteCollections.remove(parentRid); + } + + if (!localChildren.isEmpty()) { + localCollections[parentRid] = localChildren; + } else { + localCollections.remove(parentRid); + } + } + + void processLocalCollections(const RemoteId &parentRid, const Collection &parentCollection) + { + const Collection::List originalChildren = localCollections.value(parentRid); + processCollections(parentRid); + + const Collection::List remoteChildren = remoteCollections.take(parentRid); + const Collection::List localChildren = localCollections.take(parentRid); + + // At this point remoteChildren contains collections that don't exist locally yet + if (!remoteChildren.isEmpty()) { + Q_FOREACH (Collection c, remoteChildren) { + c.setParentCollection(parentCollection); + remoteCollectionsToCreate.append(c); + } + } + // At this point localChildren contains collections that don't exist remotely anymore + if (!localChildren.isEmpty() && !incremental) { + Q_FOREACH (const auto &c, localChildren) { + if (!c.remoteId().isEmpty()) + localCollectionsToRemove.push_back(c); + } + } + + // Recurse into children + Q_FOREACH (const Collection &c, originalChildren) { + processLocalCollections(remoteIdForCollection(c), c); + } + } + + void localCollectionFetchResult(KJob *job) + { + if (job->error()) { + return; // handled by the base class + } + + processLocalCollections(RemoteId::rootRid, akonadiRootCollection); + localListDone = true; + execute(); + } + + bool ignoreAttributeChanges(const Akonadi::Collection &col, const QByteArray &attribute) const + { + return (keepLocalChanges.contains(attribute) || col.keepLocalChanges().contains(attribute)); + } + + /** + Checks if the given localCollection and remoteCollection are different + */ + bool collectionNeedsUpdate(const Collection &localCollection, const Collection &remoteCollection) const + { + if (!ignoreAttributeChanges(remoteCollection, CONTENTMIMETYPES)) { + if (localCollection.contentMimeTypes().size() != remoteCollection.contentMimeTypes().size()) { + return true; + } else { + for (int i = 0; i < remoteCollection.contentMimeTypes().size(); i++) { + const QString &m = remoteCollection.contentMimeTypes().at(i); + if (!localCollection.contentMimeTypes().contains(m)) { + return true; + } + } + } + } + + if (localCollection.parentCollection().remoteId() != remoteCollection.parentCollection().remoteId()) { + return true; + } + if (localCollection.name() != remoteCollection.name()) { + return true; + } + if (localCollection.remoteId() != remoteCollection.remoteId()) { + return true; + } + if (localCollection.remoteRevision() != remoteCollection.remoteRevision()) { + return true; + } + if (!(localCollection.cachePolicy() == remoteCollection.cachePolicy())) { + return true; + } + if (localCollection.enabled() != remoteCollection.enabled()) { + return true; + } + + // CollectionModifyJob adds the remote attributes to the local collection + Q_FOREACH (const Attribute *attr, remoteCollection.attributes()) { + const Attribute *localAttr = localCollection.attribute(attr->type()); + if (localAttr && ignoreAttributeChanges(remoteCollection, attr->type())) { + continue; + } + // The attribute must both exist and have equal contents + if (!localAttr || localAttr->serialized() != attr->serialized()) { + return true; + } + } + + return false; + } + + void createLocalCollections() + { + if (remoteCollectionsToCreate.isEmpty()) { + updateLocalCollections(); + return; + } + + Collection::List::Iterator iter, end; + for (iter = remoteCollectionsToCreate.begin(), end = remoteCollectionsToCreate.end(); iter != end;) { + const Collection col = *iter; + const Collection parentCollection = col.parentCollection(); + // The parent already exists locally + if (parentCollection == akonadiRootCollection || parentCollection.id() > 0) { + ++pendingJobs; + CollectionCreateJob *create = new CollectionCreateJob(col, currentTransaction); + connect(create, SIGNAL(result(KJob*)), + q, SLOT(createLocalCollectionResult(KJob*))); + + // Commit transaction after every 100 collections are created, + // otherwise it overlads database journal and things get veeery slow + if (pendingJobs % 100 == 0) { + currentTransaction->commit(); + createTransaction(); + } + + iter = remoteCollectionsToCreate.erase(iter); + end = remoteCollectionsToCreate.end(); + } else { + // Skip the collection, we'll try again once we create all the other + // collection we already have a parent for + ++iter; + } + } + } + + void createLocalCollectionResult(KJob *job) + { + --pendingJobs; + if (job->error()) { + return; // handled by the base class + } + + q->setProcessedAmount(KJob::Bytes, ++progress); + + const Collection newLocal = static_cast(job)->collection(); + uidRidMap.insert(newLocal.id(), newLocal.remoteId()); + const RemoteId newLocalRID = remoteIdForCollection(newLocal); + + // See if there are any pending collections that this collection is parent of and + // update them if so + Collection::List::Iterator iter, end; + for (iter = remoteCollectionsToCreate.begin(), end = remoteCollectionsToCreate.end(); iter != end; ++iter) { + const Collection parentCollection = iter->parentCollection(); + if (parentCollection != akonadiRootCollection && parentCollection.id() <= 0) { + const RemoteId remoteRID = remoteIdForCollection(*iter); + if (remoteRID.isAbsolute()) { + if (newLocalRID == remoteIdForCollection(*iter)) { + iter->setParentCollection(newLocal); + } + } else if (!hierarchicalRIDs) { + if (remoteRID.ridChain.startsWith(parentCollection.remoteId())) { + iter->setParentCollection(newLocal); + } + } + } + } + + // Enqueue all pending remote collections that are children of the just-created + // collection + Collection::List collectionsToCreate = remoteCollections.take(newLocalRID); + if (collectionsToCreate.isEmpty() && !hierarchicalRIDs) { + collectionsToCreate = remoteCollections.take(RemoteId(newLocal.remoteId())); + } + Q_FOREACH (Collection col, collectionsToCreate) { + col.setParentCollection(newLocal); + remoteCollectionsToCreate.append(col); + } + + // If there are still any collections to create left, try if we just created + // a parent for any of them + if (!remoteCollectionsToCreate.isEmpty()) { + createLocalCollections(); + } else if (pendingJobs == 0) { + Q_ASSERT(remoteCollectionsToCreate.isEmpty()); + if (!remoteCollections.isEmpty()) { + currentTransaction->rollback(); + q->setError(Unknown); + q->setErrorText(i18n("Found unresolved orphan collections")); + qCWarning(AKONADICORE_LOG) << "found unresolved orphan collection"; + emitResult(); + return; + } + + currentTransaction->commit(); + createTransaction(); + + // Otherwise move to next task: updating existing collections + updateLocalCollections(); + } + /* + * else if (!remoteCollections.isEmpty()) { + currentTransaction->rollback(); + q->setError(Unknown); + q->setErrorText(i18n("Incomplete collection tree")); + emitResult(); + return; + } + */ + } + + /** + Performs a local update for the given node pair. + */ + void updateLocalCollections() + { + if (remoteCollectionsToUpdate.isEmpty()) { + deleteLocalCollections(); + return; + } + + typedef QPair CollectionPair; + Q_FOREACH (const CollectionPair &pair, remoteCollectionsToUpdate) { + const Collection local = pair.first; + const Collection remote = pair.second; + Collection upd(remote); + + Q_ASSERT(!upd.remoteId().isEmpty()); + Q_ASSERT(currentTransaction); + upd.setId(local.id()); + if (ignoreAttributeChanges(remote, CONTENTMIMETYPES)) { + upd.setContentMimeTypes(local.contentMimeTypes()); + } + Q_FOREACH (Attribute *remoteAttr, upd.attributes()) { + if (ignoreAttributeChanges(remote, remoteAttr->type()) && local.hasAttribute(remoteAttr->type())) { + //We don't want to overwrite the attribute changes with the defaults provided by the resource. + Attribute *localAttr = local.attribute(remoteAttr->type()); + upd.removeAttribute(localAttr->type()); + upd.addAttribute(localAttr->clone()); + } + } + + // ### HACK to work around the implicit move attempts of CollectionModifyJob + // which we do explicitly below + Collection c(upd); + c.setParentCollection(local.parentCollection()); + ++pendingJobs; + CollectionModifyJob *mod = new CollectionModifyJob(c, currentTransaction); + connect(mod, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*))); + + // detecting moves is only possible with global RIDs + if (!hierarchicalRIDs) { + if (remote.parentCollection().isValid() && remote.parentCollection().id() != local.parentCollection().id()) { + ++pendingJobs; + CollectionMoveJob *move = new CollectionMoveJob(upd, remote.parentCollection(), currentTransaction); + connect(move, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*))); + } + } + } + } + + void updateLocalCollectionResult(KJob *job) + { + --pendingJobs; + if (job->error()) { + return; // handled by the base class + } + if (qobject_cast(job)) { + q->setProcessedAmount(KJob::Bytes, ++progress); + } + + // All updates are done, time to move on to next task: deletion + if (pendingJobs == 0) { + currentTransaction->commit(); + createTransaction(); + + deleteLocalCollections(); + } + } + + void deleteLocalCollections() + { + if (localCollectionsToRemove.isEmpty()) { + done(); + return; + } + + Q_FOREACH (const Collection &col, localCollectionsToRemove) { + Q_ASSERT(!col.remoteId().isEmpty()); // empty RID -> stuff we haven't even written to the remote side yet + + ++pendingJobs; + Q_ASSERT(currentTransaction); + CollectionDeleteJob *job = new CollectionDeleteJob(col, currentTransaction); + connect(job, SIGNAL(result(KJob*)), q, SLOT(deleteLocalCollectionsResult(KJob*))); + + // It can happen that the groupware servers report us deleted collections + // twice, in this case this collection delete job will fail on the second try. + // To avoid a rollback of the complete transaction we gracefully allow the job + // to fail :) + currentTransaction->setIgnoreJobFailure(job); + } + } + + void deleteLocalCollectionsResult(KJob *) + { + --pendingJobs; + q->setProcessedAmount(KJob::Bytes, ++progress); + + if (pendingJobs == 0) { + currentTransaction->commit(); + currentTransaction = 0; + + done(); + } + } + + void done() + { + if (currentTransaction) { + //This can trigger a direct call of transactionSequenceResult + currentTransaction->commit(); + currentTransaction = 0; + } + + if (!remoteCollections.isEmpty()) { + q->setError(Unknown); + q->setErrorText(i18n("Found unresolved orphan collections")); + } + emitResult(); + } + + void emitResult() + { + //Prevent double result emission + Q_ASSERT(!resultEmitted); + if (!resultEmitted) { + if (q->hasSubjobs()) { + // If there are subjobs, pick one, wait for it to finish, then + // try again. This way we make sure we don't emit result() signal + // while there is still a Transaction job running + KJob *subjob = q->subjobs().at(0); + connect(subjob, &KJob::result, + q, [this](KJob*) { + emitResult(); + }, + Qt::QueuedConnection); + } else { + resultEmitted = true; + q->emitResult(); + } + } + } + + void createTransaction() + { + currentTransaction = new TransactionSequence(q); + currentTransaction->setAutomaticCommittingEnabled(false); + q->connect(currentTransaction, SIGNAL(finished(KJob*)), + q, SLOT(transactionSequenceResult(KJob*))); + } + + /** After the transaction has finished report we're done as well. */ + void transactionSequenceResult(KJob *job) + { + if (job->error()) { + return; // handled by the base class + } + + // If this was the last transaction, then finish, otherwise there's + // a new transaction in the queue already + if (job == currentTransaction) { + currentTransaction = 0; + } + } + + /** + Process what's currently available. + */ + void execute() + { + qCDebug(AKONADICORE_LOG) << "localListDone: " << localListDone << " deliveryDone: " << deliveryDone; + if (!localListDone && !deliveryDone) { + return; + } + + if (!localListDone && deliveryDone) { + Job *parent = (currentTransaction ? static_cast(currentTransaction) : static_cast(q)); + CollectionFetchJob *job = new CollectionFetchJob(akonadiRootCollection, CollectionFetchJob::Recursive, parent); + job->fetchScope().setResource(resourceId); + job->fetchScope().setListFilter(CollectionFetchScope::NoFilter); + job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + q, SLOT(localCollectionsReceived(Akonadi::Collection::List))); + q->connect(job, SIGNAL(result(KJob*)), + q, SLOT(localCollectionFetchResult(KJob*))); + return; + } + + // If a transaction is not started yet, it means we just finished local listing + if (!currentTransaction) { + // There's nothing to do after local listing -> we are done! + if (remoteCollectionsToCreate.isEmpty() && remoteCollectionsToUpdate.isEmpty() && localCollectionsToRemove.isEmpty()) { + qCDebug(AKONADICORE_LOG) << "Nothing to do"; + emitResult(); + return; + } + // Ok, there's some work to do, so create a transaction we can use + createTransaction(); + } + + createLocalCollections(); + } + + CollectionSync *q; + + QString resourceId; + + int pendingJobs; + int progress; + + TransactionSequence *currentTransaction; + + bool incremental; + bool streaming; + bool hierarchicalRIDs; + + bool localListDone; + bool deliveryDone; + + // List of parts where local changes should not be overwritten + QSet keepLocalChanges; + + QHash removedRemoteCollections; + QHash remoteCollections; + QHash localCollections; + + Collection::List localCollectionsToRemove; + Collection::List remoteCollectionsToCreate; + QList > remoteCollectionsToUpdate; + QHash uidRidMap; + + // HACK: To workaround Collection copy constructor being very expensive, we + // store the Collection::root() collection in a variable here for faster + // access + Collection akonadiRootCollection; + + bool resultEmitted; + +}; + +CollectionSync::CollectionSync(const QString &resourceId, QObject *parent) + : Job(parent) + , d(new Private(this)) +{ + d->resourceId = resourceId; + setTotalAmount(KJob::Bytes, 0); +} + +CollectionSync::~CollectionSync() +{ + delete d; +} + +void CollectionSync::setRemoteCollections(const Collection::List &remoteCollections) +{ + setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + remoteCollections.count()); + Q_FOREACH (const Collection &c, remoteCollections) { + d->addRemoteColection(c); + } + + if (!d->streaming) { + d->deliveryDone = true; + } + d->execute(); +} + +void CollectionSync::setRemoteCollections(const Collection::List &changedCollections, const Collection::List &removedCollections) +{ + setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + changedCollections.count()); + d->incremental = true; + Q_FOREACH (const Collection &c, changedCollections) { + d->addRemoteColection(c); + } + Q_FOREACH (const Collection &c, removedCollections) { + d->addRemoteColection(c, true); + } + + if (!d->streaming) { + d->deliveryDone = true; + } + d->execute(); +} + +void CollectionSync::doStart() +{ +} + +void CollectionSync::setStreamingEnabled(bool streaming) +{ + d->streaming = streaming; +} + +void CollectionSync::retrievalDone() +{ + d->deliveryDone = true; + d->execute(); +} + +void CollectionSync::setHierarchicalRemoteIds(bool hierarchical) +{ + d->hierarchicalRIDs = hierarchical; +} + +void CollectionSync::rollback() +{ + if (d->currentTransaction) { + d->currentTransaction->rollback(); + } +} + +void CollectionSync::setKeepLocalChanges(const QSet &parts) +{ + d->keepLocalChanges = parts; +} + +#include "moc_collectionsync_p.cpp" + diff --git a/src/core/collectionsync_p.h b/src/core/collectionsync_p.h new file mode 100644 index 0000000..eb56844 --- /dev/null +++ b/src/core/collectionsync_p.h @@ -0,0 +1,144 @@ +/* + Copyright (c) 2007, 2009 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_COLLECTIONSYNC_P_H +#define AKONADI_COLLECTIONSYNC_P_H + +#include "akonadicore_export.h" +#include "collection.h" +#include "transactionsequence.h" + +namespace Akonadi +{ + +/** + @internal + + Syncs remote and local collections. + + Basic terminology: + - "local": The current state in the Akonadi server + - "remote": The state in the backend, which is also the state the Akonadi + server is supposed to have afterwards. + + There are three options to influence the way syncing is done: + - Streaming vs. complete delivery: If streaming is enabled remote collections + do not need to be delivered in a single batch but can be delivered in multiple + chunks. This improves performance but requires an explicit notification + when delivery has been completed. + - Incremental vs. non-incremental: In the incremental case only remote changes + since the last sync have to be delivered, in the non-incremental mode the full + remote state has to be provided. The first is obviously the preferred way, + but requires support by the backend. + - Hierarchical vs. global RIDs: The first requires RIDs to be unique per parent + collection, the second one requires globally unique RIDs (per resource). Those + have different advantages and disadvantages, esp. regarding moving. Which one + to chose mostly depends on what the backend provides in this regard. + +*/ +class AKONADICORE_EXPORT CollectionSync : public Job +{ + Q_OBJECT + +public: + /** + Creates a new collection synchronzier. + @param resourceId The identifier of the resource we are syncing. + @param parent The parent object. + */ + explicit CollectionSync(const QString &resourceId, QObject *parent = 0); + + /** + Destroys this job. + */ + ~CollectionSync(); + + /** + Sets the result of a full remote collection listing. + @param remoteCollections A list of collections. + Important: All of these need a unique remote identifier and parent remote + identifier. + */ + void setRemoteCollections(const Collection::List &remoteCollections); + + /** + Sets the result of an incremental remote collection listing. + @param changedCollections A list of remotely added or changed collections. + @param removedCollections A list of remotely deleted collections. + */ + void setRemoteCollections(const Collection::List &changedCollections, + const Collection::List &removedCollections); + + /** + Enables streaming, that is not all collections are delivered at once. + Use setRemoteCollections() multiple times when streaming is enabled and call + retrievalDone() when all collections have been retrieved. + Must be called before the first call to setRemoteCollections(). + @param streaming enables streaming if set as @c true + */ + void setStreamingEnabled(bool streaming); + + /** + Indicate that all collections have been retrieved in streaming mode. + */ + void retrievalDone(); + + /** + Indicate whether the resource supplies collections with hierarchical or + global remote identifiers. @c false by default. + Must be called before the first call to setRemoteCollections(). + @param hierarchical @c true if collection remote IDs are relative to their parents' remote IDs + */ + void setHierarchicalRemoteIds(bool hierarchical); + + /** + Do a rollback operation if needed. In read only cases this is a noop. + */ + void rollback(); + + /** + * Allows to specify parts of the collection that should not be changed if locally available. + * + * This is useful for resources to provide default values during the collection sync, while + * preserving more up-to date values if available. + * + * Use CONTENTMIMETYPES as identifier to not overwrite the content mimetypes. + * + * @since 4.14 + */ + void setKeepLocalChanges(const QSet &parts); + +protected: + void doStart() Q_DECL_OVERRIDE; + +private: + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void localCollectionsReceived(const Akonadi::Collection::List &localCols)) + Q_PRIVATE_SLOT(d, void localCollectionFetchResult(KJob *job)) + Q_PRIVATE_SLOT(d, void updateLocalCollectionResult(KJob *job)) + Q_PRIVATE_SLOT(d, void createLocalCollectionResult(KJob *job)) + Q_PRIVATE_SLOT(d, void deleteLocalCollectionsResult(KJob *job)) + Q_PRIVATE_SLOT(d, void transactionSequenceResult(KJob *job)) +}; + +} + +#endif diff --git a/src/core/collectionutils.h b/src/core/collectionutils.h new file mode 100644 index 0000000..2fdfd06 --- /dev/null +++ b/src/core/collectionutils.h @@ -0,0 +1,141 @@ +/* + 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 + +#include "entitydisplayattribute.h" +#include "collectionstatistics.h" +#include "item.h" + +namespace Akonadi +{ + +/** + * @internal + */ +namespace CollectionUtils +{ +inline bool isVirtualParent(const Collection &collection) +{ + return (collection.parentCollection() == Collection::root() && collection.isVirtual()); +} + +inline bool isReadOnly(const Collection &collection) +{ + return !(collection.rights() &Collection::CanCreateItem); +} + +inline bool isRoot(const Collection &collection) +{ + return (collection == Collection::root()); +} + +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 (!isRoot(collection) && + !isResource(collection) && + !isStructural(collection) && + collection.resource() != QLatin1String("akonadi_search_resource") && + collection.resource() != QLatin1String("akonadi_nepomuktag_resource")); +} + +inline QString defaultIconName(const Collection &col) +{ + if (CollectionUtils::isVirtualParent(col)) { + return QStringLiteral("edit-find"); + } + if (col.isVirtual()) { + return QStringLiteral("document-preview"); + } + if (CollectionUtils::isResource(col)) { + return QStringLiteral("network-server"); + } + if (CollectionUtils::isStructural(col)) { + return QStringLiteral("folder-grey"); + } + if (CollectionUtils::isReadOnly(col)) { + return QStringLiteral("folder-grey"); + } + + const QStringList content = col.contentMimeTypes(); + if ((content.size() == 1) || + (content.size() == 2 && content.contains(Collection::mimeType()))) { + if (content.contains(QStringLiteral("text/x-vcard")) || + content.contains(QStringLiteral("text/directory")) || + content.contains(QStringLiteral("text/vcard"))) { + return QStringLiteral("x-office-address-book"); + } + // TODO: add all other content types and/or fix their mimetypes + if (content.contains(QStringLiteral("akonadi/event")) || content.contains(QStringLiteral("text/ical"))) { + return QStringLiteral("view-pim-calendar"); + } + if (content.contains(QStringLiteral("akonadi/task"))) { + return QStringLiteral("view-pim-tasks"); + } + } else if (content.isEmpty()) { + return QStringLiteral("folder-grey"); + } + return QStringLiteral("folder"); +} +inline QString displayIconName(const Collection &col) +{ + QString iconName = defaultIconName(col); + if (col.hasAttribute() && + !col.attribute()->iconName().isEmpty()) { + if (!col.attribute()->activeIconName().isEmpty() && col.statistics().unreadCount() > 0) { + iconName = col.attribute()->activeIconName(); + } else { + iconName = col.attribute()->iconName(); + } + } + return iconName; + +} +inline bool hasValidHierarchicalRID(const Collection &col) +{ + if (col == Collection::root()) { + return true; + } + if (col.remoteId().isEmpty()) { + return false; + } + return hasValidHierarchicalRID(col.parentCollection()); +} +inline bool hasValidHierarchicalRID(const Item &item) +{ + return !item.remoteId().isEmpty() && hasValidHierarchicalRID(item.parentCollection()); +} +} + +} + +#endif diff --git a/src/core/conflicthandler.cpp b/src/core/conflicthandler.cpp new file mode 100644 index 0000000..1fabe3a --- /dev/null +++ b/src/core/conflicthandler.cpp @@ -0,0 +1,145 @@ +/* + Copyright (c) 2010 KDAB + Author: 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. +*/ + +#include "conflicthandler_p.h" + +//KF5 PORT ME +//#include "conflictresolvedialog_p.h" + +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "session.h" +#include + +using namespace Akonadi; + +ConflictHandler::ConflictHandler(ConflictType type, QObject *parent) + : QObject(parent) + , mConflictType(type) + , mSession(new Session("conflict handling session", this)) +{ +} + +void ConflictHandler::setConflictingItems(const Akonadi::Item &changedItem, const Akonadi::Item &conflictingItem) +{ + mChangedItem = changedItem; + mConflictingItem = conflictingItem; +} + +void ConflictHandler::start() +{ + if (mConflictType == LocalLocalConflict || mConflictType == LocalRemoteConflict) { + ItemFetchJob *job = new ItemFetchJob(mConflictingItem, mSession); + job->fetchScope().fetchFullPayload(); + job->fetchScope().setAncestorRetrieval(ItemFetchScope::Parent); + connect(job, &ItemFetchJob::result, this, &ConflictHandler::slotOtherItemFetched); + } else { + resolve(); + } +} + +void ConflictHandler::slotOtherItemFetched(KJob *job) +{ + if (job->error()) { + emit error(job->errorText()); //TODO: extend error message + return; + } + + ItemFetchJob *fetchJob = qobject_cast(job); + if (fetchJob->items().isEmpty()) { + emit error(i18n("Did not find other item for conflict handling")); + return; + } + + mConflictingItem = fetchJob->items().at(0); + QMetaObject::invokeMethod(this, "resolve", Qt::QueuedConnection); +} + +void ConflictHandler::resolve() +{ +#warning KF5 Port me! +#if 0 + ConflictResolveDialog dlg; + dlg.setConflictingItems(mChangedItem, mConflictingItem); + dlg.exec(); + + const ResolveStrategy strategy = dlg.resolveStrategy(); + switch (strategy) { + case UseLocalItem: + useLocalItem(); + break; + case UseOtherItem: + useOtherItem(); + break; + case UseBothItems: + useBothItems(); + break; + } +#endif +} + +void ConflictHandler::useLocalItem() +{ + // We have to overwrite the other item inside the Akonadi storage with the local + // item. To make this happen, we have to set the revision of the local item to + // the one of the other item to let the Akonadi server accept it. + + Item newItem(mChangedItem); + newItem.setRevision(mConflictingItem.revision()); + + ItemModifyJob *job = new ItemModifyJob(newItem, mSession); + connect(job, &ItemModifyJob::result, this, &ConflictHandler::slotUseLocalItemFinished); +} + +void ConflictHandler::slotUseLocalItemFinished(KJob *job) +{ + if (job->error()) { + emit error(job->errorText()); //TODO: extend error message + } else { + emit conflictResolved(); + } +} + +void ConflictHandler::useOtherItem() +{ + // We can just ignore the local item here and leave everything as it is. + emit conflictResolved(); +} + +void ConflictHandler::useBothItems() +{ + // We have to create a new item for the local item under the collection that has + // been retrieved when we fetched the other item. + ItemCreateJob *job = new ItemCreateJob(mChangedItem, mConflictingItem.parentCollection(), mSession); + connect(job, &ItemCreateJob::result, this, &ConflictHandler::slotUseBothItemsFinished); +} + +void ConflictHandler::slotUseBothItemsFinished(KJob *job) +{ + if (job->error()) { + emit error(job->errorText()); //TODO: extend error message + } else { + emit conflictResolved(); + } +} + +#include "moc_conflicthandler_p.cpp" diff --git a/src/core/conflicthandler_p.h b/src/core/conflicthandler_p.h new file mode 100644 index 0000000..5962a25 --- /dev/null +++ b/src/core/conflicthandler_p.h @@ -0,0 +1,122 @@ +/* + Copyright (c) 2010 KDAB + Author: 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_CONFLICTHANDLER_P_H +#define AKONADI_CONFLICTHANDLER_P_H + +#include + +#include "item.h" + +class KJob; + +namespace Akonadi +{ + +class Session; + +/** + * @short A class to handle conflicts in Akonadi + * + * @author Tobias Koenig + */ +class ConflictHandler : public QObject +{ + Q_OBJECT + +public: + /** + * Describes the type of conflict that should be resolved by + * the conflict handler. + */ + enum ConflictType { + LocalLocalConflict, ///< Changes of two Akonadi client applications conflict. + LocalRemoteConflict, ///< Changes of an Akonadi client application and a resource conflict. + BackendConflict ///< Changes of a resource and the backend data conflict. + }; + + /** + * Describes the strategy that should be used for resolving the conflict. + */ + enum ResolveStrategy { + UseLocalItem, ///< The local item overwrites the other item inside the Akonadi storage. + UseOtherItem, ///< The local item is dropped and the other item from the Akonadi storage is used. + UseBothItems ///< Both items are kept in the Akonadi storage. + }; + + /** + * Creates a new conflict handler. + * + * @param type The type of the conflict that should be resolved. + * @param parent The parent object. + */ + explicit ConflictHandler(ConflictType type, QObject *parent = 0); + + /** + * Sets the items that causes the conflict. + * + * @param changedItem The item that has been changed, it needs the complete payload set. + * @param conflictingItem The item from the Akonadi storage that is conflicting. + * This needs only the id set, the payload will be refetched automatically. + */ + void setConflictingItems(const Akonadi::Item &changedItem, const Akonadi::Item &conflictingItem); + +public Q_SLOTS: + /** + * Starts the conflict handling. + */ + void start(); + +Q_SIGNALS: + /** + * This signal is emitted whenever the conflict has been resolved + * automatically or by the user. + */ + void conflictResolved(); + + /** + * This signal is emitted whenever an error occurred during the conflict + * handling. + * + * @param message A user visible string that describes the error. + */ + void error(const QString &message); + +private Q_SLOTS: + void slotOtherItemFetched(KJob *); + void slotUseLocalItemFinished(KJob *); + void slotUseBothItemsFinished(KJob *); + void resolve(); + +private: + void useLocalItem(); + void useOtherItem(); + void useBothItems(); + + ConflictType mConflictType; + Akonadi::Item mChangedItem; + Akonadi::Item mConflictingItem; + + Session *mSession; +}; + +} + +#endif diff --git a/src/core/connectionthread.cpp b/src/core/connectionthread.cpp new file mode 100644 index 0000000..2f611a5 --- /dev/null +++ b/src/core/connectionthread.cpp @@ -0,0 +1,296 @@ +/* + * Copyright 2015 Daniel Vrátil + * + * 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 "connectionthread_p.h" +#include "session_p.h" +#include "servermanager_p.h" +#include "akonadicore_debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; + +ConnectionThread::ConnectionThread(const QByteArray &sessionId, QObject *parent) + : QObject(parent) + , mSocket(Q_NULLPTR) + , mLogFile(Q_NULLPTR) + , mSessionId(sessionId) +{ + qRegisterMetaType(); + qRegisterMetaType(); + + QThread *thread = new QThread(); + moveToThread(thread); + thread->start(); + + const QByteArray sessionLogFile = qgetenv("AKONADI_SESSION_LOGFILE"); + if (!sessionLogFile.isEmpty()) { + mLogFile = new QFile(QStringLiteral("%1.%2.%3").arg(QString::fromLatin1(sessionLogFile), + QString::number(QApplication::applicationPid()), + QString::fromLatin1(mSessionId.replace('/', '_')))); + if (!mLogFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qCWarning(AKONADICORE_LOG) << "Failed to open Akonadi Session log file" << mLogFile->fileName(); + delete mLogFile; + mLogFile = Q_NULLPTR; + } + } +} + +ConnectionThread::~ConnectionThread() +{ + QMetaObject::invokeMethod(this, "doThreadQuit"); + if (!thread()->wait(10 * 1000)) { + thread()->terminate(); + // Make sure to wait until it's done, otherwise it can crash when the pthread callback is called + thread()->wait(); + } + delete mLogFile; + delete thread(); +} + +void ConnectionThread::doThreadQuit() +{ + Q_ASSERT(QThread::currentThread() == thread()); + if (mSocket) { + mSocket->disconnect(this); + mSocket->close(); + delete mSocket; + } + thread()->quit(); +} + +void ConnectionThread::reconnect() +{ + const bool ok = QMetaObject::invokeMethod(this, "doReconnect", Qt::QueuedConnection); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void ConnectionThread::doReconnect() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (mSocket && (mSocket->state() == QLocalSocket::ConnectedState + || mSocket->state() == QLocalSocket::ConnectingState)) { + // nothing to do, we are still/already connected + return; + } + + // try to figure out where to connect to + QString serverAddress; + + // env var has precedence + const QByteArray serverAddressEnvVar = qgetenv("AKONADI_SERVER_ADDRESS"); + if (!serverAddressEnvVar.isEmpty()) { + const int pos = serverAddressEnvVar.indexOf(':'); + const QByteArray protocol = serverAddressEnvVar.left(pos); + QMap options; + Q_FOREACH (const QString &entry, QString::fromLatin1(serverAddressEnvVar.mid(pos + 1)).split(QLatin1Char(','))) { + const QStringList pair = entry.split(QLatin1Char('=')); + if (pair.size() != 2) { + continue; + } + options.insert(pair.first(), pair.last()); + } + qCDebug(AKONADICORE_LOG) << protocol << options; + + if (protocol == "unix") { + serverAddress = options.value(QStringLiteral("path")); + } else if (protocol == "pipe") { + serverAddress = options.value(QStringLiteral("name")); + } + } + + // try config file next, fall back to defaults if that fails as well + if (serverAddress.isEmpty()) { + const QString connectionConfigFile = SessionPrivate::connectionFile(); + const QFileInfo fileInfo(connectionConfigFile); + if (!fileInfo.exists()) { + qCDebug(AKONADICORE_LOG) << "Akonadi Client Session: connection config file '" + "akonadi/akonadiconnectionrc' can not be found in" + << XdgBaseDirs::homePath("config") << "nor in any of" + << XdgBaseDirs::systemPathList("config"); + } + const QSettings connectionSettings(connectionConfigFile, QSettings::IniFormat); + + const QString defaultSocketDir = StandardDirs::saveDir("data"); + serverAddress = connectionSettings.value(QStringLiteral("Data/UnixPath"), QString(defaultSocketDir + QStringLiteral("/akonadiserver.socket"))).toString(); + } + + // create sockets if not yet done, note that this does not yet allow changing socket types on the fly + // but that's probably not something we need to support anyway + if (!mSocket) { + mSocket = new QLocalSocket(this); + connect(mSocket, static_cast(&QLocalSocket::error), this, + [this](QLocalSocket::LocalSocketError) { + Q_EMIT socketError(mSocket->errorString()); + Q_EMIT socketDisconnected(); + }); + connect(mSocket, &QLocalSocket::disconnected, this, &ConnectionThread::socketDisconnected); + connect(mSocket, &QLocalSocket::readyRead, this, &ConnectionThread::dataReceived); + } + + // actually do connect + qCDebug(AKONADICORE_LOG) << "connectToServer" << serverAddress; + mSocket->connectToServer(serverAddress); + + Q_EMIT reconnected(); +} + +void ConnectionThread::forceReconnect() +{ + const bool ok = QMetaObject::invokeMethod(this, "doForceReconnect", + Qt::QueuedConnection); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void ConnectionThread::doForceReconnect() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (mSocket) { + mSocket->disconnect(this, SIGNAL(socketDisconnected())); + delete mSocket; + mSocket = Q_NULLPTR; + } + mSocket = Q_NULLPTR; +} + +void ConnectionThread::disconnect() +{ + const bool ok = QMetaObject::invokeMethod(this, "doDisconnect", Qt::QueuedConnection); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void ConnectionThread::doDisconnect() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (mSocket) { + mSocket->close(); + mSocket->readAll(); + } +} + +void ConnectionThread::dataReceived() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + QElapsedTimer timer; + timer.start(); + + while (mSocket->bytesAvailable() > 0) { + QDataStream stream(mSocket); + qint64 tag; + // TODO: Verify the tag matches the last tag we sent + stream >> tag; + + Protocol::Command cmd; + try { + cmd = Protocol::deserialize(mSocket); + } catch (const Akonadi::ProtocolException &) { + // cmd's type will be Invalid by default. + } + if (cmd.type() == Protocol::Command::Invalid) { + qCWarning(AKONADICORE_LOG) << "Invalid command, the world is going to end!"; + mSocket->close(); + mSocket->readAll(); + reconnect(); + return; + } + + if (mLogFile) { + mLogFile->write("S: " + cmd.debugString().toUtf8()); + mLogFile->write("\n\n"); + mLogFile->flush(); + } + + Q_EMIT commandReceived(tag, cmd); + /* + if (!handleCommand(tag, cmd)) { + break; + } + */ + + // FIXME: It happens often that data are arriving from the server faster + // than we Jobs can process them which means, that we often process all + // responses in single dataReceived() call and thus not returning to back + // to QEventLoop, which breaks batch-delivery of ItemFetchJob (among other + // things). To workaround that we force processing of events every + // now and then. + // + // Longterm we want something better, like processing and parsing in + // separate thread which would only post the parsed Protocol::Commands + // to the jobs through event loop. That will be overall slower but should + // result in much more responsive applications. + if (timer.elapsed() > 50) { + QThread::currentThread()->eventDispatcher()->processEvents(QEventLoop::ExcludeSocketNotifiers); + timer.restart(); + } + } +} + +void ConnectionThread::sendCommand(qint64 tag, const Protocol::Command &cmd) +{ + const bool ok = QMetaObject::invokeMethod(this, "doSendCommand", + Qt::QueuedConnection, + Q_ARG(qint64, tag), + Q_ARG(Akonadi::Protocol::Command, cmd)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void ConnectionThread::doSendCommand(qint64 tag, const Protocol::Command &cmd) +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (mLogFile) { + mLogFile->write("C: " + cmd.debugString().toUtf8()); + mLogFile->write("\n\n"); + mLogFile->flush(); + } + + if (mSocket && mSocket->isOpen()) { + QDataStream stream(mSocket); + stream << tag; + try { + Protocol::serialize(mSocket, cmd); + } catch (const Akonadi::ProtocolException &e) { + qCWarning(AKONADICORE_LOG) << "Protocol Exception:" << QString::fromUtf8(e.what()); + mSocket->close(); + mSocket->readAll(); + reconnect(); + } + } else { + // TODO: Queue the commands and resend on reconnect? + } +} diff --git a/src/core/connectionthread_p.h b/src/core/connectionthread_p.h new file mode 100644 index 0000000..3f221a1 --- /dev/null +++ b/src/core/connectionthread_p.h @@ -0,0 +1,82 @@ +/* + * Copyright 2015 Daniel Vrátil + * + * 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 CONNECTIONTHREAD_P_H +#define CONNECTIONTHREAD_P_H + +#include +#include +#include +#include + +#include + +class QAbstractSocket; +class QFile; + +namespace Akonadi +{ + +class ConnectionThread : public QObject +{ + Q_OBJECT + +public: + explicit ConnectionThread(const QByteArray &sessionId, QObject *parent = Q_NULLPTR); + ~ConnectionThread(); + + Q_INVOKABLE void reconnect(); + void forceReconnect(); + void disconnect(); + void sendCommand(qint64 tag, const Protocol::Command &command); + +Q_SIGNALS: + void connected(); + void reconnected(); + void commandReceived(qint64 tag, const Akonadi::Protocol::Command &command); + void socketDisconnected(); + void socketError(const QString &message); + +private Q_SLOTS: + void doThreadQuit(); + void doReconnect(); + void doForceReconnect(); + void doDisconnect(); + void doSendCommand(qint64 tag, const Akonadi::Protocol::Command &command); + + void dataReceived(); + +private: + + bool handleCommand(qint64 tag, const Protocol::Command &cmd); + + QLocalSocket *mSocket; + QFile *mLogFile; + QByteArray mSessionId; + QMutex mLock; + struct Command { + qint64 tag; + Protocol::Command cmd; + }; + QQueue mOutQueue; +}; + +} + +#endif // CONNECTIONTHREAD_P_H diff --git a/src/core/control.cpp b/src/core/control.cpp new file mode 100644 index 0000000..b867e4f --- /dev/null +++ b/src/core/control.cpp @@ -0,0 +1,179 @@ +/* + 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 "control.h" +#include "servermanager.h" +#include "akonadicore_debug.h" + +#include +#include +#include +#include + +using namespace Akonadi; + +namespace Akonadi +{ +namespace Internal +{ + +class StaticControl : public Control +{ +public: + StaticControl() + : Control() + { + } +}; + +} + +Q_GLOBAL_STATIC(Internal::StaticControl, s_instance) + +/** + * @internal + */ +class Q_DECL_HIDDEN Control::Private +{ +public: + Private(Control *parent) + : mParent(parent) + , mEventLoop(0) + , mSuccess(false) + , mStarting(false) + , mStopping(false) + { + } + + ~Private() + { + } + + void cleanup() + { + } + + bool exec(); + void serverStateChanged(ServerManager::State state); + + QPointer mParent; + QEventLoop *mEventLoop; + bool mSuccess; + + bool mStarting; + bool mStopping; +}; + +bool Control::Private::exec() +{ + qCDebug(AKONADICORE_LOG) << "Starting/Stopping Akonadi (using an event loop)."; + mEventLoop = new QEventLoop(mParent); + mEventLoop->exec(); + mEventLoop->deleteLater(); + mEventLoop = 0; + + if (!mSuccess) { + qCWarning(AKONADICORE_LOG) << "Could not start/stop Akonadi!"; + } + + mStarting = false; + mStopping = false; + + const bool rv = mSuccess; + mSuccess = false; + return rv; +} + +void Control::Private::serverStateChanged(ServerManager::State state) +{ + qCDebug(AKONADICORE_LOG) << state; + if (mEventLoop && mEventLoop->isRunning()) { + // ignore transient states going into the right direction + if ((mStarting && (state == ServerManager::Starting || state == ServerManager::Upgrading)) || + (mStopping && state == ServerManager::Stopping)) { + return; + } + mEventLoop->quit(); + mSuccess = (mStarting && state == ServerManager::Running) || (mStopping && state == ServerManager::NotRunning); + } +} + +Control::Control() + : d(new Private(this)) +{ + connect(ServerManager::self(), SIGNAL(stateChanged(Akonadi::ServerManager::State)), + SLOT(serverStateChanged(Akonadi::ServerManager::State))); + // mProgressIndicator is a widget, so it better be deleted before the QApplication is deleted + // Otherwise we get a crash in QCursor code with Qt-4.5 + if (QCoreApplication::instance()) { + connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(cleanup())); + } +} + +Control::~Control() +{ + delete d; +} + +bool Control::start() +{ + if (ServerManager::state() == ServerManager::Stopping) { + qCDebug(AKONADICORE_LOG) << "Server is currently being stopped, wont try to start it now"; + return false; + } + if (ServerManager::isRunning() || s_instance->d->mEventLoop) { + qCDebug(AKONADICORE_LOG) << "Server is already running"; + return true; + } + s_instance->d->mStarting = true; + if (!ServerManager::start()) { + qCDebug(AKONADICORE_LOG) << "ServerManager::start failed -> return false"; + return false; + } + return s_instance->d->exec(); +} + +bool Control::stop() +{ + if (ServerManager::state() == ServerManager::Starting) { + return false; + } + if (!ServerManager::isRunning() || s_instance->d->mEventLoop) { + return true; + } + s_instance->d->mStopping = true; + if (!ServerManager::stop()) { + return false; + } + return s_instance->d->exec(); +} + +bool Control::restart() +{ + if (ServerManager::isRunning()) { + if (!stop()) { + return false; + } + } + return start(); +} + +} + +#include "moc_control.cpp" diff --git a/src/core/control.h b/src/core/control.h new file mode 100644 index 0000000..7f65a48 --- /dev/null +++ b/src/core/control.h @@ -0,0 +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_CONTROL_H +#define AKONADI_CONTROL_H + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ + +/** + * @short Provides methods to control the Akonadi server process. + * + * This class provides synchronous methods (ie. use a sub-eventloop) + * to control the Akonadi service. For asynchronous methods see + * Akonadi::ServerManager. + * + * The most important method in here is widgetNeedsAkonadi(). It is + * recommended to call it with every top-level widget of your application + * as argument, assuming your application relies on Akonadi being operational + * of course. + * + * While the Akonadi server automatically started by Akonadi::Session + * on first use, it might be necessary for some use-cases to guarantee + * a running Akonadi service at some point. This can be done using + * start(). + * + * Example: + * + * @code + * + * if ( !Akonadi::Control::start() ) { + * qDebug() << "Unable to start Akonadi server, exit application"; + * return 1; + * } else { + * ... + * } + * + * @endcode + * + * @author Volker Krause + * + * @see Akonadi::ServerManager + */ +class AKONADICORE_EXPORT Control : public QObject +{ + Q_OBJECT + +public: + /** + * Destroys the control object. + */ + ~Control(); + + /** + * Starts the Akonadi server synchronously if it is not already running. + * @return @c true if the server was started successfully or was already + * running, @c false otherwise + */ + static bool start(); + + /** + * Stops the Akonadi server synchronously if it is currently running. + * @return @c true if the server was shutdown successfully or was + * not running at all, @c false otherwise. + * @since 4.2 + */ + static bool stop(); + + /** + * Restarts the Akonadi server synchronously. + * @return @c true if the restart was successful, @c false otherwise, + * the server state is undefined in this case. + * @since 4.2 + */ + static bool restart(); + +protected: + /** + * Creates the control object. + */ + Control(); + +private: + //@cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void serverStateChanged(Akonadi::ServerManager::State)) + Q_PRIVATE_SLOT(d, void cleanup()) + //@endcond +}; + +} + +#endif diff --git a/src/core/differencesalgorithminterface.h b/src/core/differencesalgorithminterface.h new file mode 100644 index 0000000..18430f7 --- /dev/null +++ b/src/core/differencesalgorithminterface.h @@ -0,0 +1,65 @@ +/* + Copyright (c) 2010 KDAB + Author: 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 DIFFERENCESALGORITHMINTERFACE_P_H +#define DIFFERENCESALGORITHMINTERFACE_P_H + +#include + +namespace Akonadi +{ + +class AbstractDifferencesReporter; +class Item; + +/** + * @short An interface to find out differences between two Akonadi objects. + * + * @author Tobias Koenig + * @since 4.6 + */ +class DifferencesAlgorithmInterface +{ +public: + /** + * Destroys the differences algorithm interface. + */ + virtual ~DifferencesAlgorithmInterface() + { + } + + /** + * Calculates the differences between two Akonadi objects and reports + * them to a reporter object. + * + * @param reporter The reporter object that will be used for reporting the differences. + * @param leftItem The left-hand side item that will be compared. + * @param rightItem The right-hand side item that will be compared. + */ + virtual void compare(AbstractDifferencesReporter *reporter, + const Akonadi::Item &leftItem, + const Akonadi::Item &rightItem) = 0; +}; + +} + +Q_DECLARE_INTERFACE(Akonadi::DifferencesAlgorithmInterface, "org.freedesktop.Akonadi.DifferencesAlgorithmInterface/1.0") + +#endif diff --git a/src/core/entityannotationsattribute.cpp b/src/core/entityannotationsattribute.cpp new file mode 100644 index 0000000..942d326 --- /dev/null +++ b/src/core/entityannotationsattribute.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2008 Omat Holding B.V. + * Copyright (C) 2014 Christian Mollekopf + * + * 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 "entityannotationsattribute.h" + +#include +#include + +using namespace Akonadi; + +EntityAnnotationsAttribute::EntityAnnotationsAttribute() +{ +} + +EntityAnnotationsAttribute::EntityAnnotationsAttribute(const QMap &annotations) + : mAnnotations(annotations) +{ +} + +void EntityAnnotationsAttribute::setAnnotations(const QMap &annotations) +{ + mAnnotations = annotations; +} + +QMap EntityAnnotationsAttribute::annotations() const +{ + return mAnnotations; +} + +void EntityAnnotationsAttribute::insert(const QByteArray &key, const QString &value) +{ + mAnnotations.insert(key, value.toUtf8()); +} + +QString EntityAnnotationsAttribute::value(const QByteArray &key) +{ + return QString::fromUtf8(mAnnotations.value(key).data()); +} + +bool EntityAnnotationsAttribute::contains(const QByteArray &key) const +{ + return mAnnotations.contains(key); +} + +QByteArray EntityAnnotationsAttribute::type() const +{ + static const QByteArray sType("entityannotations"); + return sType; +} + +Akonadi::Attribute *EntityAnnotationsAttribute::clone() const +{ + return new EntityAnnotationsAttribute(mAnnotations); +} + +QByteArray EntityAnnotationsAttribute::serialized() const +{ + QByteArray result = ""; + + for (auto it = mAnnotations.cbegin(), e = mAnnotations.cend(); it != e; ++it) { + result += it.key(); + result += ' '; + result += it.value(); + result += " % "; // We use this separator as '%' is not allowed in keys or values + } + result.chop(3); + + return result; +} + +void EntityAnnotationsAttribute::deserialize(const QByteArray &data) +{ + mAnnotations.clear(); + const QList lines = data.split('%'); + + for (int i = 0; i < lines.size(); ++i) { + QByteArray line = lines[i]; + if (i != 0 && line.startsWith(' ')) { + line = line.mid(1); + } + if (i != lines.size() - 1 && line.endsWith(' ')) { + line.chop(1); + } + if (line.trimmed().isEmpty()) { + continue; + } + int wsIndex = line.indexOf(' '); + if (wsIndex > 0) { + const QByteArray key = line.mid(0, wsIndex); + const QByteArray value = line.mid(wsIndex + 1); + mAnnotations[key] = value; + } else { + mAnnotations.insert(line, QByteArray()); + } + } +} diff --git a/src/core/entityannotationsattribute.h b/src/core/entityannotationsattribute.h new file mode 100644 index 0000000..5a4ef71 --- /dev/null +++ b/src/core/entityannotationsattribute.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2008 Omat Holding B.V. + * Copyright (C) 2014 Christian Mollekopf + * + * 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_ENTITYANNOTATIONSATTRIBUTE_H +#define AKONADI_ENTITYANNOTATIONSATTRIBUTE_H + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +namespace Akonadi +{ + +/** + * An attribute for annotations. + * + * The attribute is inspired by RFC5257(IMAP ANNOTATION) and RFC5464(IMAP METADATA), but serves + * the purpose of RFC5257. + * + * For a private note annotation the entry name is: + * /private/comment + * for a shared note: + * /shared/comment + * + * @since 4.13 + */ +class AKONADICORE_EXPORT EntityAnnotationsAttribute : public Akonadi::Attribute +{ +public: + EntityAnnotationsAttribute(); + EntityAnnotationsAttribute(const QMap &annotations); + + void setAnnotations(const QMap &annotations); + QMap annotations() const; + + void insert(const QByteArray &key, const QString &value); + QString value(const QByteArray &key); + bool contains(const QByteArray &key) const; + + QByteArray type() const Q_DECL_OVERRIDE; + Attribute *clone() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + +private: + QMap mAnnotations; +}; + +} + +#endif diff --git a/src/core/entitycache.cpp b/src/core/entitycache.cpp new file mode 100644 index 0000000..d651f47 --- /dev/null +++ b/src/core/entitycache.cpp @@ -0,0 +1,35 @@ +/* + 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_p.h" + +using namespace Akonadi; + +EntityCacheBase::EntityCacheBase(Session *_session, QObject *parent) + : QObject(parent) + , session(_session) +{ +} + +void EntityCacheBase::setSession(Session *_session) +{ + session = _session; +} + +#include "moc_entitycache_p.cpp" diff --git a/src/core/entitycache_p.h b/src/core/entitycache_p.h new file mode 100644 index 0000000..8142c82 --- /dev/null +++ b/src/core/entitycache_p.h @@ -0,0 +1,544 @@ +/* + 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. +*/ + +#ifndef AKONADI_ENTITYCACHE_P_H +#define AKONADI_ENTITYCACHE_P_H + +#include "item.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "collection.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "tag.h" +#include "tagfetchjob.h" +#include "tagfetchscope.h" +#include "session.h" + +#include "akonaditests_export.h" + +#include +#include +#include +#include +#include + +class KJob; + +Q_DECLARE_METATYPE(QList) + +namespace Akonadi +{ + +/** + @internal + QObject part of EntityCache. +*/ +class AKONADI_TESTS_EXPORT EntityCacheBase : public QObject +{ + Q_OBJECT +public: + explicit EntityCacheBase(Session *session, QObject *parent = 0); + + void setSession(Session *session); + +protected: + Session *session; + +Q_SIGNALS: + void dataAvailable(); + +private Q_SLOTS: + virtual void processResult(KJob *job) = 0; +}; + +template +struct EntityCacheNode { + EntityCacheNode() + : pending(false) + , invalid(false) + { + } + EntityCacheNode(typename T::Id id) + : entity(T(id)) + , pending(true) + , invalid(false) + { + } + T entity; + bool pending; + bool invalid; +}; + +/** + * @internal + * A in-memory FIFO cache for a small amount of Item or Collection objects. + */ +template +class EntityCache : public EntityCacheBase +{ +public: + typedef FetchScope_ FetchScope; + explicit EntityCache(int maxCapacity, Session *session = 0, QObject *parent = 0) + : EntityCacheBase(session, parent) + , mCapacity(maxCapacity) + { + } + + ~EntityCache() + { + qDeleteAll(mCache); + } + + /** Object is available in the cache and can be retrieved. */ + bool isCached(typename T::Id id) const + { + EntityCacheNode *node = cacheNodeForId(id); + return node && !node->pending; + } + + /** Object has been requested but is not yet loaded into the cache or is already available. */ + bool isRequested(typename T::Id id) const + { + return cacheNodeForId(id); + } + + /** Returns the cached object if available, an empty instance otherwise. */ + virtual T retrieve(typename T::Id id) const + { + EntityCacheNode *node = cacheNodeForId(id); + if (node && !node->pending && !node->invalid) { + return node->entity; + } + return T(); + } + + /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */ + void invalidate(typename T::Id id) + { + EntityCacheNode *node = cacheNodeForId(id); + if (node) { + node->invalid = true; + } + } + + /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */ + void update(typename T::Id id, const FetchScope &scope) + { + EntityCacheNode *node = cacheNodeForId(id); + if (node) { + mCache.removeAll(node); + if (node->pending) { + request(id, scope); + } + delete node; + } + } + + /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */ + virtual bool ensureCached(typename T::Id id, const FetchScope &scope) + { + EntityCacheNode *node = cacheNodeForId(id); + if (!node) { + request(id, scope); + return false; + } + return !node->pending; + } + + /** + Asks the cache to retrieve @p id. @p request is used as + a token to indicate which request has been finished in the + dataAvailable() signal. + */ + virtual void request(typename T::Id id, const FetchScope &scope) + { + Q_ASSERT(!isRequested(id)); + shrinkCache(); + EntityCacheNode *node = new EntityCacheNode(id); + FetchJob *job = createFetchJob(id, scope); + job->setProperty("EntityCacheNode", QVariant::fromValue(id)); + connect(job, SIGNAL(result(KJob *)), SLOT(processResult(KJob *))); + mCache.enqueue(node); + } + +private: + EntityCacheNode *cacheNodeForId(typename T::Id id) const + { + for (typename QQueue *>::const_iterator it = mCache.constBegin(), endIt = mCache.constEnd(); + it != endIt; ++it) { + if ((*it)->entity.id() == id) { + return *it; + } + } + return 0; + } + + void processResult(KJob *job) Q_DECL_OVERRIDE { + if (job->error()) + { + //This can happen if we have stale notifications for items that have already been removed + } + typename T::Id id = job->property("EntityCacheNode").template value(); + EntityCacheNode *node = cacheNodeForId(id); + if (!node) + { + return; // got replaced in the meantime + } + + node->pending = false; + extractResult(node, job); + // make sure we find this node again if something went wrong here, + // most likely the object got deleted from the server in the meantime + if (node->entity.id() != id) + { + // TODO: Recursion guard? If this is called with non-existing ids, the if will never be true! + node->entity.setId(id); + node->invalid = true; + } + emit dataAvailable(); + } + + void extractResult(EntityCacheNode *node, KJob *job) const; + + inline FetchJob *createFetchJob(typename T::Id id, const FetchScope &scope) + { + FetchJob *fetch = new FetchJob(T(id), session); + fetch->setFetchScope(scope); + return fetch; + } + + /** Tries to reduce the cache size until at least one more object fits in. */ + void shrinkCache() + { + while (mCache.size() >= mCapacity && !mCache.first()->pending) { + delete mCache.dequeue(); + } + } + +private: + QQueue *> mCache; + int mCapacity; +}; + +template<> inline void EntityCache::extractResult(EntityCacheNode *node, KJob *job) const +{ + CollectionFetchJob *fetch = qobject_cast(job); + Q_ASSERT(fetch); + if (fetch->collections().isEmpty()) { + node->entity = Collection(); + } else { + node->entity = fetch->collections().at(0); + } +} + +template<> inline void EntityCache::extractResult(EntityCacheNode *node, KJob *job) const +{ + ItemFetchJob *fetch = qobject_cast(job); + Q_ASSERT(fetch); + if (fetch->items().isEmpty()) { + node->entity = Item(); + } else { + node->entity = fetch->items().at(0); + } +} + +template<> inline void EntityCache::extractResult(EntityCacheNode *node, KJob *job) const +{ + TagFetchJob *fetch = qobject_cast(job); + Q_ASSERT(fetch); + if (fetch->tags().isEmpty()) { + node->entity = Tag(); + } else { + node->entity = fetch->tags().at(0); + } +} + +template<> inline CollectionFetchJob *EntityCache::createFetchJob(Collection::Id id, const CollectionFetchScope &scope) +{ + CollectionFetchJob *fetch = new CollectionFetchJob(Collection(id), CollectionFetchJob::Base, session); + fetch->setFetchScope(scope); + return fetch; +} + +typedef EntityCache CollectionCache; +typedef EntityCache ItemCache; +typedef EntityCache TagCache; + +template +struct EntityListCacheNode { + EntityListCacheNode() + : pending(false) + , invalid(false) + { + } + EntityListCacheNode(typename T::Id id) + : entity(id) + , pending(true) + , invalid(false) + { + } + + T entity; + bool pending; + bool invalid; +}; + +template +class EntityListCache : public EntityCacheBase +{ +public: + typedef FetchScope_ FetchScope; + + explicit EntityListCache(int maxCapacity, Session *session = 0, QObject *parent = 0) + : EntityCacheBase(session, parent) + , mCapacity(maxCapacity) + { + } + + ~EntityListCache() + { + qDeleteAll(mCache); + } + + /** Returns the cached object if available, an empty instance otherwise. */ + typename T::List retrieve(const QList &ids) const + { + typename T::List list; + + Q_FOREACH (typename T::Id id, ids) { + EntityListCacheNode *node = mCache.value(id); + if (!node || node->pending || node->invalid) { + return typename T::List(); + } + + list << node->entity; + } + + return list; + } + + /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */ + bool ensureCached(const QList &ids, const FetchScope &scope) + { + QList toRequest; + bool result = true; + + Q_FOREACH (typename T::Id id, ids) { + EntityListCacheNode *node = mCache.value(id); + if (!node) { + toRequest << id; + continue; + } + + if (node->pending) { + result = false; + } + } + + if (!toRequest.isEmpty()) { + request(toRequest, scope, ids); + return false; + } + + return result; + } + + /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */ + void invalidate(const QList &ids) + { + Q_FOREACH (typename T::Id id, ids) { + EntityListCacheNode *node = mCache.value(id); + if (node) { + node->invalid = true; + } + } + } + + /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */ + void update(const QList &ids, const FetchScope &scope) + { + QList toRequest; + + Q_FOREACH (typename T::Id id, ids) { + EntityListCacheNode *node = mCache.value(id); + if (node) { + mCache.remove(id); + if (node->pending) { + toRequest << id; + } + delete node; + } + } + + if (!toRequest.isEmpty()) { + request(toRequest, scope); + } + } + + /** + Asks the cache to retrieve @p id. @p request is used as + a token to indicate which request has been finished in the + dataAvailable() signal. + */ + void request(const QList &ids, const FetchScope &scope, + const QList &preserveIds = QList()) + { + Q_ASSERT(isNotRequested(ids)); + shrinkCache(preserveIds); + Q_FOREACH (typename T::Id id, ids) { + EntityListCacheNode *node = new EntityListCacheNode(id); + mCache.insert(id, node); + } + FetchJob *job = createFetchJob(ids, scope); + job->setProperty("EntityListCacheIds", QVariant::fromValue>(ids)); + connect(job, SIGNAL(result(KJob *)), SLOT(processResult(KJob *))); + } + + bool isNotRequested(const QList &ids) const + { + Q_FOREACH (typename T::Id id, ids) { + if (mCache.contains(id)) { + return false; + } + } + + return true; + } + + /** Object is available in the cache and can be retrieved. */ + bool isCached(const QList &ids) const + { + Q_FOREACH (typename T::Id id, ids) { + EntityListCacheNode *node = mCache.value(id); + if (!node || node->pending) { + return false; + } + } + return true; + } + +private: + /** Tries to reduce the cache size until at least one more object fits in. */ + void shrinkCache(const QList &preserveIds) + { + typename QHash *>::Iterator iter = mCache.begin(); + while (iter != mCache.end() && mCache.size() >= mCapacity) { + if (iter.value()->pending || preserveIds.contains(iter.key())) { + ++iter; + continue; + } + + delete iter.value(); + iter = mCache.erase(iter); + } + } + + inline FetchJob *createFetchJob(const QList &ids, const FetchScope &scope) + { + FetchJob *job = new FetchJob(ids, session); + job->setFetchScope(scope); + return job; + } + + void processResult(KJob *job) Q_DECL_OVERRIDE + { + if (job->error()) { + qWarning() << job->errorString(); + } + const QList ids = job->property("EntityListCacheIds").value>(); + + typename T::List entities; + extractResults(job, entities); + + Q_FOREACH (typename T::Id id, ids) + { + EntityListCacheNode *node = mCache.value(id); + if (!node) { + continue; // got replaced in the meantime + } + + node->pending = false; + + T result; + typename T::List::Iterator iter = entities.begin(); + for (; iter != entities.end(); ++iter) { + if ((*iter).id() == id) { + result = *iter; + entities.erase(iter); + break; + } + } + + // make sure we find this node again if something went wrong here, + // most likely the object got deleted from the server in the meantime + if (!result.isValid()) { + node->entity = T(id); + node->invalid = true; + } else { + node->entity = result; + } + } + + emit dataAvailable(); + } + + void extractResults(KJob *job, typename T::List &entities) const; + +private: + QHash *> mCache; + int mCapacity; +}; + +template<> inline void EntityListCache::extractResults(KJob *job, Collection::List &collections) const +{ + CollectionFetchJob *fetch = qobject_cast(job); + Q_ASSERT(fetch); + collections = fetch->collections(); +} + +template<> inline void EntityListCache::extractResults(KJob *job, Item::List &items) const +{ + ItemFetchJob *fetch = qobject_cast(job); + Q_ASSERT(fetch); + items = fetch->items(); +} + +template<> inline void EntityListCache::extractResults(KJob *job, Tag::List &tags) const +{ + TagFetchJob *fetch = qobject_cast(job); + Q_ASSERT(fetch); + tags = fetch->tags(); +} + +template<> +inline CollectionFetchJob *EntityListCache::createFetchJob(const QList &ids, const CollectionFetchScope &scope) +{ + CollectionFetchJob *fetch = new CollectionFetchJob(ids, CollectionFetchJob::Base, session); + fetch->setFetchScope(scope); + return fetch; +} + +typedef EntityListCache CollectionListCache; +typedef EntityListCache ItemListCache; +typedef EntityListCache TagListCache; + +} + +#endif diff --git a/src/core/entitydeletedattribute.cpp b/src/core/entitydeletedattribute.cpp new file mode 100644 index 0000000..469cde8 --- /dev/null +++ b/src/core/entitydeletedattribute.cpp @@ -0,0 +1,131 @@ +/* + Copyright (c) 2011 Christian Mollekopf + + 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 "entitydeletedattribute.h" + +#include "private/imapparser_p.h" + +#include "akonadicore_debug.h" + +#include +#include + +using namespace Akonadi; + +class EntityDeletedAttribute::EntityDeletedAttributePrivate +{ +public: + EntityDeletedAttributePrivate() {} + + Collection restoreCollection; + QString restoreResource; +}; + +EntityDeletedAttribute::EntityDeletedAttribute() + : d_ptr(new EntityDeletedAttributePrivate()) +{ + +} + +EntityDeletedAttribute::~EntityDeletedAttribute() +{ + delete d_ptr; +} + +void EntityDeletedAttribute::setRestoreCollection(const Akonadi::Collection &collection) +{ + Q_D(EntityDeletedAttribute); + if (!collection.isValid()) { + qCWarning(AKONADICORE_LOG) << "invalid collection" << collection; + } + Q_ASSERT(collection.isValid()); + d->restoreCollection = collection; + if (collection.resource().isEmpty()) { + qCWarning(AKONADICORE_LOG) << "no resource set"; + } + d->restoreResource = collection.resource(); +} + +Collection EntityDeletedAttribute::restoreCollection() const +{ + Q_D(const EntityDeletedAttribute); + return d->restoreCollection; +} + +QString EntityDeletedAttribute::restoreResource() const +{ + Q_D(const EntityDeletedAttribute); + return d->restoreResource; +} + +QByteArray Akonadi::EntityDeletedAttribute::type() const +{ + static const QByteArray sType("DELETED"); + return sType; +} + +EntityDeletedAttribute *EntityDeletedAttribute::clone() const +{ + const Q_D(EntityDeletedAttribute); + EntityDeletedAttribute *attr = new EntityDeletedAttribute(); + attr->d_ptr->restoreCollection = d->restoreCollection; + attr->d_ptr->restoreResource = d->restoreResource; + return attr; +} + +QByteArray EntityDeletedAttribute::serialized() const +{ + const Q_D(EntityDeletedAttribute); + + QList l; + l << ImapParser::quote(d->restoreResource.toUtf8()); + QList components; + components << QByteArray::number(d->restoreCollection.id()); + + l << '(' + ImapParser::join(components, " ") + ')'; + return '(' + ImapParser::join(l, " ") + ')'; +} + +void EntityDeletedAttribute::deserialize(const QByteArray &data) +{ + Q_D(EntityDeletedAttribute); + + QList l; + ImapParser::parseParenthesizedList(data, l); + if (l.size() != 2) { + qCWarning(AKONADICORE_LOG) << "invalid size"; + return; + } + d->restoreResource = QString::fromUtf8(l[0]); + + if (!l[1].isEmpty()) { + QList componentData; + ImapParser::parseParenthesizedList(l[1], componentData); + if (componentData.size() != 1) { + return; + } + QList components; + bool ok; + components << componentData.at(0).toInt(&ok); + if (!ok) { + return; + } + d->restoreCollection = Collection(components.at(0)); + } +} diff --git a/src/core/entitydeletedattribute.h b/src/core/entitydeletedattribute.h new file mode 100644 index 0000000..1920f9c --- /dev/null +++ b/src/core/entitydeletedattribute.h @@ -0,0 +1,105 @@ +/* + Copyright (c) 2011 Christian Mollekopf + + 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_ENTITYDELETEDATTRIBUTE_H +#define AKONADI_ENTITYDELETEDATTRIBUTE_H + +#include "akonadicore_export.h" +#include "attribute.h" +#include "collection.h" + +namespace Akonadi +{ + +/** + * @short An Attribute that marks that an entity was marked as deleted + * + * This class represents the attribute of all hidden items. The hidden + * items shouldn't be displayed in UI applications (unless in some kind + * of "debug" mode). + * + * Example: + * + * @code + * + * @endcode + * + * @author Christian Mollekopf + * @see Akonadi::Attribute + * @since 4.8 + */ +class AKONADICORE_EXPORT EntityDeletedAttribute : public Attribute +{ +public: + /** + * Creates a new entity deleted attribute. + */ + EntityDeletedAttribute(); + + /** + * Destroys the entity deleted attribute. + */ + ~EntityDeletedAttribute(); + /** + * Sets the collection used to restore items which have been moved to trash using a TrashJob + * If the Resource is set on the collection, the resource root will be used as fallback during the restore operation. + */ + void setRestoreCollection(const Collection &col); + + /** + * Returns the original collection of an item that has been moved to trash using a TrashJob + */ + Collection restoreCollection() const; + + /** + * Returns the resource of the restoreCollection + */ + QString restoreResource() const; + + /** + * Reimplemented from Attribute + */ + QByteArray type() const Q_DECL_OVERRIDE; + + /** + * Reimplemented from Attribute + */ + EntityDeletedAttribute *clone() const Q_DECL_OVERRIDE; + + /** + * Reimplemented from Attribute + */ + QByteArray serialized() const Q_DECL_OVERRIDE; + + /** + * Reimplemented from Attribute + */ + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class EntityDeletedAttributePrivate; + EntityDeletedAttributePrivate *const d_ptr; + Q_DECLARE_PRIVATE(EntityDeletedAttribute) + //@endcond +}; + +} + +#endif diff --git a/src/core/entitydisplayattribute.cpp b/src/core/entitydisplayattribute.cpp new file mode 100644 index 0000000..6af7ee0 --- /dev/null +++ b/src/core/entitydisplayattribute.cpp @@ -0,0 +1,166 @@ +/* + 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 "entitydisplayattribute.h" + +#include "private/imapparser_p.h" + +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN EntityDisplayAttribute::Private +{ +public: + Private() + : hidden(false) + { + } + QString name; + QString icon; + QString activeIcon; + QColor backgroundColor; + bool hidden; +}; + +EntityDisplayAttribute::EntityDisplayAttribute() + : d(new Private) +{ +} + +EntityDisplayAttribute::~ EntityDisplayAttribute() +{ + delete d; +} + +QString EntityDisplayAttribute::displayName() const +{ + return d->name; +} + +void EntityDisplayAttribute::setDisplayName(const QString &name) +{ + d->name = name; +} + +QIcon EntityDisplayAttribute::icon() const +{ + return QIcon::fromTheme(d->icon); +} + +QString EntityDisplayAttribute::iconName() const +{ + return d->icon; +} + +void EntityDisplayAttribute::setIconName(const QString &icon) +{ + d->icon = icon; +} + +QByteArray Akonadi::EntityDisplayAttribute::type() const +{ + static const QByteArray sType("ENTITYDISPLAY"); + return sType; +} + +EntityDisplayAttribute *EntityDisplayAttribute::clone() const +{ + EntityDisplayAttribute *attr = new EntityDisplayAttribute(); + attr->d->name = d->name; + attr->d->icon = d->icon; + attr->d->activeIcon = d->activeIcon; + attr->d->backgroundColor = d->backgroundColor; + return attr; +} + +QByteArray EntityDisplayAttribute::serialized() const +{ + QList l; + l << ImapParser::quote(d->name.toUtf8()); + l << ImapParser::quote(d->icon.toUtf8()); + l << ImapParser::quote(d->activeIcon.toUtf8()); + QList components; + if (d->backgroundColor.isValid()) { + components = QList() << QByteArray::number(d->backgroundColor.red()) + << QByteArray::number(d->backgroundColor.green()) + << QByteArray::number(d->backgroundColor.blue()) + << QByteArray::number(d->backgroundColor.alpha()); + } + l << '(' + ImapParser::join(components, " ") + ')'; + return '(' + ImapParser::join(l, " ") + ')'; +} + +void EntityDisplayAttribute::deserialize(const QByteArray &data) +{ + QList l; + ImapParser::parseParenthesizedList(data, l); + int size = l.size(); + Q_ASSERT(size >= 2); + d->name = QString::fromUtf8(l[0]); + d->icon = QString::fromUtf8(l[1]); + if (size >= 3) { + d->activeIcon = QString::fromUtf8(l[2]); + } + if (size >= 4) { + if (!l[3].isEmpty()) { + QList componentData; + ImapParser::parseParenthesizedList(l[3], componentData); + if (componentData.size() != 4) { + return; + } + QList components; + components.reserve(4); + + bool ok; + for (int i = 0; i <= 3; ++i) { + components << componentData.at(i).toInt(&ok); + if (!ok) { + return; + } + } + d->backgroundColor = QColor(components.at(0), components.at(1), components.at(2), components.at(3)); + } + } +} + +void EntityDisplayAttribute::setActiveIconName(const QString &name) +{ + d->activeIcon = name; +} + +QIcon EntityDisplayAttribute::activeIcon() const +{ + return QIcon::fromTheme(d->activeIcon); +} + +QString EntityDisplayAttribute::activeIconName() const +{ + return d->activeIcon; +} + +QColor EntityDisplayAttribute::backgroundColor() const +{ + return d->backgroundColor; +} + +void EntityDisplayAttribute::setBackgroundColor(const QColor &color) +{ + d->backgroundColor = color; +} diff --git a/src/core/entitydisplayattribute.h b/src/core/entitydisplayattribute.h new file mode 100644 index 0000000..0773ac3 --- /dev/null +++ b/src/core/entitydisplayattribute.h @@ -0,0 +1,126 @@ +/* + 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_ENTITYDISPLAYATTRIBUTE_H +#define AKONADI_ENTITYDISPLAYATTRIBUTE_H + +#include "akonadicore_export.h" +#include "attribute.h" + +#include +#include + +namespace Akonadi +{ + +/** + * @short Attribute that stores the properties that are used to display an entity. + * + * Display properties of a collection or item, such as translated names and icons. + * + * @author Volker Krause + * @since 4.2 + */ +class AKONADICORE_EXPORT EntityDisplayAttribute : public Attribute +{ +public: + /** + * Creates a new entity display attribute. + */ + EntityDisplayAttribute(); + + /** + * Destroys the entity display attribute. + */ + ~EntityDisplayAttribute(); + + /** + * Sets the @p name that should be used for display. + */ + void setDisplayName(const QString &name); + + /** + * Returns the name that should be used for display. + * Users of this should fall back to Collection::name() if this is empty. + */ + QString displayName() const; + + /** + * Sets the icon @p name for the default icon. + */ + void setIconName(const QString &name); + + /** + * Returns the icon that should be used for this collection or item. + */ + QIcon icon() const; + + /** + * Returns the icon name of the icon returned by icon(). + */ + QString iconName() const; + + /** + * Sets the icon @p name for the active icon. + * @param name the icon name to use + * @since 4.4 + */ + void setActiveIconName(const QString &name); + + /** + * Returns the icon that should be used for this collection or item when active. + * @since 4.4 + */ + QIcon activeIcon() const; + + /** + * Returns the icon name of an active item. + * @since 4.4 + */ + QString activeIconName() const; + + /** + * Returns the backgroundColor or an invalid color if none is set. + * @since 4.4 + */ + QColor backgroundColor() const; + + /** + * Sets the backgroundColor to @p color. + * @param color the background color to use + * @since 4.4 + */ + void setBackgroundColor(const QColor &color); + + /* reimpl */ + QByteArray type() const Q_DECL_OVERRIDE; + EntityDisplayAttribute *clone() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/entityhiddenattribute.cpp b/src/core/entityhiddenattribute.cpp new file mode 100644 index 0000000..854f1d8 --- /dev/null +++ b/src/core/entityhiddenattribute.cpp @@ -0,0 +1,57 @@ +/****************************************************************************** + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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 "entityhiddenattribute.h" + +#include + +using namespace Akonadi; + +EntityHiddenAttribute::EntityHiddenAttribute() + : d(0) +{ +} + +EntityHiddenAttribute::~EntityHiddenAttribute() +{ +} + +QByteArray Akonadi::EntityHiddenAttribute::type() const +{ + static const QByteArray sType("HIDDEN"); + return sType; +} + +EntityHiddenAttribute *EntityHiddenAttribute::clone() const +{ + return new EntityHiddenAttribute(); +} + +QByteArray EntityHiddenAttribute::serialized() const +{ + return QByteArray(); +} + +void EntityHiddenAttribute::deserialize(const QByteArray &data) +{ + Q_ASSERT(data.isEmpty()); + Q_UNUSED(data); +} diff --git a/src/core/entityhiddenattribute.h b/src/core/entityhiddenattribute.h new file mode 100644 index 0000000..2534b8c --- /dev/null +++ b/src/core/entityhiddenattribute.h @@ -0,0 +1,104 @@ +/****************************************************************************** + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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_ENTITYHIDDENATTRIBUTE_H +#define AKONADI_ENTITYHIDDENATTRIBUTE_H + +#include "akonadicore_export.h" +#include "attribute.h" + +namespace Akonadi +{ + +/** + * @short An Attribute that marks that an entity should be hidden in the UI. + * + * This class represents the attribute of all hidden items. The hidden + * items shouldn't be displayed in UI applications (unless in some kind + * of "debug" mode). + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * ... + * // hide a collection by setting the hidden attribute + * Collection collection = collectionFetchJob->collections().at(0); + * collection.attribute( Collection::AddIfMissing ); + * new CollectionModifyJob( collection, this ); // save back to storage + * + * // check if the collection is hidden + * if ( collection.hasAttribute() ) + * qDebug() << "collection is hidden"; + * else + * qDebug() << "collection is visible"; + * + * @endcode + * + * @author Szymon Stefanek + * @see Akonadi::Attribute + * @since 4.4 + */ +class AKONADICORE_EXPORT EntityHiddenAttribute : public Attribute +{ +public: + /** + * Creates a new entity hidden attribute. + */ + EntityHiddenAttribute(); + + /** + * Destroys the entity hidden attribute. + */ + ~EntityHiddenAttribute(); + + /** + * Reimplemented from Attribute + */ + QByteArray type() const Q_DECL_OVERRIDE; + + /** + * Reimplemented from Attribute + */ + EntityHiddenAttribute *clone() const Q_DECL_OVERRIDE; + + /** + * Reimplemented from Attribute + */ + QByteArray serialized() const Q_DECL_OVERRIDE; + + /** + * Reimplemented from Attribute + */ + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/exception.cpp b/src/core/exception.cpp new file mode 100644 index 0000000..6ad954b --- /dev/null +++ b/src/core/exception.cpp @@ -0,0 +1,126 @@ +/* + 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 "exception.h" + +#include + +#include + +using namespace Akonadi; + +class Exception::Private +{ +public: + QByteArray what; + QByteArray assembledWhat; +}; + +Exception::Exception(const char *what) throw() + : d(0) +{ + try { + std::unique_ptr nd(new Private); + nd->what = what; + d = nd.release(); + } catch (...) { + } +} + +Exception::Exception(const QByteArray &what) throw() + : d(0) +{ + try { + std::unique_ptr nd(new Private); + nd->what = what; + d = nd.release(); + } catch (...) { + } +} + +Exception::Exception(const QString &what) throw() + : d(0) +{ + try { + std::unique_ptr nd(new Private); + nd->what = what.toUtf8(); + d = nd.release(); + } catch (...) { + } +} + +Exception::Exception(const Akonadi::Exception &other) throw() + : std::exception(other) + , d(0) +{ + if (!other.d) { + return; + } + try { + std::unique_ptr nd(new Private(*other.d)); + d = nd.release(); + } catch (...) { + } +} + +Exception::~Exception() throw() +{ + delete d; +} + +QByteArray Exception::type() const throw() +{ + static const char mytype[] = "Akonadi::Exception"; + try { + return QByteArray::fromRawData("Akonadi::Exception", sizeof(mytype) - 1); + } catch (...) { + return QByteArray(); + } +} + +const char *Exception::what() const throw() +{ + static const char fallback[] = ""; + if (!d) { + return fallback; + } + if (d->assembledWhat.isEmpty()) { + try { + d->assembledWhat = QByteArray(type() + ": " + d->what); + } catch (...) { + return "caught some exception while assembling Akonadi::Exception::what() return value"; + } + } + return d->assembledWhat.constData(); +} + +#define AKONADI_EXCEPTION_IMPLEMENT_TRIVIAL_INSTANCE( classname ) \ + Akonadi::classname::~classname() throw() {} \ + QByteArray Akonadi::classname::type() const throw() { \ + static const char mytype[] = "Akonadi::" #classname ; \ + try { \ + return QByteArray::fromRawData( mytype, sizeof (mytype)-1 ); \ + } catch ( ... ) { \ + return QByteArray(); \ + } \ + } + +AKONADI_EXCEPTION_IMPLEMENT_TRIVIAL_INSTANCE(PayloadException) + +#undef AKONADI_EXCEPTION_IMPLEMENT_TRIVIAL_INSTANCE diff --git a/src/core/exception.h b/src/core/exception.h new file mode 100644 index 0000000..5bbdbae --- /dev/null +++ b/src/core/exception.h @@ -0,0 +1,107 @@ +/* + Copyright (c) 2009 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_EXCEPTION_H +#define AKONADI_EXCEPTION_H + +// The std_exception.h file is generated at build-time and #includes C++ stdlib +// header "exception" by aboslute path. This is to workaround an include loop on +// case-insensitive systems, where #include includes our "Exception" +// fancy header instead of stdlib's exception, causing an endless loop of +// includes between "Exception" and "exception.h". +#include "std_exception.h" + +#include "akonadicore_export.h" + +class QByteArray; +class QString; + +namespace Akonadi +{ + +/** + Base class for exceptions used by the Akonadi library. +*/ +class AKONADICORE_EXPORT Exception : public std::exception //krazy:exclude=dpointer +{ +public: + /** + Creates a new exception with the error message @p what. + */ + Exception(const char *what) throw(); + + /** + Creates a new exception with the error message @p what. + */ + Exception(const QByteArray &what) throw(); + + /** + Creates a new exception with the error message @p what. + */ + Exception(const QString &what) throw(); + + /** + Copy constructor. + */ + Exception(const Exception &other) throw(); + + /** + Destructor. + */ + virtual ~Exception() throw(); + + /** + Returns the error message associated with this exception. + */ + const char *what() const throw(); + + /** + Returns the type of this exception. + */ + virtual QByteArray type() const throw(); // ### Akonadi 2: return const char * + +private: + class Private; + Private *d; +}; + +#define AKONADI_EXCEPTION_MAKE_TRIVIAL_INSTANCE( classname ) \ + class AKONADICORE_EXPORT classname : public Akonadi::Exception \ + { \ + public: \ + classname ( const char *what ) throw() : Akonadi::Exception( what ) \ + { \ + } \ + classname ( const QByteArray &what ) throw() : Akonadi::Exception( what ) \ + { \ + } \ + classname ( const QString &what ) throw() : Akonadi::Exception( what ) \ + { \ + } \ + ~classname() throw(); \ + QByteArray type() const throw(); \ + } + +AKONADI_EXCEPTION_MAKE_TRIVIAL_INSTANCE(PayloadException); + +#undef AKONADI_EXCEPTION_MAKE_TRIVIAL_INSTANCE + +} + +#endif diff --git a/src/core/firstrun.cpp b/src/core/firstrun.cpp new file mode 100644 index 0000000..cfc3d6d --- /dev/null +++ b/src/core/firstrun.cpp @@ -0,0 +1,229 @@ +/* + 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 "firstrun_p.h" +#include "KDBusConnectionPool" +#include "servermanager.h" + +#include "agentinstance.h" +#include "agentinstancecreatejob.h" +#include "agentmanager.h" +#include "agenttype.h" + +#include "akonadicore_debug.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +static const char FIRSTRUN_DBUSLOCK[] = "org.kde.Akonadi.Firstrun.lock"; + +using namespace Akonadi; + +Firstrun::Firstrun(QObject *parent) + : QObject(parent) + , mConfig(new KConfig(ServerManager::addNamespace(QStringLiteral("akonadi-firstrunrc")))) + , mCurrentDefault(0) + , mProcess(0) +{ + //The code in firstrun is not safe in multi-instance mode + Q_ASSERT(!ServerManager::hasInstanceIdentifier()); + if (ServerManager::hasInstanceIdentifier()) { + deleteLater(); + return; + } + qCDebug(AKONADICORE_LOG); + if (KDBusConnectionPool::threadConnection().registerService(QLatin1String(FIRSTRUN_DBUSLOCK))) { + findPendingDefaults(); + qCDebug(AKONADICORE_LOG) << mPendingDefaults; + setupNext(); + } else { + qCDebug(AKONADICORE_LOG) << "D-Bus lock found, so someone else does the work for us already."; + deleteLater(); + } +} + +Firstrun::~Firstrun() +{ + if (qApp) { + KDBusConnectionPool::threadConnection().unregisterService(QLatin1String(FIRSTRUN_DBUSLOCK)); + } + delete mConfig; + qCDebug(AKONADICORE_LOG) << "done"; +} + +void Firstrun::findPendingDefaults() +{ + const KConfigGroup cfg(mConfig, "ProcessedDefaults"); + const QStringList paths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("akonadi/firstrun"), QStandardPaths::LocateDirectory); + foreach (const QString &dirName, paths) { + const QStringList files = QDir(dirName).entryList(QDir::Files | QDir::Readable); + foreach (const QString &fileName, files) { + const QString fullName = dirName + QLatin1Char('/') + fileName; + KConfig c(fullName); + const QString id = KConfigGroup(&c, "Agent").readEntry("Id", QString()); + if (id.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Found invalid default configuration in " << fullName; + continue; + } + if (cfg.hasKey(id)) { + continue; + } + mPendingDefaults << fullName; + } + } + +} + +void Firstrun::setupNext() +{ + delete mCurrentDefault; + mCurrentDefault = 0; + + if (mPendingDefaults.isEmpty()) { + deleteLater(); + return; + } + + mCurrentDefault = new KConfig(mPendingDefaults.takeFirst()); + const KConfigGroup agentCfg = KConfigGroup(mCurrentDefault, "Agent"); + + AgentType type = AgentManager::self()->type(agentCfg.readEntry("Type", QString())); + if (!type.isValid()) { + qCCritical(AKONADICORE_LOG) << "Unable to obtain agent type for default resource agent configuration " << mCurrentDefault->name(); + setupNext(); + return; + } + if (type.capabilities().contains(QStringLiteral("Unique"))) { + Q_FOREACH (const AgentInstance &agent, AgentManager::self()->instances()) { + if (agent.type() == type) { + // remember we set this one up already + KConfigGroup cfg(mConfig, "ProcessedDefaults"); + cfg.writeEntry(agentCfg.readEntry("Id", QString()), agent.identifier()); + cfg.sync(); + setupNext(); + return; + } + } + } + + AgentInstanceCreateJob *job = new AgentInstanceCreateJob(type); + connect(job, &AgentInstanceCreateJob::result, this, &Firstrun::instanceCreated); + job->start(); +} + +void Firstrun::instanceCreated(KJob *job) +{ + Q_ASSERT(mCurrentDefault); + + if (job->error()) { + qCCritical(AKONADICORE_LOG) << "Creating agent instance failed for " << mCurrentDefault->name(); + setupNext(); + return; + } + + AgentInstance instance = static_cast(job)->instance(); + const KConfigGroup agentCfg = KConfigGroup(mCurrentDefault, "Agent"); + const QString agentName = agentCfg.readEntry("Name", QString()); + if (!agentName.isEmpty()) { + instance.setName(agentName); + } + + + QDBusInterface *iface = new QDBusInterface(QStringLiteral("org.freedesktop.Akonadi.Agent.%1").arg(instance.identifier()), + QStringLiteral("/Settings"), QString(), + KDBusConnectionPool::threadConnection(), this); + if (!iface->isValid()) { + qCCritical(AKONADICORE_LOG) << "Unable to obtain the KConfigXT D-Bus interface of " << instance.identifier(); + setupNext(); + delete iface; + return; + } + // agent specific settings, using the D-Bus <-> KConfigXT bridge + const KConfigGroup settings = KConfigGroup(mCurrentDefault, "Settings"); + + foreach (const QString &setting, settings.keyList()) { + qCDebug(AKONADICORE_LOG) << "Setting up " << setting << " for agent " << instance.identifier(); + const QString methodName = QStringLiteral("set%1").arg(setting); + const QVariant::Type argType = argumentType(iface->metaObject(), methodName); + if (argType == QVariant::Invalid) { + qCCritical(AKONADICORE_LOG) << "Setting " << setting << " not found in agent configuration interface of " << instance.identifier(); + continue; + } + + QVariant arg; + if (argType == QVariant::String) { + // Since a string could be a path we always use readPathEntry here, + // that shouldn't harm any normal string settings + arg = settings.readPathEntry(setting, QString()); + } else { + arg = settings.readEntry(setting, QVariant(argType)); + } + + const QDBusReply reply = iface->call(methodName, arg); + if (!reply.isValid()) { + qCCritical(AKONADICORE_LOG) << "Setting " << setting << " failed for agent " << instance.identifier(); + } + } + + iface->call(QStringLiteral("writeConfig")); + + instance.reconfigure(); + instance.synchronize(); + delete iface; + + // remember we set this one up already + KConfigGroup cfg(mConfig, "ProcessedDefaults"); + cfg.writeEntry(agentCfg.readEntry("Id", QString()), instance.identifier()); + cfg.sync(); + + setupNext(); +} + +QVariant::Type Firstrun::argumentType(const QMetaObject *mo, const QString &method) +{ + QMetaMethod m; + for (int i = 0; i < mo->methodCount(); ++i) { + const QString signature = QString::fromLatin1(mo->method(i).methodSignature()); + if (signature.startsWith(method)) { + m = mo->method(i); + } + } + + if (m.methodSignature().isEmpty()) { + return QVariant::Invalid; + } + + const QList argTypes = m.parameterTypes(); + if (argTypes.count() != 1) { + return QVariant::Invalid; + } + + return QVariant::nameToType(argTypes.first().constData()); +} + +#include "moc_firstrun_p.cpp" diff --git a/src/core/firstrun_p.h b/src/core/firstrun_p.h new file mode 100644 index 0000000..6020900 --- /dev/null +++ b/src/core/firstrun_p.h @@ -0,0 +1,92 @@ +/* + 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_FIRSTRUN_P_H +#define AKONADI_FIRSTRUN_P_H + +#include +#include +#include + +class KConfig; +class KJob; +class KProcess; +struct QMetaObject; + +namespace Akonadi +{ + +/** + Takes care of setting up default resource agents when running Akonadi for the first time. + +

Defining your own default agent setups

+ + To add an additional agent to the default Akonadi setup, add a file with the + agent setup description into /akonadi/firstrun. + + Such a file looks as follows: + + @verbatim + [Agent] + Id=defaultaddressbook + Type=akonadi_vcard_resource + Name=My Addressbook + + [Settings] + Path[$e]=~/.kde/share/apps/kabc/std.ics + AutosaveInterval=1 + @endverbatim + + The keys in the [Agent] group are mandatory: +
    +
  • Id: A unique identifier of the setup description, should never change to avoid the agent + being set up twice.
  • +
  • Type: The agent type
  • +
  • Name: The user visible name for this agent (only used for resource agents currently)
  • +
+ + The [Settings] group is optional and contains agent-dependent settings. + For those settings to be applied, the agent needs to export its settings + via D-Bus using the KConfigXT <-> D-Bus bridge. +*/ +class Firstrun : public QObject +{ + Q_OBJECT +public: + explicit Firstrun(QObject *parent = 0); + ~Firstrun(); + +private: + void findPendingDefaults(); + void setupNext(); + static QVariant::Type argumentType(const QMetaObject *mo, const QString &method); + +private Q_SLOTS: + void instanceCreated(KJob *job); + +private: + QStringList mPendingDefaults; + KConfig *mConfig; + KConfig *mCurrentDefault; + KProcess *mProcess; +}; + +} + +#endif diff --git a/src/core/gidextractor.cpp b/src/core/gidextractor.cpp new file mode 100644 index 0000000..abbff3e --- /dev/null +++ b/src/core/gidextractor.cpp @@ -0,0 +1,52 @@ +/* + Author: (2013) Christian Mollekopf + + 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 "gidextractor_p.h" +#include "gidextractorinterface.h" + +#include "item.h" +#include "typepluginloader_p.h" + +namespace Akonadi +{ + +QString GidExtractor::extractGid(const Item &item) +{ + const QObject *object = TypePluginLoader::objectForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds()); + if (object) { + const GidExtractorInterface *extractor = qobject_cast(object); + if (extractor) { + return extractor->extractGid(item); + } + } + return QString(); +} + +QString GidExtractor::getGid(const Item &item) +{ + const QString gid = item.gid(); + if (!gid.isNull()) { + return gid; + } + if (item.loadedPayloadParts().isEmpty()) { + return QString(); + } + return extractGid(item); +} + +} diff --git a/src/core/gidextractor_p.h b/src/core/gidextractor_p.h new file mode 100644 index 0000000..4b2fcc5 --- /dev/null +++ b/src/core/gidextractor_p.h @@ -0,0 +1,54 @@ +/* + Author: (2013) Christian Mollekopf + + 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 GIDEXTRACTOR_H +#define GIDEXTRACTOR_H + +#include + +namespace Akonadi +{ + +class Item; + +/** + * @internal + * Extracts the GID of an object contained in an akonadi item using a plugin that implements the GidExtractorInterface. + */ +class GidExtractor +{ +public: + /** + * Extracts the GID from @p item. using an extractor plugin. + */ + static QString extractGid(const Item &item); + + /** + * Extracts the gid from @p item. + * + * If the item has a GID set, that GID will be returned. + * If the item has no GID set, and the item has a payload, the GID is extracted using extractGid(). + * If the item has no GID set and no payload, a default constructed QString is returned. + */ + static QString getGid(const Item &item); +}; + +} + +#endif diff --git a/src/core/gidextractorinterface.h b/src/core/gidextractorinterface.h new file mode 100644 index 0000000..1d5bd79 --- /dev/null +++ b/src/core/gidextractorinterface.h @@ -0,0 +1,57 @@ +/* + Author: (2013) Christian Mollekopf + + 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 GIDEXTRACTORINTERFACE_H +#define GIDEXTRACTORINTERFACE_H + +#include + +namespace Akonadi +{ + +class Item; + +/** + * @short An interface to extract the GID of an object contained in an akonadi item. + * + * @author Christian Mollekopf + * @since 4.11 + */ +class GidExtractorInterface +{ +public: + /** + * Destructor. + */ + virtual ~GidExtractorInterface() + { + } + /** + * Extracts the globally unique id of @p item + * + * If you want to clear the gid from the database return QString(""). + */ + virtual QString extractGid(const Item &item) const = 0; +}; + +} + +Q_DECLARE_INTERFACE(Akonadi::GidExtractorInterface, "org.freedesktop.Akonadi.GidExtractorInterface/1.0") + +#endif diff --git a/src/core/indexpolicyattribute.cpp b/src/core/indexpolicyattribute.cpp new file mode 100644 index 0000000..0805a32 --- /dev/null +++ b/src/core/indexpolicyattribute.cpp @@ -0,0 +1,89 @@ +/* + Copyright (c) 2010 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 "indexpolicyattribute.h" + +#include "private/imapparser_p.h" + +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN IndexPolicyAttribute::Private +{ +public: + Private() + : enable(true) + { + } + bool enable; +}; + +IndexPolicyAttribute::IndexPolicyAttribute() + : d(new Private) +{ +} + +IndexPolicyAttribute::~IndexPolicyAttribute() +{ + delete d; +} + +bool IndexPolicyAttribute::indexingEnabled() const +{ + return d->enable; +} + +void IndexPolicyAttribute::setIndexingEnabled(bool enable) +{ + d->enable = enable; +} + +QByteArray IndexPolicyAttribute::type() const +{ + static const QByteArray sType("INDEXPOLICY"); + return sType; +} + +Attribute *IndexPolicyAttribute::clone() const +{ + IndexPolicyAttribute *attr = new IndexPolicyAttribute; + attr->setIndexingEnabled(indexingEnabled()); + return attr; +} + +QByteArray IndexPolicyAttribute::serialized() const +{ + QList l; + l.append("ENABLE"); + l.append(d->enable ? "true" : "false"); + return "(" + ImapParser::join(l, " ") + ')'; //krazy:exclude=doublequote_chars +} + +void IndexPolicyAttribute::deserialize(const QByteArray &data) +{ + QList l; + ImapParser::parseParenthesizedList(data, l); + for (int i = 0; i < l.size() - 1; i += 2) { + const QByteArray key = l.at(i); + if (key == "ENABLE") { + d->enable = l.at(i + 1) == "true"; + } + } +} diff --git a/src/core/indexpolicyattribute.h b/src/core/indexpolicyattribute.h new file mode 100644 index 0000000..2234135 --- /dev/null +++ b/src/core/indexpolicyattribute.h @@ -0,0 +1,77 @@ +/* + Copyright (c) 2010 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_INDEXPOLICYATTRIBUTE_H +#define AKONADI_INDEXPOLICYATTRIBUTE_H + +#include "akonadicore_export.h" +#include "attribute.h" + +namespace Akonadi +{ + +/** + * @short An attribute to specify how a collection should be indexed for searching. + * + * This attribute can be attached to any collection and should be honored by indexing + * agents. + * + * @since 4.6 + */ +class AKONADICORE_EXPORT IndexPolicyAttribute : public Akonadi::Attribute +{ +public: + /** + * Creates a new index policy attribute. + */ + IndexPolicyAttribute(); + + /** + * Destroys the index policy attribute. + */ + ~IndexPolicyAttribute(); + + /** + * Returns whether this collection is supposed to be indexed at all. + */ + bool indexingEnabled() const; + + /** + * Sets whether this collection should be indexed at all. + * @param enable @c true to enable indexing, @c false to exclude this collection from indexing + */ + void setIndexingEnabled(bool enable); + + //@cond PRIVATE + QByteArray type() const Q_DECL_OVERRIDE; + Attribute *clone() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + //@endcond + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/item.cpp b/src/core/item.cpp new file mode 100644 index 0000000..26bde62 --- /dev/null +++ b/src/core/item.cpp @@ -0,0 +1,750 @@ +/* + 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 "akonadicore_debug.h" +#include "itemserializer_p.h" +#include "private/protocol_p.h" + +#include +#include + +#include +#include + +#include +#include +#include + +using namespace Akonadi; + +Q_GLOBAL_STATIC(Akonadi::Collection, s_defaultParentCollection) + +uint Akonadi::qHash(const Akonadi::Item &item) +{ + return ::qHash(item.id()); +} + +namespace +{ + +struct ByTypeId { + typedef bool result_type; + bool operator()(const std::shared_ptr &lhs, + const std::shared_ptr &rhs) const + { + return strcmp(lhs->typeName(), rhs->typeName()) < 0 ; + } +}; + +} // anon namespace + +typedef QHash, std::pair, ByTypeId>> LegacyMap; +Q_GLOBAL_STATIC(LegacyMap, typeInfoToMetaTypeIdMap) +Q_GLOBAL_STATIC_WITH_ARGS(QReadWriteLock, legacyMapLock, (QReadWriteLock::Recursive)) + +void Item::addToLegacyMappingImpl(const QString &mimeType, int spid, int mtid, + std::unique_ptr &p) +{ + if (!p.get()) { + return; + } + const std::shared_ptr sp(p.release()); + const QWriteLocker locker(legacyMapLock()); + std::pair &item = (*typeInfoToMetaTypeIdMap())[mimeType][sp]; + item.first = spid; + item.second = mtid; +} + +namespace +{ +class MyReadLocker +{ +public: + explicit MyReadLocker(QReadWriteLock *rwl) + : rwl(rwl) + , locked(false) + { + if (rwl) { + rwl->lockForRead(); + } + locked = true; + } + + ~MyReadLocker() + { + if (rwl && locked) { + rwl->unlock(); + } + } + + template + std::shared_ptr makeUnlockingPointer(T *t) + { + if (t) { + // the bind() doesn't throw, so if shared_ptr + // construction line below, or anything else after it, + // throws, we're unlocked. Mark us as such: + locked = false; + const std::shared_ptr result(t, [&](const void *) { + rwl->unlock(); + }); + // from now on, the shared_ptr is responsible for unlocking + return result; + } else { + return std::shared_ptr(); + } + } +private: + Q_DISABLE_COPY(MyReadLocker) + QReadWriteLock *const rwl; + bool locked; +}; +} + +static std::shared_ptr> lookupLegacyMapping(const QString &mimeType, + Internal::PayloadBase *p) +{ + MyReadLocker locker(legacyMapLock()); + const LegacyMap::const_iterator hit = typeInfoToMetaTypeIdMap()->constFind(mimeType); + if (hit == typeInfoToMetaTypeIdMap()->constEnd()) { + return std::shared_ptr>(); + } + const std::shared_ptr sp(p, [=](Internal::PayloadBase *) { + /*noop*/ + }); + const LegacyMap::mapped_type::const_iterator it = hit->find(sp); + if (it == hit->end()) { + return std::shared_ptr>(); + } + + return locker.makeUnlockingPointer(&it->second); +} + +// Change to something != RFC822 as soon as the server supports it +const char Item::FullPayload[] = "RFC822"; + +Item::Item() + : d_ptr(new ItemPrivate) +{ +} + +Item::Item(Id id) + : d_ptr(new ItemPrivate(id)) +{ +} + +Item::Item(const QString &mimeType) + : d_ptr(new ItemPrivate) +{ + d_ptr->mMimeType = mimeType; +} + +Item::Item(const Item &other) + : d_ptr(other.d_ptr) +{ +} + +Item::~Item() +{ +} + +void Item::setId(Item::Id identifier) +{ + d_ptr->mId = identifier; +} + +Item::Id Item::id() const +{ + return d_ptr->mId; +} + +void Item::setRemoteId(const QString &id) +{ + d_ptr->mRemoteId = id; +} + +QString Item::remoteId() const +{ + return d_ptr->mRemoteId; +} + +void Item::setRemoteRevision(const QString &revision) +{ + d_ptr->mRemoteRevision = revision; +} + +QString Item::remoteRevision() const +{ + return d_ptr->mRemoteRevision; +} + +bool Item::isValid() const +{ + return (d_ptr->mId >= 0); +} + +bool Item::operator==(const Item &other) const +{ + // Invalid collections are the same, no matter what their internal ID is + return (!isValid() && !other.isValid()) || (d_ptr->mId == other.d_ptr->mId); +} + +bool Akonadi::Item::operator!=(const Item &other) const +{ + return (isValid() || other.isValid()) && (d_ptr->mId != other.d_ptr->mId); +} + +Item &Item ::operator=(const Item &other) +{ + if (this != &other) { + d_ptr = other.d_ptr; + } + + return *this; +} + +bool Akonadi::Item::operator<(const Item &other) const +{ + return d_ptr->mId < other.d_ptr->mId; +} + +void Item::addAttribute(Attribute *attr) +{ + Q_ASSERT(attr); + Attribute *existing = d_ptr->mAttributes.value(attr->type()); + if (existing) { + if (attr == existing) { + return; + } + d_ptr->mAttributes.remove(attr->type()); + delete existing; + } + d_ptr->mAttributes.insert(attr->type(), attr); + ItemChangeLog::instance()->deletedAttributes(d_ptr).remove(attr->type()); +} + +void Item::removeAttribute(const QByteArray &type) +{ + ItemChangeLog::instance()->deletedAttributes(d_ptr).insert(type); + delete d_ptr->mAttributes.take(type); +} + +bool Item::hasAttribute(const QByteArray &type) const +{ + return d_ptr->mAttributes.contains(type); +} + +Attribute::List Item::attributes() const +{ + return d_ptr->mAttributes.values(); +} + +void Akonadi::Item::clearAttributes() +{ + ItemChangeLog *changelog = ItemChangeLog::instance(); + QSet &deletedAttributes = changelog->deletedAttributes(d_ptr); + Q_FOREACH (Attribute *attr, d_ptr->mAttributes) { + deletedAttributes.insert(attr->type()); + delete attr; + } + d_ptr->mAttributes.clear(); +} + +Attribute *Item::attribute(const QByteArray &type) const +{ + return d_ptr->mAttributes.value(type); +} + +Collection &Item::parentCollection() +{ + if (!d_ptr->mParent) { + d_ptr->mParent = new Collection(); + } + return *(d_ptr->mParent); +} + +Collection Item::parentCollection() const +{ + if (!d_ptr->mParent) { + return *(s_defaultParentCollection); + } else { + return *(d_ptr->mParent); + } +} + +void Item::setParentCollection(const Collection &parent) +{ + delete d_ptr->mParent; + d_ptr->mParent = new Collection(parent); +} + +Item::Flags Item::flags() const +{ + return d_ptr->mFlags; +} + +void Item::setFlag(const QByteArray &name) +{ + d_ptr->mFlags.insert(name); + if (!d_ptr->mFlagsOverwritten) { + Item::Flags &deletedFlags = ItemChangeLog::instance()->deletedFlags(d_ptr); + auto iter = deletedFlags.find(name); + if (iter != deletedFlags.end()) { + deletedFlags.erase(iter); + } else { + ItemChangeLog::instance()->addedFlags(d_ptr).insert(name); + } + } +} + +void Item::clearFlag(const QByteArray &name) +{ + d_ptr->mFlags.remove(name); + if (!d_ptr->mFlagsOverwritten) { + Item::Flags &addedFlags = ItemChangeLog::instance()->addedFlags(d_ptr); + auto iter = addedFlags.find(name); + if (iter != addedFlags.end()) { + addedFlags.erase(iter); + } else { + ItemChangeLog::instance()->deletedFlags(d_ptr).insert(name); + } + } +} + +void Item::setFlags(const Flags &flags) +{ + d_ptr->mFlags = flags; + d_ptr->mFlagsOverwritten = true; +} + +void Item::clearFlags() +{ + d_ptr->mFlags.clear(); + d_ptr->mFlagsOverwritten = true; +} + +QDateTime Item::modificationTime() const +{ + return d_ptr->mModificationTime; +} + +void Item::setModificationTime(const QDateTime &datetime) +{ + d_ptr->mModificationTime = datetime; +} + +bool Item::hasFlag(const QByteArray &name) const +{ + return d_ptr->mFlags.contains(name); +} + +void Item::setTags(const Tag::List &list) +{ + d_ptr->mTags = list; + d_ptr->mTagsOverwritten = true; +} + +void Item::setTag(const Tag &tag) +{ + d_ptr->mTags << tag; + if (!d_ptr->mTagsOverwritten) { + Tag::List &deletedTags = ItemChangeLog::instance()->deletedTags(d_ptr); + if (deletedTags.contains(tag)) { + deletedTags.removeOne(tag); + } else { + ItemChangeLog::instance()->addedTags(d_ptr).push_back(tag); + } + } +} + +void Item::clearTags() +{ + d_ptr->mTags.clear(); + d_ptr->mTagsOverwritten = true; +} + +void Item::clearTag(const Tag &tag) +{ + d_ptr->mTags.removeOne(tag); + if (!d_ptr->mTagsOverwritten) { + Tag::List &addedTags = ItemChangeLog::instance()->addedTags(d_ptr); + if (addedTags.contains(tag)) { + addedTags.removeOne(tag); + } else { + ItemChangeLog::instance()->deletedTags(d_ptr).push_back(tag); + } + } +} + +bool Item::hasTag(const Tag &tag) const +{ + return d_ptr->mTags.contains(tag); +} + +Tag::List Item::tags() const +{ + return d_ptr->mTags; +} + +Relation::List Item::relations() const +{ + return d_ptr->mRelations; +} + +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); +} + +void Item::clearPayload() +{ + d_ptr->mClearPayload = true; +} + +int Item::revision() const +{ + return d_ptr->mRevision; +} + +void Item::setRevision(int rev) +{ + d_ptr->mRevision = rev; +} + +Collection::Id Item::storageCollectionId() const +{ + return d_ptr->mCollectionId; +} + +void Item::setStorageCollectionId(Collection::Id collectionId) +{ + d_ptr->mCollectionId = collectionId; +} + +QString Item::mimeType() const +{ + return d_ptr->mMimeType; +} + +void Item::setSize(qint64 size) +{ + d_ptr->mSize = size; + d_ptr->mSizeChanged = true; +} + +qint64 Item::size() const +{ + return d_ptr->mSize; +} + +void Item::setMimeType(const QString &mimeType) +{ + d_ptr->mMimeType = mimeType; +} + +void Item::setGid(const QString &id) +{ + d_ptr->mGid = id; +} + +QString Item::gid() const +{ + return d_ptr->mGid; +} + +void Item::setVirtualReferences(const Collection::List &collections) +{ + d_ptr->mVirtualReferences = collections; +} + +Collection::List Item::virtualReferences() const +{ + return d_ptr->mVirtualReferences; +} + +bool Item::hasPayload() const +{ + return d_ptr->hasMetaTypeId(-1); +} + +QUrl Item::url(UrlType type) const +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("item"), QString::number(id())); + if (type == UrlWithMimeType) { + query.addQueryItem(QStringLiteral("type"), mimeType()); + } + + QUrl url; + url.setScheme(QStringLiteral("akonadi")); + url.setQuery(query); + return url; +} + +Item Item::fromUrl(const QUrl &url) +{ + if (url.scheme() != QLatin1String("akonadi")) { + return Item(); + } + + const QString itemStr = QUrlQuery(url).queryItemValue(QStringLiteral("item")); + bool ok = false; + Item::Id itemId = itemStr.toLongLong(&ok); + if (!ok) { + return Item(); + } + + return Item(itemId); +} + +namespace +{ +class Dummy +{ +}; +} + +Q_GLOBAL_STATIC(Internal::Payload, dummyPayload) + +Internal::PayloadBase *Item::payloadBase() const +{ + d_ptr->tryEnsureLegacyPayload(); + if (d_ptr->mLegacyPayload) { + return d_ptr->mLegacyPayload.get(); + } else { + return dummyPayload(); + } +} + +void ItemPrivate::tryEnsureLegacyPayload() const +{ + if (!mLegacyPayload) { + for (PayloadContainer::const_iterator it = mPayloads.begin(), end = mPayloads.end() ; it != end ; ++it) { + if (lookupLegacyMapping(mMimeType, it->payload.get())) { + mLegacyPayload = it->payload; // clones + } + } + } +} + +Internal::PayloadBase *Item::payloadBaseV2(int spid, int mtid) const +{ + return d_ptr->payloadBaseImpl(spid, mtid); +} + +namespace +{ +class ConversionGuard +{ + const bool old; + bool &b; +public: + explicit ConversionGuard(bool &b) + : old(b) + , b(b) + { + b = true; + } + ~ConversionGuard() + { + b = old; + } +private: + Q_DISABLE_COPY(ConversionGuard) +}; +} + +bool Item::ensureMetaTypeId(int mtid) const +{ + // 0. Nothing there - nothing to convert from, either + if (d_ptr->mPayloads.empty()) { + return false; + } + + // 1. Look whether we already have one: + if (d_ptr->hasMetaTypeId(mtid)) { + return true; + } + + // recursion detection (shouldn't trigger, but does if the + // serialiser plugins are acting funky): + if (d_ptr->mConversionInProgress) { + return false; + } + + // 2. Try to create one by conversion from a different representation: + try { + const ConversionGuard guard(d_ptr->mConversionInProgress); + Item converted = ItemSerializer::convert(*this, mtid); + return d_ptr->movePayloadFrom(converted.d_ptr, mtid); + } catch (const std::exception &e) { + qCDebug(AKONADICORE_LOG) << "conversion threw:" << e.what(); + return false; + } catch (...) { + qCDebug(AKONADICORE_LOG) << "conversion threw something not derived from std::exception: fix the program!"; + return false; + } +} + +static QString format_type(int spid, int mtid) +{ + return QStringLiteral("sp(%1)<%2>") + .arg(spid).arg(QLatin1String(QMetaType::typeName(mtid))); +} + +static QString format_types(const PayloadContainer &c) +{ + QStringList result; + result.reserve(c.size()); + for (PayloadContainer::const_iterator it = c.begin(), end = c.end() ; it != end ; ++it) { + result.push_back(format_type(it->sharedPointerId, it->metaTypeId)); + } + return result.join(QStringLiteral(", ")); +} + +#if 0 +QString Item::payloadExceptionText(int spid, int mtid) const +{ + if (d_ptr->mPayloads.empty()) { + return QStringLiteral("No payload set"); + } else { + return QStringLiteral("Wrong payload type (requested: %1; present: %2") + .arg(format_type(spid, mtid), format_types(d_ptr->mPayloads)); + } +} +#else +void Item::throwPayloadException(int spid, int mtid) const +{ + if (d_ptr->mPayloads.empty()) { + throw PayloadException("No payload set"); + } else { + throw PayloadException(QStringLiteral("Wrong payload type (requested: %1; present: %2") + .arg(format_type(spid, mtid), format_types(d_ptr->mPayloads))); + } +} +#endif + +void Item::setPayloadBase(Internal::PayloadBase *p) +{ + d_ptr->setLegacyPayloadBaseImpl(std::unique_ptr(p)); +} + +void ItemPrivate::setLegacyPayloadBaseImpl(std::unique_ptr p) +{ + if (const std::shared_ptr> pair = lookupLegacyMapping(mMimeType, p.get())) { + std::unique_ptr clone; + if (p.get()) { + clone.reset(p->clone()); + } + setPayloadBaseImpl(pair->first, pair->second, p, false); + mLegacyPayload.reset(clone.release()); + } else { + mPayloads.clear(); + mLegacyPayload.reset(p.release()); + } +} + +void Item::setPayloadBaseV2(int spid, int mtid, std::unique_ptr &p) +{ + d_ptr->setPayloadBaseImpl(spid, mtid, p, false); +} + +void Item::addPayloadBaseVariant(int spid, int mtid, std::unique_ptr &p) const +{ + d_ptr->setPayloadBaseImpl(spid, mtid, p, true); +} + +QSet Item::cachedPayloadParts() const +{ + return d_ptr->mCachedPayloadParts; +} + +void Item::setCachedPayloadParts(const QSet &cachedParts) +{ + d_ptr->mCachedPayloadParts = cachedParts; +} + +QSet Item::availablePayloadParts() const +{ + return ItemSerializer::availableParts(*this); +} + +QVector Item::availablePayloadMetaTypeIds() const +{ + QVector result; + result.reserve(d_ptr->mPayloads.size()); + // Stable Insertion Sort - N is typically _very_ low (1 or 2). + for (PayloadContainer::const_iterator it = d_ptr->mPayloads.begin(), end = d_ptr->mPayloads.end() ; it != end ; ++it) { + result.insert(std::upper_bound(result.begin(), result.end(), it->metaTypeId), it->metaTypeId); + } + return result; +} + +void Item::apply(const Item &other) +{ + if (mimeType() != other.mimeType() || id() != other.id()) { + qCDebug(AKONADICORE_LOG) << "mimeType() = " << mimeType() << "; other.mimeType() = " << other.mimeType(); + qCDebug(AKONADICORE_LOG) << "id() = " << id() << "; other.id() = " << other.id(); + Q_ASSERT_X(false, "Item::apply", "mimetype or id missmatch"); + } + + setRemoteId(other.remoteId()); + setRevision(other.revision()); + setRemoteRevision(other.remoteRevision()); + setFlags(other.flags()); + setTags(other.tags()); + setModificationTime(other.modificationTime()); + setSize(other.size()); + setParentCollection(other.parentCollection()); + setStorageCollectionId(other.storageCollectionId()); + + QList attrs; + attrs.reserve(other.attributes().count()); + Q_FOREACH (Attribute *attribute, other.attributes()) { + addAttribute(attribute->clone()); + attrs.append(attribute->type()); + } + + QMutableHashIterator it(d_ptr->mAttributes); + while (it.hasNext()) { + it.next(); + if (!attrs.contains(it.key())) { + delete it.value(); + it.remove(); + } + } + + ItemSerializer::apply(*this, other); + d_ptr->resetChangeLog(); +} diff --git a/src/core/item.h b/src/core/item.h new file mode 100644 index 0000000..89af530 --- /dev/null +++ b/src/core/item.h @@ -0,0 +1,1057 @@ +/* + 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 "akonadicore_export.h" +#include "attribute.h" +#include "exception.h" +#include "tag.h" +#include "collection.h" +#include "relation.h" +#include "itempayloadinternals_p.h" +#include "job.h" + +#include +#include +#include + +#include +#include +#include + +class QUrl; + +template +class QVector; + +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 is implicitly shared. + * + *

Payload

+ * + * This class contains, beside some type-agnostic information (flags, revision), + * zero or more payload objects representing its actual data. Which objects these actually + * are depends on the mimetype of the item and the corresponding serializer plugin(s). + * + * 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, QSharedPointer or std::shared_ptr). + * + * Using a shared pointer is also required in case the payload is a polymorphic + * type. 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. + * + * Since KDE 4.6, Item supports multiple payload types per mime type, + * and will automatically convert between them using the serialiser + * plugins (which is slow). It also supports mixing shared pointer + * types, e.g. inserting a boost::shared_ptr and extracting a + * QSharedPointer. Since the two shared pointer types cannot + * share ownership of the same object, the payload class @c T needs to + * provide a @c clone() method with the usual signature, ie. + * + * @code + * virtual T * T::clone() const + * @endcode + * + * If the class that does not have a @c clone() method, asking for an + * incompatible shared pointer will throw a PayloadException. + * + * Since using different shared pointer types and different payload + * types for the same mimetype incurs slow conversions (between + * payload types) and cloning (between shared pointer types), as well + * as manifold memory usage (results of conversions are cached inside + * the Item, and only destroyed when a new payload is set by the user + * of the class), you want to restrict yourself to just one type and + * one shared pointer type. This mechanism was mainly introduced for + * backwards compatibility (e.g., putting in a + * boost::shared_ptr and extracting a + * QSharedPointer), so it is not optimized for + * performance. + * + * 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 , Marc Mutz + */ +class AKONADICORE_EXPORT Item +{ +public: + /** + * Describes the unique id type. + */ + typedef qint64 Id; + + /** + * Describes a list of items. + */ + typedef QVector 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 QUrl &url); + + /** + * Sets the unique @p identifier of the item. + */ + void setId(Id identifier); + + /** + * Returns the unique identifier of the item. + */ + Id id() const; + + /** + * Sets the remote @p id of the item. + */ + void setRemoteId(const QString &id); + + /** + * Returns the remote id of the item. + */ + QString remoteId() const; + + /** + * Sets the remote @p revision of the item. + * @param revision the item's remote revision + * The remote revision can be used by resources to store some + * revision information of the backend to detect changes there. + * + * @note This method is supposed to be used by resources only. + * @since 4.5 + */ + void setRemoteRevision(const QString &revision); + + /** + * Returns the remote revision of the item. + * + * @note This method is supposed to be used by resources only. + * @since 4.5 + */ + QString remoteRevision() const; + + /** + * Returns whether the item is valid. + */ + bool isValid() const; + + /** + * Returns whether this item's id equals the id of the @p other item. + */ + bool operator==(const Item &other) const; + + /** + * Returns whether the item's id does not equal the id of the @p other item. + */ + bool operator!=(const Item &other) const; + + /** + * Assigns the @p other to this item and returns a reference to this item. + * @param other the item to assign + */ + Item &operator=(const Item &other); + + /** + * @internal For use with containers only. + * + * @since 4.8 + */ + bool operator<(const Item &other) const; + + /** + * Returns the parent collection of this object. + * @note This will of course only return a useful value if it was explictly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Collection parentCollection() const; + + /** + * Returns a reference to the parent collection of this object. + * @note This will of course only return a useful value if it was explictly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Collection &parentCollection(); + + /** + * Set the parent collection of this object. + * @note Calling this method has no immediate effect for the object itself, + * such as being moved to another collection. + * It is mainly relevant to provide a context for RID-based operations + * inside resources. + * @param parent The parent collection. + * @since 4.4 + */ + void setParentCollection(const Collection &parent); + + /** + * Adds an attribute to the item. + * + * If an attribute of the same type name already exists, it is deleted and + * replaced with the new one. + * + * @param attribute The new attribute. + * + * @note The collection takes the ownership of the attribute. + */ + void addAttribute(Attribute *attribute); + + /** + * Removes and deletes the attribute of the given type @p name. + */ + void removeAttribute(const QByteArray &name); + + /** + * Returns @c true if the item has an attribute of the given type @p name, + * false otherwise. + */ + bool hasAttribute(const QByteArray &name) const; + + /** + * Returns a list of all attributes of the item. + */ + Attribute::List attributes() const; + + /** + * Removes and deletes all attributes of the item. + */ + void clearAttributes(); + + /** + * Returns the attribute of the given type @p name if available, 0 otherwise. + */ + Attribute *attribute(const QByteArray &name) const; + + /** + * Describes the options that can be passed to access attributes. + */ + enum CreateOption { + AddIfMissing ///< Creates the attribute if it is missing + }; + + /** + * Returns the attribute of the requested type. + * If the item has no attribute of that type yet, a new one + * is created and added to the entity. + * + * @param option The create options. + */ + template + inline T *attribute(CreateOption option); + + /** + * Returns the attribute of the requested type or 0 if it is not available. + */ + template + inline T *attribute() const; + + /** + * Removes and deletes the attribute of the requested type. + */ + template + inline void removeAttribute(); + + /** + * Returns whether the item has an attribute of the requested type. + */ + template + inline bool hasAttribute() const; + + /** + * 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. + * @param datetime the modification time to set + * @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(); + + void setTags(const Tag::List &list); + + void setTag(const Tag &tag); + + Tag::List tags() const; + + bool hasTag(const Tag &tag) const; + + void clearTag(const Tag &tag); + + void clearTags(); + + /** + * Returns all relations of this item. + * @since 4.15 + * @see RelationCreateJob, RelationDeleteJob to modify relations + */ + Relation::List relations() const; + + /** + * 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; + + /** + * Marks that the payload shall be cleared from the cache when this + * item is passed to an ItemModifyJob the next time. + * This will trigger a refetch of the payload from the backend when the + * item is accessed afterwards. Only resources should have a need for + * this functionality. + * + * @since 4.5 + */ + void clearPayload(); + + /** + * Sets the @p revision number of the item. + * @param revision the revision number to set + * @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 known, -1 otherwise. + * @since 4.3 + */ + Collection::Id storageCollectionId() const; + + /** + * Set the size of the item in bytes. + * @param size 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 @p gid of the entity. + * + * @since 4.12 + */ + void setGid(const QString &gid); + + /** + * Returns the gid of the entity. + * + * @since 4.12 + */ + QString gid() const; + + /** + * Sets the virtual @p collections that this item is linked into. + * + * @note Note that changing this value makes no effect on what collections + * this item is linked to. To link or unlink an item to/from a virtual + * collection, use LinkJob and UnlinkJob. + * + * @since 4.14 + */ + void setVirtualReferences(const Collection::List &collections); + + /** + * Lists virtual collections that this item is linked to. + * + * @note This value is populated only when this item was retrieved by + * ItemFetchJob with fetchVirtualReferences set to true in ItemFetchScope, + * otherwise this list is always empty. + * + * @since 4.14 + */ + Collection::List virtualReferences() const; + + /** + * Returns a list of metatype-ids, describing the different + * variants of payload that are currently contained in this item. + * + * The result is always sorted (increasing ids). + */ + QVector availablePayloadMetaTypeIds() 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, QSharedPointer + * and std::shared_ptr 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); + +// We know that auto_ptr is deprecated, but we still want to handle the case +// without compilers yelling at us all the time just because item.h gets included +// virtually everywhere +#ifdef __GNUC__ +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif +#endif + template void setPayload(std::auto_ptr p); +#ifdef __GNUC__ +#ifdef __clang__ +#pragma clang diagnostic pop +#else +#pragma GCC diagnostic pop +#endif +#endif + template void setPayload(std::unique_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, QSharedPointer and + * std::shared_ptr), 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, + * QSharedPointer and std::shared_ptr), 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. + */ + QUrl url(UrlType type = UrlShort) const; + + /** + * Returns the parts available for this item. + * + * The returned set refers to parts available on the akonadi server or remotely, + * but does not include the loadedPayloadParts() of this item. + * + * @since 4.4 + */ + QSet availablePayloadParts() const; + + /** + * Returns the parts available for this item in the cache. The list might be a subset + * of the actual parts in cache, as it contains only the requested parts. See @see ItemFetchJob and + * @see ItemFetchScope + * + * The returned set refers to parts available on the akonadi server. + * + * @since 4.11 + */ + QSet cachedPayloadParts() const; + + /** + * Applies the parts of Item @p other to this item. + * Any parts or attributes available in other, will be applied to 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. + * @param other the item to get values from + * @since 4.4 + */ + void apply(const Item &other); + + /** + * Registers \a T as a legacy type for mime type \a mimeType. + * + * This is required information for Item to return the correct + * type from payload() when clients have not been recompiled to + * use the new code. + * @param mimeType the mimeType to register + * @since 4.6 + */ + template static void addToLegacyMapping(const QString &mimeType); + void setCachedPayloadParts(const QSet &cachedParts); + +private: + //@cond PRIVATE + friend class ItemCreateJob; + friend class ItemModifyJob; + friend class ItemModifyJobPrivate; + friend class ItemSync; + friend class ProtocolHelper; + Internal::PayloadBase *payloadBase() const; + void setPayloadBase(Internal::PayloadBase *p); + Internal::PayloadBase *payloadBaseV2(int sharedPointerId, int metaTypeId) const; + //std::auto_ptr takePayloadBase( int sharedPointerId, int metaTypeId ); + void setPayloadBaseV2(int sharedPointerId, int metaTypeId, + std::unique_ptr &p); + void addPayloadBaseVariant(int sharedPointerId, int metaTypeId, + std::unique_ptr &p) const; + static void addToLegacyMappingImpl(const QString &mimeType, int sharedPointerId, int metaTypeId, + std::unique_ptr &p); + + /** + * Try to ensure that we have a variant of the payload for metatype id @a mtid. + * @return @c true if a type exists or could be created through conversion, @c false otherwise. + */ + bool ensureMetaTypeId(int mtid) const; + + template + typename std::enable_if::isPolymorphic, void>::type + setPayloadImpl(const T &p, const int * /*disambiguate*/ = 0); + template + typename std::enable_if < !Internal::PayloadTrait::isPolymorphic, void >::type + setPayloadImpl(const T &p); + + template + typename std::enable_if::isPolymorphic, T>::type + payloadImpl(const int * /*disambiguate*/ = 0) const; + template + typename std::enable_if < !Internal::PayloadTrait::isPolymorphic, T >::type + payloadImpl() const; + + template + typename std::enable_if::isPolymorphic, bool>::type + hasPayloadImpl(const int * /*disambiguate*/ = 0) const; + template + typename std::enable_if < !Internal::PayloadTrait::isPolymorphic, bool >::type + hasPayloadImpl() const; + + template + typename std::enable_if::value, bool>::type + tryToClone(T *ret, const int * /*disambiguate*/ = 0) const; + template + typename std::enable_if < !Internal::is_shared_pointer::value, bool >::type + tryToClone(T *ret) const; + + template + typename std::enable_if < !std::is_same::value, bool >::type + tryToCloneImpl(T *ret, const int * /*disambiguate*/ = 0) const; + template + typename std::enable_if::value, bool>::type + tryToCloneImpl(T *ret) const; + + /** + * 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 collection where this item is stored in. + * @since 4.3 + */ + void setStorageCollectionId(Collection::Id collectionId); + +#if 0 + /** + * Helper function for non-template throwing of PayloadException. + */ + QString payloadExceptionText(int spid, int mtid) const; + + /** + * Non-template throwing of PayloadException. + * Needs to be inline, otherwise catch (Akonadi::PayloadException) + * won't work (only catch (Akonadi::Exception)) + */ + inline void throwPayloadException(int spid, int mtid) const + { + throw PayloadException(payloadExceptionText(spid, mtid)); + } +#else + void throwPayloadException(int spid, int mtid) const; +#endif + + QSharedDataPointer d_ptr; + friend class ItemPrivate; + //@endcond +}; + +AKONADICORE_EXPORT uint qHash(const Akonadi::Item &item); + +template +inline T *Item::attribute(Item::CreateOption option) +{ + Q_UNUSED(option); + + const T dummy; + if (hasAttribute(dummy.type())) { + T *attr = dynamic_cast(attribute(dummy.type())); + if (attr) { + return attr; + } + //Reuse 5250 + qWarning() << "Found attribute of unknown type" << dummy.type() + << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } + + T *attr = new T(); + addAttribute(attr); + return attr; +} + +template +inline T *Item::attribute() const +{ + const T dummy; + if (hasAttribute(dummy.type())) { + T *attr = dynamic_cast(attribute(dummy.type())); + if (attr) { + return attr; + } + //reuse 5250 + qWarning() << "Found attribute of unknown type" << dummy.type() + << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } + + return 0; +} + +template +inline void Item::removeAttribute() +{ + const T dummy; + removeAttribute(dummy.type()); +} + +template +inline bool Item::hasAttribute() const +{ + const T dummy; + return hasAttribute(dummy.type()); +} + +template +T Item::payload() const +{ + static_assert(!std::is_pointer::value, "Payload must not be a pointer"); + + if (!hasPayload()) { + throwPayloadException(-1, -1); + } + + return payloadImpl(); +} + +template +typename std::enable_if::isPolymorphic, T>::type +Item::payloadImpl(const int *) const +{ + typedef Internal::PayloadTrait PayloadType; + static_assert(PayloadType::isPolymorphic, + "Non-polymorphic payload type in polymorphic implementation is not allowed"); + + typedef typename Internal::get_hierarchy_root::type Root_T; + typedef Internal::PayloadTrait RootType; + static_assert(!RootType::isPolymorphic, + "Root type of payload type must not be polymorphic"); // prevent endless recursion + + return PayloadType::castFrom(payloadImpl()); +} + +template +typename std::enable_if < !Internal::PayloadTrait::isPolymorphic, T >::type +Item::payloadImpl() const +{ + typedef Internal::PayloadTrait PayloadType; + static_assert(!PayloadType::isPolymorphic, + "Polymorphic payload type in non-polymorphic implementation is not allowed"); + + const int metaTypeId = PayloadType::elementMetaTypeId(); + + // make sure that we have a payload format represented by 'metaTypeId': + if (!ensureMetaTypeId(metaTypeId)) { + throwPayloadException(PayloadType::sharedPointerId, metaTypeId); + } + + // Check whether we have the exact payload + // (metatype id and shared pointer type match) + if (const Internal::Payload *const p = Internal::payload_cast(payloadBaseV2(PayloadType::sharedPointerId, metaTypeId))) { + return p->payload; + } + + T ret; + if (!tryToClone(&ret)) { + throwPayloadException(PayloadType::sharedPointerId, metaTypeId); + } + return ret; +} + +template +typename std::enable_if < !std::is_same::value, bool >::type +Item::tryToCloneImpl(T *ret, const int *) const +{ + typedef Internal::PayloadTrait PayloadType; + typedef Internal::PayloadTrait NewPayloadType; + + const int metaTypeId = PayloadType::elementMetaTypeId(); + Internal::PayloadBase *payloadBase = payloadBaseV2(NewPayloadType::sharedPointerId, metaTypeId); + if (const Internal::Payload *const p = Internal::payload_cast(payloadBase)) { + // If found, attempt to make a clone (required the payload to provide virtual T * T::clone() const) + const T nt = PayloadType::clone(p->payload); + if (!PayloadType::isNull(nt)) { + // if clone succeeded, add the clone to the Item: + std::unique_ptr npb(new Internal::Payload(nt)); + addPayloadBaseVariant(PayloadType::sharedPointerId, metaTypeId, npb); + // and return it + if (ret) { + *ret = nt; + } + return true; + } + } + + return tryToCloneImpl::next_shared_ptr>(ret); +} + +template +typename std::enable_if::value, bool>::type +Item::tryToCloneImpl(T *) const +{ + return false; +} + +template +typename std::enable_if::value, bool>::type +Item::tryToClone(T *ret, const int *) const +{ + typedef Internal::PayloadTrait PayloadType; + static_assert(!PayloadType::isPolymorphic, + "Polymorphic payload type in non-polymorphic implementation is not allowed"); + + return tryToCloneImpl::next_shared_ptr>(ret); +} + +template +typename std::enable_if < !Internal::is_shared_pointer::value, bool >::type +Item::tryToClone(T *) const +{ + typedef Internal::PayloadTrait PayloadType; + static_assert(!PayloadType::isPolymorphic, + "Polymorphic payload type in non-polymorphic implementation is not allowed"); + + return false; +} + +template +bool Item::hasPayload() const +{ + static_assert(!std::is_pointer::value, "Payload type cannot be a pointer"); + return hasPayload() && hasPayloadImpl(); +} + +template +typename std::enable_if::isPolymorphic, bool>::type +Item::hasPayloadImpl(const int *) const +{ + typedef Internal::PayloadTrait PayloadType; + static_assert(PayloadType::isPolymorphic, + "Non-polymorphic payload type in polymorphic implementation is no allowed"); + + typedef typename Internal::get_hierarchy_root::type Root_T; + typedef Internal::PayloadTrait RootType; + static_assert(!RootType::isPolymorphic, + "Root type of payload type must not be polymorphic"); // prevent endless recursion + + try { + return hasPayloadImpl() + && PayloadType::canCastFrom(payload()); + } catch (const Akonadi::PayloadException &e) { + qDebug() << e.what(); + Q_UNUSED(e) + return false; + } +} + +template +typename std::enable_if < !Internal::PayloadTrait::isPolymorphic, bool >::type +Item::hasPayloadImpl() const +{ + typedef Internal::PayloadTrait PayloadType; + static_assert(!PayloadType::isPolymorphic, + "Polymorphic payload type in non-polymorphic implementation is not allowed"); + + const int metaTypeId = PayloadType::elementMetaTypeId(); + + // make sure that we have a payload format represented by 'metaTypeId': + if (!ensureMetaTypeId(metaTypeId)) { + return false; + } + + // Check whether we have the exact payload + // (metatype id and shared pointer type match) + if (const Internal::Payload *const p = Internal::payload_cast(payloadBaseV2(PayloadType::sharedPointerId, metaTypeId))) { + return true; + } + + return tryToClone(0); +} + +template +void Item::setPayload(const T &p) +{ + static_assert(!std::is_pointer::value, "Payload type must not be a pointer"); + setPayloadImpl(p); +} + +template +typename std::enable_if::isPolymorphic>::type +Item::setPayloadImpl(const T &p, const int *) +{ + typedef Internal::PayloadTrait PayloadType; + static_assert(PayloadType::isPolymorphic, + "Non-polymorphic payload type in polymorphic implementation is not allowed"); + + typedef typename Internal::get_hierarchy_root::type Root_T; + typedef Internal::PayloadTrait RootType; + static_assert(!RootType::isPolymorphic, + "Root type of payload type must not be polymorphic"); // prevent endless recursion + + setPayloadImpl(p); +} + +template +typename std::enable_if < !Internal::PayloadTrait::isPolymorphic >::type +Item::setPayloadImpl(const T &p) +{ + typedef Internal::PayloadTrait PayloadType; + std::unique_ptr pb(new Internal::Payload(p)); + setPayloadBaseV2(PayloadType::sharedPointerId, + PayloadType::elementMetaTypeId(), + pb); +} + +template +void Item::setPayload(T *p) +{ + p->You_MUST_NOT_use_a_pointer_as_payload; +} + +#ifdef __GNUC__ +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif +#endif +template +void Item::setPayload(std::auto_ptr p) +{ + p.Nice_try_but_a_std_auto_ptr_is_not_allowed_as_payload_either; +} +#ifdef __GNUC__ +#ifdef __clang__ +#pragma clang diagnostic pop +#else +#pragma GCC diagnostic pop +#endif +#endif + +template +void Item::setPayload(std::unique_ptr p) +{ + p.Nope_even_std_unique_ptr_is_not_allowed; +} + +template +void Item::addToLegacyMapping(const QString &mimeType) +{ + typedef Internal::PayloadTrait PayloadType; + static_assert(!PayloadType::isPolymorphic, "Payload type must not be polymorphic"); + std::unique_ptr p(new Internal::Payload); + addToLegacyMappingImpl(mimeType, PayloadType::sharedPointerId, PayloadType::elementMetaTypeId(), p); +} + +} // namespace Akonadi + +Q_DECLARE_METATYPE(Akonadi::Item) +Q_DECLARE_METATYPE(Akonadi::Item::List) + +#endif diff --git a/src/core/item_p.h b/src/core/item_p.h new file mode 100644 index 0000000..012f1b1 --- /dev/null +++ b/src/core/item_p.h @@ -0,0 +1,466 @@ +/* + 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_ITEM_P_H +#define AKONADI_ITEM_P_H + +#include +#include +#include + +#include "itempayloadinternals_p.h" +#include "itemchangelog_p.h" +#include "tag.h" + +#include +#include +#include +#include + +namespace Akonadi +{ + +namespace _detail +{ + +template +class clone_ptr +{ + T *t; +public: + clone_ptr() + : t(0) + { + } + explicit clone_ptr(T *t) + : t(t) + { + } + clone_ptr(const clone_ptr &other) + : t(other.t ? other.t->clone() : 0) + { + } + ~clone_ptr() + { + delete t; + } + clone_ptr &operator=(const clone_ptr &other) + { + if (this != &other) { + clone_ptr copy(other); + swap(copy); + } + return *this; + } + void swap(clone_ptr &other) + { + using std::swap; + swap(t, other.t); + } + T *operator->() const + { + return get(); + } + T &operator*() const + { + assert(get() != 0); + return *get(); + } + T *get() const + { + return t; + } + T *release() + { + T *const r = t; + t = 0; + return r; + } + void reset(T *other = 0) + { + delete t; + t = other; + } + +private: + struct _save_bool + { + void f() + { + } + }; + typedef void (_save_bool::*save_bool)(); +public: + operator save_bool() const + { + return get() ? &_save_bool::f : 0; + } +}; + +template +inline void swap(clone_ptr &lhs, clone_ptr &rhs) +{ + lhs.swap(rhs); +} + +template +class VarLengthArray +{ + QVarLengthArray impl; // ###should be replaced by self-written container that doesn't waste so much space +public: + typedef T value_type; + typedef T *iterator; + typedef const T *const_iterator; + typedef T *pointer; + typedef const T *const_pointer; + typedef T &reference; + typedef const T &const_reference; + + explicit VarLengthArray(int size = 0) + : impl(size) + { + } + // compiler-generated dtor, copy ctor, copy assignment are ok + // swap() makes little sense + + void push_back(const T &t) + { + impl.append(t); + } + int capacity() const + { + return impl.capacity(); + } + void clear() + { + impl.clear(); + } + size_t size() const + { + return impl.count(); + } + bool empty() const + { + return impl.isEmpty(); + } + void pop_back() + { + return impl.removeLast(); + } + void reserve(size_t n) + { + impl.reserve(n); + } + void resize(size_t n) + { + impl.resize(n); + } + + iterator begin() + { + return impl.data(); + } + iterator end() + { + return impl.data() + impl.size(); + } + const_iterator begin() const + { + return impl.data(); + } + const_iterator end() const + { + return impl.data() + impl.size(); + } + const_iterator cbegin() const + { + return begin(); + } + const_iterator cend() const + { + return end(); + } + + reference front() + { + return *impl.data(); + } + reference back() + { + return *(impl.data() + impl.size()); + } + const_reference front() const + { + return *impl.data(); + } + const_reference back() const + { + return *(impl.data() + impl.size()); + } + + reference operator[](size_t n) + { + return impl[n]; + } + const_reference operator[](size_t n) const + { + return impl[n]; + } +}; + +struct TypedPayload +{ + clone_ptr payload; + int sharedPointerId; + int metaTypeId; +}; + +struct BySharedPointerAndMetaTypeID : std::unary_function { + const int spid; + const int mtid; + BySharedPointerAndMetaTypeID(int spid, int mtid) + : spid(spid) + , mtid(mtid) + { + } + bool operator()(const TypedPayload &tp) const + { + return (mtid == -1 || mtid == tp.metaTypeId) + && (spid == -1 || spid == tp.sharedPointerId) ; + } +}; + +} + +} // namespace Akonadi + +namespace std +{ +template <> +inline void swap(Akonadi::_detail::TypedPayload &lhs, + Akonadi::_detail::TypedPayload &rhs) +{ + lhs.payload.swap(rhs.payload); + swap(lhs.sharedPointerId, rhs.sharedPointerId); + swap(lhs.metaTypeId, rhs.metaTypeId); +} +} + +namespace Akonadi +{ +//typedef _detail::VarLengthArray<_detail::TypedPayload,2> PayloadContainer; +typedef std::vector<_detail::TypedPayload> PayloadContainer; +} + +// disable Q_FOREACH on PayloadContainer (b/c it likes to take copies and clone_ptr doesn't like that) +template <> +class QForeachContainer +{ +}; + +namespace Akonadi +{ + +/** + * @internal + */ +class ItemPrivate : public QSharedData +{ +public: + explicit ItemPrivate(Item::Id id = -1) + : QSharedData() + , mRevision(-1) + , mId(id) + , mParent(Q_NULLPTR) + , mLegacyPayload() + , mPayloads() + , mCollectionId(-1) + , mSize(0) + , mModificationTime() + , mFlagsOverwritten(false) + , mTagsOverwritten(false) + , mSizeChanged(false) + , mClearPayload(false) + , mConversionInProgress(false) + { + } + + ItemPrivate(const ItemPrivate &other) + : QSharedData(other) + , mParent(Q_NULLPTR) + { + mId = other.mId; + mRemoteId = other.mRemoteId; + mRemoteRevision = other.mRemoteRevision; + Q_FOREACH (Attribute *attr, other.mAttributes) { + mAttributes.insert(attr->type(), attr->clone()); + } + if (other.mParent) { + mParent = new Collection(*(other.mParent)); + } + mFlags = other.mFlags; + mRevision = other.mRevision; + mTags = other.mTags; + mRelations = other.mRelations; + mSize = other.mSize; + mModificationTime = other.mModificationTime; + mMimeType = other.mMimeType; + mLegacyPayload = other.mLegacyPayload; + mPayloads = other.mPayloads; + mFlagsOverwritten = other.mFlagsOverwritten; + mSizeChanged = other.mSizeChanged; + mCollectionId = other.mCollectionId; + mClearPayload = other.mClearPayload; + mVirtualReferences = other.mVirtualReferences; + mGid = other.mGid; + mCachedPayloadParts = other.mCachedPayloadParts; + mTagsOverwritten = other.mTagsOverwritten; + mConversionInProgress = false; + + ItemChangeLog *changelog = ItemChangeLog::instance(); + changelog->addedFlags(this) = changelog->addedFlags(&other); + changelog->deletedFlags(this) = changelog->deletedFlags(&other); + changelog->addedTags(this) = changelog->addedTags(&other); + changelog->deletedTags(this) = changelog->deletedTags(&other); + changelog->deletedAttributes(this) = changelog->deletedAttributes(&other); + } + + ~ItemPrivate() + { + qDeleteAll(mAttributes); + delete mParent; + + ItemChangeLog::instance()->clearItemChangelog(this); + } + + void resetChangeLog() + { + mFlagsOverwritten = false; + mSizeChanged = false; + mTagsOverwritten = false; + ItemChangeLog::instance()->clearItemChangelog(this); + } + + bool hasMetaTypeId(int mtid) const + { + return std::find_if(mPayloads.cbegin(), mPayloads.cend(), + _detail::BySharedPointerAndMetaTypeID(-1, mtid)) + != mPayloads.cend(); + } + + Internal::PayloadBase *payloadBaseImpl(int spid, int mtid) const + { + auto it = std::find_if(mPayloads.cbegin(), mPayloads.cend(), + _detail::BySharedPointerAndMetaTypeID(spid, mtid)); + return it == mPayloads.cend() ? 0 : it->payload.get() ; + } + + bool movePayloadFrom(ItemPrivate *other, int mtid) const /*sic!*/ + { + assert(other); + const size_t oldSize = mPayloads.size(); + PayloadContainer &oPayloads = other->mPayloads; + const _detail::BySharedPointerAndMetaTypeID matcher(-1, mtid); + const size_t numMatching = std::count_if(oPayloads.begin(), oPayloads.end(), matcher); + mPayloads.resize(oldSize + numMatching); + using namespace std; // for swap() + for (PayloadContainer::iterator + dst = mPayloads.begin() + oldSize, + src = oPayloads.begin(), end = oPayloads.end() ; src != end ; ++src) { + if (matcher(*src)) { + swap(*dst, *src); + ++dst; + } + } + return numMatching > 0 ; + } + +#if 0 + std::auto_ptr takePayloadBaseImpl(int spid, int mtid) + { + PayloadContainer::iterator it + = std::find_if(mPayloads.begin(), mPayloads.end(), + _detail::BySharedPointerAndMetaTypeID(spid, mtid)); + if (it == mPayloads.end()) { + return std::auto_ptr(); + } + std::rotate(it, it + 1, mPayloads.end()); + std::auto_ptr result(it->payload.release()); + mPayloads.pop_back(); + return result; + } +#endif + + void setPayloadBaseImpl(int spid, int mtid, std::unique_ptr &p, bool add) const /*sic!*/ + { + + if (!add) { + mLegacyPayload.reset(); + } + + if (!p.get()) { + if (!add) { + mPayloads.clear(); + } + return; + } + + // if !add, delete all payload variants + // (they're conversions of each other) + mPayloads.resize(add ? mPayloads.size() + 1 : 1); + _detail::TypedPayload &tp = mPayloads.back(); + tp.payload.reset(p.release()); + tp.sharedPointerId = spid; + tp.metaTypeId = mtid; + } + + void setLegacyPayloadBaseImpl(std::unique_ptr p); + void tryEnsureLegacyPayload() const; + + // Utilise the 4-bytes padding from QSharedData + int mRevision; + Item::Id mId; + QString mRemoteId; + QString mRemoteRevision; + QHash mAttributes; + mutable Collection *mParent; + mutable _detail::clone_ptr mLegacyPayload; + mutable PayloadContainer mPayloads; + Item::Flags mFlags; + Tag::List mTags; + Relation::List mRelations; + Item::Id mCollectionId; + Collection::List mVirtualReferences; + // TODO: Maybe just use uint? Would save us another 8 bytes after reordering + qint64 mSize; + QDateTime mModificationTime; + QString mMimeType; + QString mGid; + QSet mCachedPayloadParts; + bool mFlagsOverwritten : 1; + bool mTagsOverwritten : 1; + bool mSizeChanged : 1; + bool mClearPayload : 1; + mutable bool mConversionInProgress; + // 6 bytes padding here +}; + +} + +#endif diff --git a/src/core/itemchangelog.cpp b/src/core/itemchangelog.cpp new file mode 100644 index 0000000..0781d0d --- /dev/null +++ b/src/core/itemchangelog.cpp @@ -0,0 +1,73 @@ +/* + * Copyright 2015 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "itemchangelog_p.h" + +using namespace Akonadi; + +ItemChangeLog *ItemChangeLog::sInstance = Q_NULLPTR; + +ItemChangeLog *ItemChangeLog::instance() +{ + if (!sInstance) { + sInstance = new ItemChangeLog; + } + return sInstance; +} + +ItemChangeLog::ItemChangeLog() +{ +} + +Item::Flags& ItemChangeLog::addedFlags(const ItemPrivate *priv) +{ + return m_addedFlags[const_cast(priv)]; +} + +Item::Flags& ItemChangeLog::deletedFlags(const ItemPrivate *priv) +{ + return m_deletedFlags[const_cast(priv)]; +} + +Tag::List& ItemChangeLog::addedTags(const ItemPrivate *priv) +{ + return m_addedTags[const_cast(priv)]; +} + +Tag::List& ItemChangeLog::deletedTags(const ItemPrivate *priv) +{ + return m_deletedTags[const_cast(priv)]; +} + +QSet& ItemChangeLog::deletedAttributes(const ItemPrivate *priv) +{ + return m_deletedAttributes[const_cast(priv)]; +} + +void ItemChangeLog::clearItemChangelog(const ItemPrivate *priv) +{ + ItemPrivate *p = const_cast(priv); + m_addedFlags.remove(p); + m_deletedFlags.remove(p); + m_addedTags.remove(p); + m_deletedTags.remove(p); + m_deletedAttributes.remove(p); +} diff --git a/src/core/itemchangelog_p.h b/src/core/itemchangelog_p.h new file mode 100644 index 0000000..035f5d1 --- /dev/null +++ b/src/core/itemchangelog_p.h @@ -0,0 +1,61 @@ +/* + * Copyright 2015 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef ITEMCHANGELOG_H +#define ITEMCHANGELOG_H + +#include "item.h" + +#include "akonaditests_export.h" + +namespace Akonadi +{ + +class AKONADI_TESTS_EXPORT ItemChangeLog +{ +public: + static ItemChangeLog *instance(); + + Item::Flags& addedFlags(const ItemPrivate *priv); + Item::Flags& deletedFlags(const ItemPrivate *priv); + + Tag::List& addedTags(const ItemPrivate *priv); + Tag::List& deletedTags(const ItemPrivate *priv); + + QSet& deletedAttributes(const ItemPrivate *priv); + + void clearItemChangelog(const ItemPrivate *priv); + +private: + explicit ItemChangeLog(); + + static ItemChangeLog *sInstance; + + QHash m_addedFlags; + QHash m_deletedFlags; + QHash m_addedTags; + QHash m_deletedTags; + QHash> m_deletedAttributes; +}; + +} // namespace Akonadi + +#endif // ITEMCHANGELOG_H diff --git a/src/core/itemfetchscope.cpp b/src/core/itemfetchscope.cpp new file mode 100644 index 0000000..64afcbf --- /dev/null +++ b/src/core/itemfetchscope.cpp @@ -0,0 +1,230 @@ +/* + Copyright (c) 2008 Kevin Krammer + + 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 "itemfetchscope.h" + +#include "itemfetchscope_p.h" + +#include + +using namespace Akonadi; + +ItemFetchScope::ItemFetchScope() +{ + d = new ItemFetchScopePrivate(); +} + +ItemFetchScope::ItemFetchScope(const ItemFetchScope &other) + : d(other.d) +{ +} + +ItemFetchScope::~ItemFetchScope() +{ +} + +ItemFetchScope &ItemFetchScope::operator=(const ItemFetchScope &other) +{ + if (&other != this) { + d = other.d; + } + + return *this; +} + +QSet< QByteArray > ItemFetchScope::payloadParts() const +{ + return d->mPayloadParts; +} + +void ItemFetchScope::fetchPayloadPart(const QByteArray &part, bool fetch) +{ + if (fetch) { + d->mPayloadParts.insert(part); + } else { + d->mPayloadParts.remove(part); + } +} + +bool ItemFetchScope::fullPayload() const +{ + return d->mFullPayload; +} + +void ItemFetchScope::fetchFullPayload(bool fetch) +{ + d->mFullPayload = fetch; +} + +QSet< QByteArray > ItemFetchScope::attributes() const +{ + return d->mAttributes; +} + +void ItemFetchScope::fetchAttribute(const QByteArray &type, bool fetch) +{ + if (fetch) { + d->mAttributes.insert(type); + } else { + d->mAttributes.remove(type); + } +} + +bool ItemFetchScope::allAttributes() const +{ + return d->mAllAttributes; +} + +void ItemFetchScope::fetchAllAttributes(bool fetch) +{ + d->mAllAttributes = fetch; +} + +bool ItemFetchScope::isEmpty() const +{ + return d->mPayloadParts.isEmpty() && d->mAttributes.isEmpty() && !d->mFullPayload && !d->mAllAttributes && !d->mFetchTags && !d->mFetchVRefs; +} + +bool ItemFetchScope::cacheOnly() const +{ + return d->mCacheOnly; +} + +void ItemFetchScope::setCacheOnly(bool cacheOnly) +{ + d->mCacheOnly = cacheOnly; +} + +void ItemFetchScope::setCheckForCachedPayloadPartsOnly(bool check) +{ + if (check) { + setCacheOnly(true); + } + d->mCheckCachedPayloadPartsOnly = check; +} + +bool ItemFetchScope::checkForCachedPayloadPartsOnly() const +{ + return d->mCheckCachedPayloadPartsOnly; +} + +ItemFetchScope::AncestorRetrieval ItemFetchScope::ancestorRetrieval() const +{ + return d->mAncestorDepth; +} + +void ItemFetchScope::setAncestorRetrieval(AncestorRetrieval depth) +{ + d->mAncestorDepth = depth; +} + +void ItemFetchScope::setFetchModificationTime(bool retrieveMtime) +{ + d->mFetchMtime = retrieveMtime; +} + +bool ItemFetchScope::fetchModificationTime() const +{ + return d->mFetchMtime; +} + +void ItemFetchScope::setFetchGid(bool retrieveGid) +{ + d->mFetchGid = retrieveGid; +} + +bool ItemFetchScope::fetchGid() const +{ + return d->mFetchGid; +} + +void ItemFetchScope::setIgnoreRetrievalErrors(bool ignore) +{ + d->mIgnoreRetrievalErrors = ignore; +} + +bool ItemFetchScope::ignoreRetrievalErrors() const +{ + return d->mIgnoreRetrievalErrors; +} + +void ItemFetchScope::setFetchChangedSince(const QDateTime &changedSince) +{ + d->mChangedSince = changedSince; +} + +QDateTime ItemFetchScope::fetchChangedSince() const +{ + return d->mChangedSince; +} + +void ItemFetchScope::setFetchRemoteIdentification(bool retrieveRid) +{ + d->mFetchRid = retrieveRid; +} + +bool ItemFetchScope::fetchRemoteIdentification() const +{ + return d->mFetchRid; +} + +void ItemFetchScope::setFetchTags(bool fetchTags) +{ + d->mFetchTags = fetchTags; +} + +bool ItemFetchScope::fetchTags() const +{ + return d->mFetchTags; +} + +void ItemFetchScope::setTagFetchScope(const TagFetchScope &tagFetchScope) +{ + d->mTagFetchScope = tagFetchScope; +} + +TagFetchScope &ItemFetchScope::tagFetchScope() +{ + return d->mTagFetchScope; +} + +TagFetchScope ItemFetchScope::tagFetchScope() const +{ + return d->mTagFetchScope; +} + +void ItemFetchScope::setFetchVirtualReferences(bool fetchVRefs) +{ + d->mFetchVRefs = fetchVRefs; +} + +bool ItemFetchScope::fetchVirtualReferences() const +{ + return d->mFetchVRefs; +} + +void ItemFetchScope::setFetchRelations(bool fetchRelations) +{ + d->mFetchRelations = fetchRelations; +} + +bool ItemFetchScope::fetchRelations() const +{ + return d->mFetchRelations; +} diff --git a/src/core/itemfetchscope.h b/src/core/itemfetchscope.h new file mode 100644 index 0000000..1e138da --- /dev/null +++ b/src/core/itemfetchscope.h @@ -0,0 +1,438 @@ +/* + Copyright (c) 2008 Kevin Krammer + + 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 ITEMFETCHSCOPE_H +#define ITEMFETCHSCOPE_H + +#include "akonadicore_export.h" + +#include +#include +#include +#include + +template class QSet; + +namespace Akonadi +{ + +class ItemFetchScopePrivate; +class TagFetchScope; + +/** + * @short Specifies which parts of an item should be fetched from the Akonadi storage. + * + * When items are fetched from server either by using ItemFetchJob explicitly or + * when it is being used internally by other classes, e.g. ItemModel, the scope + * of the fetch operation can be tailored to the application's current needs. + * + * There are two supported ways of changing the currently active ItemFetchScope + * of classes: + * - in-place: modify the ItemFetchScope object the other class holds as a member + * - replace: replace the other class' member with a new scope object + * + * Example: modifying an ItemFetchJob's scope @c in-place + * @code + * Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob( collection ); + * job->fetchScope().fetchFullPayload(); + * job->fetchScope().fetchAttribute(); + * @endcode + * + * Example: @c replacing an ItemFetchJob's scope + * @code + * Akonadi::ItemFetchScope scope; + * scope.fetchFullPayload(); + * scope.fetchAttribute(); + * + * Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob( collection ); + * job->setFetchScope( scope ); + * @endcode + * + * This class is implicitly shared. + * + * @author Kevin Krammer + */ +class AKONADICORE_EXPORT ItemFetchScope +{ +public: + /** + * Describes the ancestor retrieval depth. + * @since 4.4 + */ + enum AncestorRetrieval { + None, ///< No ancestor retrieval at all (the default) + Parent, ///< Only retrieve the immediate parent collection + All ///< Retrieve all ancestors, up to Collection::root() + }; + + /** + * Creates an empty item fetch scope. + * + * Using an empty scope will only fetch the very basic meta data of items, + * e.g. local id, remote id and mime type + */ + ItemFetchScope(); + + /** + * Creates a new item fetch scope from an @p other. + */ + ItemFetchScope(const ItemFetchScope &other); + + /** + * Destroys the item fetch scope. + */ + ~ItemFetchScope(); + + /** + * Assigns the @p other to this scope and returns a reference to this scope. + */ + ItemFetchScope &operator=(const ItemFetchScope &other); + + /** + * Returns the payload parts that should be fetched. + * + * @see fetchPayloadPart() + */ + QSet payloadParts() const; + + /** + * Sets which payload parts shall be fetched. + * + * @param part The payload part identifier. + * Valid values depend on the item type. + * @param fetch @c true to fetch this part, @c false otherwise. + */ + void fetchPayloadPart(const QByteArray &part, bool fetch = true); + + /** + * Returns whether the full payload should be fetched. + * + * @see fetchFullPayload() + */ + bool fullPayload() const; + + /** + * Sets whether the full payload shall be fetched. + * The default is @c false. + * + * @param fetch @c true if the full payload should be fetched, @c false otherwise. + */ + void fetchFullPayload(bool fetch = true); + + /** + * Returns all explicitly fetched attributes. + * + * Undefined if fetchAllAttributes() returns true. + * + * @see fetchAttribute() + */ + QSet attributes() const; + + /** + * Sets whether the attribute of the given @p type should be fetched. + * + * @param type The attribute type to fetch. + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + void fetchAttribute(const QByteArray &type, bool fetch = true); + + /** + * Sets whether the attribute of the requested type should be fetched. + * + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + template inline void fetchAttribute(bool fetch = true) + { + T dummy; + fetchAttribute(dummy.type(), fetch); + } + + /** + * Returns whether all available attributes should be fetched. + * + * @see fetchAllAttributes() + */ + bool allAttributes() const; + + /** + * Sets whether all available attributes should be fetched. + * The default is @c false. + * + * @param fetch @c true if all available attributes should be fetched, @c false otherwise. + */ + void fetchAllAttributes(bool fetch = true); + + /** + * Returns whether payload data should be requested from remote sources or just + * from the local cache. + * + * @see setCacheOnly() + */ + bool cacheOnly() const; + + /** + * Sets whether payload data should be requested from remote sources or just + * from the local cache. + * + * @param cacheOnly @c true if no remote data should be requested, + * @c false otherwise (the default). + */ + void setCacheOnly(bool cacheOnly); + + /** + * Sets whether payload will be fetched or there will be only a test performed if the + * requested payload is in the cache. Calling it calls @see setCacheOnly with true automatically. + * Default is fetching the data. + * + * @since 4.11 + */ + void setCheckForCachedPayloadPartsOnly(bool check = true); + + /** + * Returns whether payload data should be fetched or only checked for presence in the cache. + * + * @see setCheckForCachedPayloadPartsOnly() + * + * @since 4.11 + */ + bool checkForCachedPayloadPartsOnly() const; + + /** + * Sets how many levels of ancestor collections should be included in the retrieval. + * The default is AncestorRetrieval::None. + * + * @param ancestorDepth The desired ancestor retrieval depth. + * @since 4.4 + */ + void setAncestorRetrieval(AncestorRetrieval ancestorDepth); + + /** + * Returns the ancestor retrieval depth. + * + * @see setAncestorRetrieval() + * @since 4.4 + */ + AncestorRetrieval ancestorRetrieval() const; + + /** + * Enables retrieval of the item modification time. + * This is enabled by default for backward compatibility reasons. + * + * @param retrieveMtime @c true to retrieve the modification time, @c false otherwise + * @since 4.6 + */ + void setFetchModificationTime(bool retrieveMtime); + + /** + * Returns whether item modification time should be retrieved. + * + * @see setFetchModificationTime() + * @since 4.6 + */ + bool fetchModificationTime() const; + + /** + * Enables retrieval of the item GID. + * This is disabled by default. + * + * @param retrieveGID @c true to retrieve the GID, @c false otherwise + * @since 4.12 + */ + void setFetchGid(bool retrieveGID); + + /** + * Returns whether item GID should be retrieved. + * + * @see setFetchGid() + * @since 4.12 + */ + bool fetchGid() const; + + /** + * Ignore retrieval errors while fetching items, and always deliver what is available. + * If items have missing parts and the part can't be retrieved from the resource (i.e. because the system is offline), + * the fetch job would normally just fail. By setting this flag, the errors are ignored, + * and all items which could be fetched completely are returned. + * Note that all items that are returned are completely fetched, and incomplete items are simply ignored. + * This flag is useful for displaying everything that is available, where it is not crucial to have all items. + * Never use this for things like data migration or alike. + * + * @since 4.10 + */ + void setIgnoreRetrievalErrors(bool enabled); + + /** + * Returns whether retrieval errors should be ignored. + * + * @see setIgnoreRetrievalErrors() + * @since 4.10 + */ + bool ignoreRetrievalErrors() const; + + /** + * Returns @c true if there is nothing to fetch. + */ + bool isEmpty() const; + + /** + * Only fetch items that were added or modified after given timestamp + * + * When this property is set, all results are filtered, i.e. even when you + * request an item with a specific ID, it will not be fetched unless it was + * modified after @p changedSince timestamp. + * + * @param changedSince The timestamp of oldest modified item to fetch + * @since 4.11 + */ + void setFetchChangedSince(const QDateTime &changedSince); + + /** + * Returns timestamp of the oldest item to fetch. + */ + QDateTime fetchChangedSince() const; + + /** + * Fetch remote identification for items. + * + * These include Akonadi::Item::remoteId() and Akonadi::Item::remoteRevision(). This should + * be off for normal clients usually, to save memory (not to mention normal clients should + * not be concerned with these information anyway). It is however crucial for resource agents. + * For backward compatibility the default is @c true. + * + * @param retrieveRid whether or not to load remote identification. + * @since 4.12 + */ + void setFetchRemoteIdentification(bool retrieveRid); + + /** + * Returns whether item remote identification should be retrieved. + * + * @see setFetchRemoteIdentification() + * @since 4.12 + */ + bool fetchRemoteIdentification() const; + + /** + * Fetch tags for items. + * + * The fetched tags have only the Tag::id() set and need to be fetched first to access further attributes. + * + * The default is @c false. + * + * @param fetchTags whether or not to load tags. + * @since 4.13 + */ + void setFetchTags(bool fetchTags); + + /** + * Returns whether tags should be retrieved. + * + * @see setFetchTags() + * @since 4.13 + */ + bool fetchTags() const; + + /** + * Sets the tag fetch scope. + * + * The TagFetchScope controls how much of an tags's data is fetched + * from the server. + * + * By default setFetchIdOnly is set to true on the tag fetch scope. + * + * @param fetchScope The new fetch scope for tag fetch operations. + * @see fetchScope() + * @since 4.15 + */ + void setTagFetchScope(const TagFetchScope &fetchScope); + + /** + * Returns the tag fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the TagFetchScope documentation + * for an example. + * + * By default setFetchIdOnly is set to true on the tag fetch scope. + * + * @return a reference to the current tag fetch scope + * + * @see setFetchScope() for replacing the current tag fetch scope + * @since 4.15 + */ + TagFetchScope &tagFetchScope(); + + /** + * Returns the tag fetch scope. + * + * By default setFetchIdOnly is set to true on the tag fetch scope. + * + * @return a reference to the current tag fetch scope + * + * @see setFetchScope() for replacing the current tag fetch scope + * @since 4.15 + */ + TagFetchScope tagFetchScope() const; + + /** + * Returns whether to fetch list of virtual collections the item is linked to + * + * @param fetchVRefs whether or not to fetch virtualc references + * @since 4.14 + */ + void setFetchVirtualReferences(bool fetchVRefs); + + /** + * Returns whether virtual references should be retrieved. + * + * @see setFetchVirtualReferences() + * @since 4.14 + */ + bool fetchVirtualReferences() const; + + /** + * Fetch relations for items. + * + * The default is @c false. + * + * @param fetchTags whether or not to load relations. + * @since 4.15 + */ + void setFetchRelations(bool fetchRelations); + + /** + * Returns whether relations should be retrieved. + * + * @see setFetchRelations() + * @since 4.15 + */ + bool fetchRelations() const; + +private: + //@cond PRIVATE + QSharedDataPointer d; + //@endcond +}; + +} + +Q_DECLARE_METATYPE(Akonadi::ItemFetchScope) + +#endif diff --git a/src/core/itemfetchscope_p.h b/src/core/itemfetchscope_p.h new file mode 100644 index 0000000..e5d1f3c --- /dev/null +++ b/src/core/itemfetchscope_p.h @@ -0,0 +1,97 @@ +/* + Copyright (c) 2008 Kevin Krammer + + 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 ITEMFETCHSCOPE_P_H +#define ITEMFETCHSCOPE_P_H + +#include +#include +#include +#include "itemfetchscope.h" +#include "tagfetchscope.h" + +namespace Akonadi +{ + +/** + * @internal + */ +class ItemFetchScopePrivate : public QSharedData +{ +public: + ItemFetchScopePrivate() + : mAncestorDepth(ItemFetchScope::None) + , mFullPayload(false) + , mAllAttributes(false) + , mCacheOnly(false) + , mCheckCachedPayloadPartsOnly(false) + , mFetchMtime(true) + , mIgnoreRetrievalErrors(false) + , mFetchRid(true) + , mFetchGid(false) + , mFetchTags(false) + , mFetchVRefs(false) + , mFetchRelations(false) + { + mTagFetchScope.setFetchIdOnly(true); + } + + ItemFetchScopePrivate(const ItemFetchScopePrivate &other) + : QSharedData(other) + { + mPayloadParts = other.mPayloadParts; + mAttributes = other.mAttributes; + mAncestorDepth = other.mAncestorDepth; + mFullPayload = other.mFullPayload; + mAllAttributes = other.mAllAttributes; + mCacheOnly = other.mCacheOnly; + mCheckCachedPayloadPartsOnly = other.mCheckCachedPayloadPartsOnly; + mFetchMtime = other.mFetchMtime; + mIgnoreRetrievalErrors = other.mIgnoreRetrievalErrors; + mChangedSince = other.mChangedSince; + mFetchRid = other.mFetchRid; + mFetchGid = other.mFetchGid; + mFetchTags = other.mFetchTags; + mTagFetchScope = other.mTagFetchScope; + mFetchVRefs = other.mFetchVRefs; + mFetchRelations = other.mFetchRelations; + } + +public: + QSet mPayloadParts; + QSet mAttributes; + ItemFetchScope::AncestorRetrieval mAncestorDepth; + bool mFullPayload; + bool mAllAttributes; + bool mCacheOnly; + bool mCheckCachedPayloadPartsOnly; + bool mFetchMtime; + bool mIgnoreRetrievalErrors; + QDateTime mChangedSince; + bool mFetchRid; + bool mFetchGid; + bool mFetchTags; + TagFetchScope mTagFetchScope; + bool mFetchVRefs; + bool mFetchRelations; +}; + +} + +#endif diff --git a/src/core/itemmonitor.cpp b/src/core/itemmonitor.cpp new file mode 100644 index 0000000..a2435ea --- /dev/null +++ b/src/core/itemmonitor.cpp @@ -0,0 +1,87 @@ +/* + Copyright (c) 2007-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. +*/ + +#include "itemmonitor.h" +#include "itemmonitor_p.h" + +#include "itemfetchscope.h" + +#include + +using namespace Akonadi; + +ItemMonitor::ItemMonitor() + : d(new Private(this)) +{ +} + +ItemMonitor::~ItemMonitor() +{ + delete d; +} + +void ItemMonitor::setItem(const Item &item) +{ + if (item == d->mItem) { + return; + } + + d->mMonitor->setItemMonitored(d->mItem, false); + + d->mItem = item; + + d->mMonitor->setItemMonitored(d->mItem, true); + + if (!d->mItem.isValid()) { + itemRemoved(); + return; + } + + // start initial fetch of the new item + ItemFetchJob *job = new ItemFetchJob(d->mItem); + job->setFetchScope(fetchScope()); + + d->connect(job, SIGNAL(result(KJob*)), d, SLOT(initialFetchDone(KJob*))); +} + +Item ItemMonitor::item() const +{ + return d->mItem; +} + +void ItemMonitor::itemChanged(const Item &item) +{ + Q_UNUSED(item) +} + +void ItemMonitor::itemRemoved() +{ +} + +void ItemMonitor::setFetchScope(const ItemFetchScope &fetchScope) +{ + d->mMonitor->setItemFetchScope(fetchScope); +} + +ItemFetchScope &ItemMonitor::fetchScope() +{ + return d->mMonitor->itemFetchScope(); +} + +#include "moc_itemmonitor_p.cpp" diff --git a/src/core/itemmonitor.h b/src/core/itemmonitor.h new file mode 100644 index 0000000..41b45f7 --- /dev/null +++ b/src/core/itemmonitor.h @@ -0,0 +1,154 @@ +/* + Copyright (c) 2007-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_ITEMMONITOR_H +#define AKONADI_ITEMMONITOR_H + +#include "akonadicore_export.h" +#include + +namespace Akonadi +{ + +class Item; +class ItemFetchScope; + +/** + * @short A convenience class to monitor a single item for changes. + * + * This class can be used as a base class for classes that want to show + * a single item to the user and keep track of status changes of the item + * without having to using a Monitor object themself. + * + * Example: + * + * @code + * + * // A label that shows the name of a contact item + * + * class ContactLabel : public QLabel, public Akonadi::ItemMonitor + * { + * public: + * ContactLabel( QWidget *parent = Q_NULLPTR ) + * : QLabel( parent ) + * { + * setText( "No Name" ); + * } + * + * protected: + * virtual void itemChanged( const Akonadi::Item &item ) + * { + * if ( item.mimeType() != "text/directory" ) + * return; + * + * const KContacts::Addressee addr = item.payload(); + * setText( addr.fullName() ); + * } + * + * virtual void itemRemoved() + * { + * setText( "No Name" ); + * } + * }; + * + * ... + * + * ContactLabel *label = new ContactLabel( this ); + * + * const Akonadi::Item item = fetchJob->items().at(0); + * label->setItem( item ); + * + * @endcode + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT ItemMonitor +{ +public: + /** + * Creates a new item monitor. + */ + ItemMonitor(); + + /** + * Destroys the item monitor. + */ + virtual ~ItemMonitor(); + + /** + * Sets the @p item that shall be monitored. + */ + void setItem(const Item &item); + + /** + * Returns the currently monitored item. + */ + Item item() const; + +protected: + /** + * This method is called whenever the monitored item has changed. + * + * @param item The changed item. + */ + virtual void itemChanged(const Item &item); + + /** + * This method is called whenever the monitored item has been removed. + */ + virtual void itemRemoved(); + + /** + * Sets the item fetch scope. + * + * Controls how much of an item's data is fetched from the server, e.g. + * whether to fetch the full item payload or only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(const ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond + + Q_DISABLE_COPY(ItemMonitor) +}; + +} + +#endif diff --git a/src/core/itemmonitor_p.h b/src/core/itemmonitor_p.h new file mode 100644 index 0000000..b03a9e7 --- /dev/null +++ b/src/core/itemmonitor_p.h @@ -0,0 +1,91 @@ +/* + Copyright (c) 2007-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_ITEMMONITOR_P_H +#define AKONADI_ITEMMONITOR_P_H + +#include + +#include "itemfetchjob.h" +#include "monitor.h" + +namespace Akonadi +{ + +/** + * @internal + */ +class Q_DECL_HIDDEN ItemMonitor::Private : public QObject +{ + Q_OBJECT + +public: + Private(ItemMonitor *parent) + : QObject(0) + , mParent(parent) + , mMonitor(new Monitor()) + { + connect(mMonitor, &Monitor::itemChanged, + this, &Private::slotItemChanged); + connect(mMonitor, &Monitor::itemRemoved, + this, &Private::slotItemRemoved); + } + + ~Private() + { + delete mMonitor; + } + + ItemMonitor *mParent; + Item mItem; + Monitor *mMonitor; + +private Q_SLOTS: + void slotItemChanged(const Akonadi::Item &item, const QSet &aSet) + { + Q_UNUSED(aSet); + mItem.apply(item); + mParent->itemChanged(item); + } + + void slotItemRemoved(const Akonadi::Item &item) + { + Q_UNUSED(item); + mItem = Item(); + mParent->itemRemoved(); + } + + void initialFetchDone(KJob *job) + { + if (job->error()) { + return; + } + + ItemFetchJob *fetchJob = qobject_cast(job); + + if (!fetchJob->items().isEmpty()) { + mItem = fetchJob->items().at(0); + mParent->itemChanged(mItem); + } + } +}; + +} + +#endif diff --git a/src/core/itempayloadinternals_p.h b/src/core/itempayloadinternals_p.h new file mode 100644 index 0000000..5faa789 --- /dev/null +++ b/src/core/itempayloadinternals_p.h @@ -0,0 +1,523 @@ +/* + Copyright (c) 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 ITEMPAYLOADINTERNALS_P_H +#define ITEMPAYLOADINTERNALS_P_H + +#include "supertrait.h" + +#include +#include +#include + +#include +#include +#include + +#include + +#include "exception.h" + +//@cond PRIVATE Doxygen 1.7.1 hangs processing this file. so skip it. +//for more info, see https://bugzilla.gnome.org/show_bug.cgi?id=531637 + +/* WARNING + * The below is an implementation detail of the Item class. It is not to be + * considered public API, and subject to change without notice + */ + +// Forward-declare boost::shared_ptr so that we don't have to explicitly include +// it. Caller that tries to use it will aready have it included anyway +namespace boost +{ +template class shared_ptr; +template +shared_ptr dynamic_pointer_cast(shared_ptr const &ptr) noexcept; +} + +namespace Akonadi +{ +namespace Internal +{ + +template +struct has_clone_method +{ +private: + template + struct sfinae + { + }; + struct No + { + }; + struct Yes + { + No no[2]; + }; + template + static No test(...); + template + static Yes test(sfinae *); +public: + static const bool value = sizeof(test(0)) == sizeof(Yes) ; +}; + +template +struct clone_traits_helper +{ + // runtime error (commented in) or compiletime error (commented out)? + // ### runtime error, until we check has_clone_method in the + // ### Item::payload impl directly... + template + static T *clone(U) + { + return 0; + } +}; + +template +struct clone_traits_helper +{ + static T *clone(T *t) + { + return t ? t->clone() : 0 ; + } +}; + +template +struct clone_traits : clone_traits_helper::value> +{ +}; + +template +struct shared_pointer_traits +{ + static const bool defined = false; +}; + +template +struct shared_pointer_traits> +{ + static const bool defined = true; + typedef T element_type; + + template + struct make + { + typedef boost::shared_ptr type; + }; + + typedef QSharedPointer next_shared_ptr; +}; + +template +struct shared_pointer_traits> +{ + static const bool defined = true; + typedef T element_type; + + template + struct make { + typedef QSharedPointer type; + }; + + typedef std::shared_ptr next_shared_ptr; +}; + +template +struct shared_pointer_traits> +{ + static const bool defined = true; + typedef T element_type; + + template + struct make { + typedef std::shared_ptr type; + }; + + typedef boost::shared_ptr next_shared_ptr; +}; + +template +struct is_shared_pointer +{ + static const bool value = shared_pointer_traits::defined; +}; + +template +struct identity +{ + typedef T type; +}; + +template +struct get_hierarchy_root; + +template +struct get_hierarchy_root_recurse : get_hierarchy_root +{ +}; + +template +struct get_hierarchy_root_recurse : identity +{ +}; + +template +struct get_hierarchy_root : get_hierarchy_root_recurse::Type> +{ +}; + +template +struct get_hierarchy_root> +{ + typedef boost::shared_ptr::type> type; +}; + +template +struct get_hierarchy_root> +{ + typedef QSharedPointer::type> type; +}; + +template +struct get_hierarchy_root> +{ + typedef std::shared_ptr::type> type; +}; + +/** + @internal + Payload type traits. Implements specialized handling for polymorphic types and smart pointers. + The default one is never used (as isPolymorphic is always false) and only contains safe dummy + implementations to make the compiler happy (in practice it will always optimized away anyway). +*/ +template +struct PayloadTrait +{ + /// type of the payload object contained inside a shared pointer + typedef T ElementType; + // the metatype id for the element type, or for pointer-to-element + // type, if in a shared pointer + static int elementMetaTypeId() + { + return qMetaTypeId(); + } + /// type of the base class of the payload object inside a shared pointer, + /// same as ElementType if there is no super class + typedef typename Akonadi::SuperClass::Type SuperElementType; + /// type of this payload object + typedef T Type; + /// type of the payload to store a base class of this payload + /// (eg. a shared pointer containing a pointer to SuperElementType) + /// same as Type if there is not super class + typedef typename Akonadi::SuperClass::Type SuperType; + /// indicates if this payload is polymorphic, that is is a shared pointer + /// and has a known super class + static const bool isPolymorphic = false; + /// checks an object of this payload type for being @c null + static inline bool isNull(const Type &p) + { + Q_UNUSED(p); + return true; + } + /// casts to Type from @c U + /// throws a PayloadException if casting failed + template + static inline Type castFrom(const U &) + { + throw PayloadException("you should never get here"); + } + /// tests if casting from @c U to Type is possible + template + static inline bool canCastFrom(const U &) + { + return false; + } + /// cast to @c U from Type + template + static inline U castTo(const Type &) + { + throw PayloadException("you should never get here"); + } + template + static T clone(const U &) + { + throw PayloadException("clone: you should never get here"); + } + /// defines the type of shared pointer used (0: none, > 0: boost::shared_ptr, QSharedPointer, ...) + static const unsigned int sharedPointerId = 0; +}; + +/** + @internal + Payload type trait specialization for boost::shared_ptr + for documentation of the various members, see above +*/ +template +struct PayloadTrait> +{ + typedef T ElementType; + static int elementMetaTypeId() + { + return qMetaTypeId(); + } + typedef typename Akonadi::SuperClass::Type SuperElementType; + typedef boost::shared_ptr Type; + typedef boost::shared_ptr SuperType; + static const bool isPolymorphic = !std::is_same::value; + static inline bool isNull(const Type &p) + { + return p.get() == 0; + } + template + static inline Type castFrom(const boost::shared_ptr &p) + { + const Type sp = boost::dynamic_pointer_cast(p); + if (sp.get() != 0 || p.get() == 0) { + return sp; + } + throw PayloadException("boost::dynamic_pointer_cast failed"); + } + template + static inline bool canCastFrom(const boost::shared_ptr &p) + { + const Type sp = boost::dynamic_pointer_cast(p); + return sp.get() != 0 || p.get() == 0; + } + template + static inline boost::shared_ptr castTo(const Type &p) + { + const boost::shared_ptr sp = boost::dynamic_pointer_cast(p); + return sp; + } + static boost::shared_ptr clone(const QSharedPointer &t) + { + if (T *nt = clone_traits::clone(t.data())) { + return boost::shared_ptr(nt); + } else { + return boost::shared_ptr(); + } + } + static boost::shared_ptr clone(const std::shared_ptr &t) + { + if (T *nt = clone_traits::clone(t.get())) { + return boost::shared_ptr(nt); + } else { + return boost::shared_ptr(); + } + } + static const unsigned int sharedPointerId = 1; +}; + +/** + @internal + Payload type trait specialization for QSharedPointer + for documentation of the various members, see above +*/ +template +struct PayloadTrait> +{ + typedef T ElementType; + static int elementMetaTypeId() + { + return qMetaTypeId(); + } + typedef typename Akonadi::SuperClass::Type SuperElementType; + typedef QSharedPointer Type; + typedef QSharedPointer SuperType; + static const bool isPolymorphic = !std::is_same::value; + static inline bool isNull(const Type &p) + { + return p.isNull(); + } + template + static inline Type castFrom(const QSharedPointer &p) + { + const Type sp = qSharedPointerDynamicCast(p); + if (!sp.isNull() || p.isNull()) { + return sp; + } + throw PayloadException("qSharedPointerDynamicCast failed"); + } + template + static inline bool canCastFrom(const QSharedPointer &p) + { + const Type sp = qSharedPointerDynamicCast(p); + return !sp.isNull() || p.isNull(); + } + template + static inline QSharedPointer castTo(const Type &p) + { + const QSharedPointer sp = qSharedPointerDynamicCast(p); + return sp; + } + static QSharedPointer clone(const boost::shared_ptr &t) + { + if (T *nt = clone_traits::clone(t.get())) { + return QSharedPointer(nt); + } else { + return QSharedPointer(); + } + } + static QSharedPointer clone(const std::shared_ptr &t) + { + if (T *nt = clone_traits::clone(t.get())) { + return QSharedPointer(nt); + } else { + return QSharedPointer(); + } + } + static const unsigned int sharedPointerId = 2; +}; + +/** + @internal + Payload type trait specialization for std::shared_ptr + for documentation of the various members, see above +*/ +template +struct PayloadTrait> +{ + typedef T ElementType; + static int elementMetaTypeId() + { + return qMetaTypeId(); + } + typedef typename Akonadi::SuperClass::Type SuperElementType; + typedef std::shared_ptr Type; + typedef std::shared_ptr SuperType; + static const bool isPolymorphic = !std::is_same::value; + static inline bool isNull(const Type &p) + { + return p.get() == 0; + } + template + static inline Type castFrom(const std::shared_ptr &p) + { + const Type sp = std::dynamic_pointer_cast(p); + if (sp.get() != 0 || p.get() == 0) { + return sp; + } + throw PayloadException("std::dynamic_pointer_cast failed"); + } + template + static inline bool canCastFrom(const std::shared_ptr &p) + { + const Type sp = std::dynamic_pointer_cast(p); + return sp.get() != 0 || p.get() == 0; + } + template + static inline std::shared_ptr castTo(const Type &p) + { + const std::shared_ptr sp = std::dynamic_pointer_cast(p); + return sp; + } + static std::shared_ptr clone(const boost::shared_ptr &t) + { + if (T *nt = clone_traits::clone(t.get())) { + return std::shared_ptr(nt); + } else { + return std::shared_ptr(); + } + } + static std::shared_ptr clone(const QSharedPointer &t) + { + if (T *nt = clone_traits::clone(t.data())) { + return std::shared_ptr(nt); + } else { + return std::shared_ptr(); + } + } + static const unsigned int sharedPointerId = 3; +}; + + +/** + * @internal + * Non-template base class for the payload container. + */ +struct PayloadBase +{ + virtual ~PayloadBase() + { + } + virtual PayloadBase *clone() const = 0; + virtual const char *typeName() const = 0; +}; + +/** + * @internal + * Container for the actual payload object. + */ +template +struct Payload : public PayloadBase +{ + Payload() + { + } + Payload(const T &p) + : payload(p) + { + } + + PayloadBase *clone() const Q_DECL_OVERRIDE + { + return new Payload(const_cast *>(this)->payload); + } + + const char *typeName() const Q_DECL_OVERRIDE + { + return typeid(const_cast *>(this)).name(); + } + + T payload; +}; + +/** + * @internal + * abstract, will therefore always fail to compile for pointer payloads + */ +template +struct Payload : public PayloadBase +{ +}; + +/** + @internal + Basically a dynamic_cast that also works across DSO boundaries. +*/ +template inline Payload *payload_cast(PayloadBase *payloadBase) +{ + Payload *p = dynamic_cast *>(payloadBase); + // try harder to cast, workaround for some gcc issue with template instances in multiple DSO's + if (!p && payloadBase && strcmp(payloadBase->typeName(), typeid(p).name()) == 0) { + p = static_cast*>(payloadBase); + } + return p; +} + +} // namespace Internal + +} // namespace Akonadi + +//@endcond + +#endif diff --git a/src/core/itemserializer.cpp b/src/core/itemserializer.cpp new file mode 100644 index 0000000..2ef5192 --- /dev/null +++ b/src/core/itemserializer.cpp @@ -0,0 +1,233 @@ +/* + 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 "typepluginloader_p.h" +#include "protocolhelper_p.h" + +#include "private/externalpartstorage_p.h" + +#include "akonadicore_debug.h" + +// Qt +#include +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(std::string) + +namespace Akonadi +{ + +DefaultItemSerializerPlugin::DefaultItemSerializerPlugin() +{ + Item::addToLegacyMapping(QStringLiteral("application/octet-stream")); +} + +bool DefaultItemSerializerPlugin::deserialize(Item &item, const QByteArray &label, QIODevice &data, int) +{ + if (label != Item::FullPayload) { + return false; + } + + item.setPayload(data.readAll()); + return true; +} + +void DefaultItemSerializerPlugin::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) +{ + Q_UNUSED(version) + Q_ASSERT(label == Item::FullPayload); + Q_UNUSED(label); + data.write(item.payload()); +} + +bool StdStringItemSerializerPlugin::deserialize(Item &item, const QByteArray &label, QIODevice &data, int) +{ + if (label != Item::FullPayload) { + return false; + } + std::string str; + { + const QByteArray ba = data.readAll(); + str.assign(ba.data(), ba.size()); + } + item.setPayload(str); + return true; +} + +void StdStringItemSerializerPlugin::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) +{ + Q_UNUSED(version) + Q_ASSERT(label == Item::FullPayload); + Q_UNUSED(label); + const std::string str = item.payload(); + data.write(QByteArray::fromRawData(str.data(), str.size())); +} + +/*static*/ +void ItemSerializer::deserialize(Item &item, const QByteArray &label, const QByteArray &data, int version, bool external) +{ + if (external) { + const QString fileName = ExternalPartStorage::resolveAbsolutePath(data); + QFile file(fileName); + if (file.open(QIODevice::ReadOnly)) { + deserialize(item, label, file, version); + file.close(); + } else { + qCWarning(AKONADICORE_LOG) << "Failed to open external payload:" << fileName << file.errorString(); + } + } 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 (!TypePluginLoader::defaultPluginForMimeType(item.mimeType())->deserialize(item, label, data, version)) { + qCWarning(AKONADICORE_LOG) << "Unable to deserialize payload part:" << label; + data.seek(0); + qCWarning(AKONADICORE_LOG) << "Payload data was: " << data.readAll(); + } +} + +/*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 = TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds()); + plugin->serialize(item, label, data, version); +} + +void ItemSerializer::apply(Item &item, const Item &other) +{ + if (!other.hasPayload()) { + return; + } + + ItemSerializerPlugin *plugin = TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds()); + + ItemSerializerPluginV2 *pluginV2 = dynamic_cast(plugin); + if (pluginV2) { + pluginV2->apply(item, other); + return; + } + + // Old-school merge: + Q_FOREACH (const QByteArray &part, other.loadedPayloadParts()) { + QByteArray partData; + QBuffer buffer; + buffer.setBuffer(&partData); + buffer.open(QIODevice::ReadWrite); + buffer.seek(0); + int version; + serialize(other, part, buffer, version); + buffer.seek(0); + deserialize(item, part, buffer, version); + } +} + +QSet ItemSerializer::parts(const Item &item) +{ + if (!item.hasPayload()) { + return QSet(); + } + return TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds())->parts(item); +} + +QSet ItemSerializer::availableParts(const Item &item) +{ + if (!item.hasPayload()) { + return QSet(); + } + ItemSerializerPlugin *plugin = TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds()); + ItemSerializerPluginV2 *pluginV2 = dynamic_cast(plugin); + + if (pluginV2) { + return pluginV2->availableParts(item); + } + + if (item.hasPayload()) { + return QSet(); + } + + return QSet() << Item::FullPayload; +} + +Item ItemSerializer::convert(const Item &item, int mtid) +{ +// qCDebug(AKONADICORE_LOG) << "asked to convert a" << item.mimeType() << "item to format" << ( mtid ? QMetaType::typeName( mtid ) : "" ); + if (!item.hasPayload()) { + qCDebug(AKONADICORE_LOG) << " -> but item has no payload!"; + return Item(); + } + + if (ItemSerializerPlugin *const plugin = TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), QVector(1, mtid), TypePluginLoader::NoDefault)) { + qCDebug(AKONADICORE_LOG) << " -> found a plugin that feels responsible, trying serialising the payload"; + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + int version; + serialize(item, Item::FullPayload, buffer, version); + buffer.seek(0); + qCDebug(AKONADICORE_LOG) << " -> serialized payload into" << buffer.size() << "bytes" << endl + << " -> going to deserialize"; + Item newItem; + if (plugin->deserialize(newItem, Item::FullPayload, buffer, version)) { + qCDebug(AKONADICORE_LOG) << " -> conversion successful"; + return newItem; + } else { + qCDebug(AKONADICORE_LOG) << " -> conversion FAILED"; + } + } else { +// qCDebug(AKONADICORE_LOG) << " -> found NO plugin that feels responsible"; + } + return Item(); +} + +void ItemSerializer::overridePluginLookup(QObject *p) +{ + TypePluginLoader::overridePluginLookup(p); +} + +} diff --git a/src/core/itemserializer_p.h b/src/core/itemserializer_p.h new file mode 100644 index 0000000..37065f0 --- /dev/null +++ b/src/core/itemserializer_p.h @@ -0,0 +1,130 @@ +/* + 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 "akonaditests_export.h" + +#include "itemserializerplugin.h" + +#include + +class QIODevice; + +namespace Akonadi +{ + +class Item; + +/** + @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); + + /** + * Throws ItemSerializerException on failure. + * @param item the item to apply to + * @param other the item to get values from + * @since 4.4 + */ + static void apply(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. + * @param item the item for which to list payload parts + * @since 4.4 + */ + static QSet availableParts(const Item &item); + + /** + * Tries to convert the payload in \a item into type with + * metatype-id \a metaTypeId. + * Throws ItemSerializerException or returns an Item w/o payload on failure. + * @param item the item to convert + * @param metaTypeID the meta type id used to convert items payload + * @since 4.6 + */ + static Item convert(const Item &item, int metaTypeId); + + /** + * Override the plugin-lookup with @p plugin. + * + * After calling this each lookup will always return @p plugin. + * This is useful to inject a special plugin for testing purposes. + * To reset the plugin, set to 0. + * + * @since 4.12 + */ + static void overridePluginLookup(QObject *plugin); +}; + +/** + @internal + Default implementation for serializer plugin. +*/ +class DefaultItemSerializerPlugin : public QObject, public ItemSerializerPlugin +{ + Q_OBJECT + Q_INTERFACES(Akonadi::ItemSerializerPlugin) +public: + DefaultItemSerializerPlugin(); + + bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) Q_DECL_OVERRIDE; + void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) Q_DECL_OVERRIDE; +}; + +/** + @internal + Serializer plugin implementation for std::string +*/ +class StdStringItemSerializerPlugin : public QObject, public ItemSerializerPlugin +{ + Q_OBJECT + Q_INTERFACES(Akonadi::ItemSerializerPlugin) +public: + StdStringItemSerializerPlugin(); + + bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) Q_DECL_OVERRIDE; + void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) Q_DECL_OVERRIDE; +}; + +} + +#endif diff --git a/src/core/itemserializerplugin.cpp b/src/core/itemserializerplugin.cpp new file mode 100644 index 0000000..d371b00 --- /dev/null +++ b/src/core/itemserializerplugin.cpp @@ -0,0 +1,74 @@ +/* + 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 "itemserializer_p.h" + +#include + +using namespace Akonadi; + +ItemSerializerPlugin::~ItemSerializerPlugin() +{ +} + +QSet ItemSerializerPlugin::parts(const Item &item) const +{ + QSet set; + if (item.hasPayload()) { + set.insert(Item::FullPayload); + } + + return set; +} + +void ItemSerializerPlugin::overridePluginLookup(QObject *p) +{ + ItemSerializer::overridePluginLookup(p); +} + +ItemSerializerPluginV2::~ItemSerializerPluginV2() +{ +} + +QSet ItemSerializerPluginV2::availableParts(const Item &item) const +{ + if (item.hasPayload()) { + return QSet(); + } + + return QSet() << Item::FullPayload; +} + +void ItemSerializerPluginV2::apply(Item &item, const Item &other) +{ + QBuffer buffer; + QByteArray data(other.payloadData()); + buffer.setBuffer(&data); + buffer.open(QIODevice::ReadOnly); + + foreach (const QByteArray &part, other.loadedPayloadParts()) { + buffer.seek(0); + deserialize(item, part, buffer, 0); + } + + buffer.close(); +} diff --git a/src/core/itemserializerplugin.h b/src/core/itemserializerplugin.h new file mode 100644 index 0000000..ab82915 --- /dev/null +++ b/src/core/itemserializerplugin.h @@ -0,0 +1,231 @@ +/* + 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 "akonadicore_export.h" + +class QIODevice; + +namespace Akonadi +{ + +/** + * @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 AKONADICORE_EXPORT ItemSerializerPlugin +{ +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; + + /** + * Override the plugin-lookup with @p plugin. + * + * After calling this each lookup will always return @p plugin. + * This is useful to inject a special plugin for testing purposes. + * To reset the plugin, set to 0. + * + * @since 4.12 + */ + static void overridePluginLookup(QObject *plugin); + +}; + +/** + * @short The extended base class for item type serializer plugins. + * + * @since 4.4 + */ +class AKONADICORE_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. + * @param item receives merged parts from @p other + * @param other the paylod parts to merge into @p item + * @since 4.4 + */ + virtual void apply(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. + * @param item the item for which to list payload parts + * @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/src/core/itemsync.cpp b/src/core/itemsync.cpp new file mode 100644 index 0000000..15fcda8 --- /dev/null +++ b/src/core/itemsync.cpp @@ -0,0 +1,556 @@ +/* + Copyright (c) 2007 Tobias Koenig + Copyright (c) 2007 Volker Krause + Copyright (c) 2014 Christian Mollekopf + + 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 "itemsync.h" + +#include "job_p.h" +#include "collection.h" +#include "item.h" +#include "item_p.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemmodifyjob.h" +#include "transactionsequence.h" +#include "itemfetchscope.h" + +#include "akonadicore_debug.h" + +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Akonadi::ItemSyncPrivate : public JobPrivate +{ +public: + ItemSyncPrivate(ItemSync *parent) + : JobPrivate(parent) + , mTransactionMode(ItemSync::SingleTransaction) + , mCurrentTransaction(0) + , mTransactionJobs(0) + , mPendingJobs(0) + , mProgress(0) + , mTotalItems(-1) + , mTotalItemsProcessed(0) + , mStreaming(false) + , mIncremental(false) + , mDeliveryDone(false) + , mFinished(false) + , mFullListingDone(false) + , mProcessingBatch(false) + , mDisableAutomaticDeliveryDone(false) + , mBatchSize(10) + , mMergeMode(Akonadi::ItemSync::RIDMerge) + { + // we want to fetch all data by default + mFetchScope.fetchFullPayload(); + mFetchScope.fetchAllAttributes(); + } + + void createOrMerge(const Item &item); + void checkDone(); + void slotItemsReceived(const Item::List &items); + void slotLocalListDone(KJob *job); + void slotLocalDeleteDone(KJob *job); + void slotLocalChangeDone(KJob *job); + void execute(); + void processItems(); + void processBatch(); + void deleteItems(const Item::List &items); + void slotTransactionResult(KJob *job); + void requestTransaction(); + Job *subjobParent() const; + void fetchLocalItemsToDelete(); + QString jobDebuggingString() const Q_DECL_OVERRIDE; + bool allProcessed() const; + + Q_DECLARE_PUBLIC(ItemSync) + Collection mSyncCollection; + QSet mListedItems; + + ItemSync::TransactionMode mTransactionMode; + TransactionSequence *mCurrentTransaction; + int mTransactionJobs; + + // fetch scope for initial item listing + ItemFetchScope mFetchScope; + + Akonadi::Item::List mRemoteItemQueue; + Akonadi::Item::List mRemovedRemoteItemQueue; + Akonadi::Item::List mCurrentBatchRemoteItems; + Akonadi::Item::List mCurrentBatchRemovedRemoteItems; + Akonadi::Item::List mItemsToDelete; + + // create counter + int mPendingJobs; + int mProgress; + int mTotalItems; + int mTotalItemsProcessed; + + bool mStreaming; + bool mIncremental; + bool mDeliveryDone; + bool mFinished; + bool mFullListingDone; + bool mProcessingBatch; + bool mDisableAutomaticDeliveryDone; + + int mBatchSize; + Akonadi::ItemSync::MergeMode mMergeMode; +}; + +void ItemSyncPrivate::createOrMerge(const Item &item) +{ + Q_Q(ItemSync); + // don't try to do anything in error state + if (q->error()) { + return; + } + mPendingJobs++; + ItemCreateJob *create = new ItemCreateJob(item, mSyncCollection, subjobParent()); + ItemCreateJob::MergeOptions merge = ItemCreateJob::Silent; + if (mMergeMode == ItemSync::GIDMerge && !item.gid().isEmpty()) { + merge |= ItemCreateJob::GID; + } else { + merge |= ItemCreateJob::RID; + } + create->setMerge(merge); + q->connect(create, SIGNAL(result(KJob*)), q, SLOT(slotLocalChangeDone(KJob*))); +} + +bool ItemSyncPrivate::allProcessed() const +{ + return mDeliveryDone && mCurrentBatchRemoteItems.isEmpty() && mRemoteItemQueue.isEmpty() && mRemovedRemoteItemQueue.isEmpty() && mCurrentBatchRemovedRemoteItems.isEmpty(); +} + +void ItemSyncPrivate::checkDone() +{ + Q_Q(ItemSync); + q->setProcessedAmount(KJob::Bytes, mProgress); + if (mPendingJobs > 0) { + return; + } + + if (mTransactionJobs > 0) { + //Commit the current transaction if we're in batch processing mode or done + //and wait until the transaction is committed to process the next batch + if (mTransactionMode == ItemSync::MultipleTransactions || (mDeliveryDone && mRemoteItemQueue.isEmpty())) { + if (mCurrentTransaction) { + q->emit transactionCommitted(); + mCurrentTransaction->commit(); + mCurrentTransaction = 0; + } + return; + } + } + mProcessingBatch = false; + if (!mRemoteItemQueue.isEmpty()) { + execute(); + //We don't have enough items, request more + if (!mProcessingBatch) { + q->emit readyForNextBatch(mBatchSize - mRemoteItemQueue.size()); + } + return; + } + q->emit readyForNextBatch(mBatchSize); + + if (allProcessed() && !mFinished) { + // prevent double result emission, can happen since checkDone() is called from all over the place + qCDebug(AKONADICORE_LOG) << "finished"; + mFinished = true; + q->emitResult(); + } +} + +ItemSync::ItemSync(const Collection &collection, QObject *parent) + : Job(new ItemSyncPrivate(this), parent) +{ + Q_D(ItemSync); + d->mSyncCollection = collection; +} + +ItemSync::~ItemSync() +{ +} + +void ItemSync::setFullSyncItems(const Item::List &items) +{ + /* + * We received a list of items from the server: + * * fetch all local id's + rid's only + * * check each full sync item whether it's locally available + * * if it is modify the item + * * if it's not create it + * * delete all superfluous items + */ + Q_D(ItemSync); + Q_ASSERT(!d->mIncremental); + if (!d->mStreaming) { + d->mDeliveryDone = true; + } + d->mRemoteItemQueue += items; + d->mTotalItemsProcessed += items.count(); + qCDebug(AKONADICORE_LOG) << "Received: " << items.count() + << "In total: " << d->mTotalItemsProcessed + << " Wanted: " << d->mTotalItems; + if (!d->mDisableAutomaticDeliveryDone && (d->mTotalItemsProcessed == d->mTotalItems)) { + d->mDeliveryDone = true; + } + d->execute(); +} + +void ItemSync::setTotalItems(int amount) +{ + Q_D(ItemSync); + Q_ASSERT(!d->mIncremental); + Q_ASSERT(amount >= 0); + setStreamingEnabled(true); + qCDebug(AKONADICORE_LOG) << amount; + d->mTotalItems = amount; + setTotalAmount(KJob::Bytes, amount); + if (!d->mDisableAutomaticDeliveryDone && (d->mTotalItems == 0)) { + d->mDeliveryDone = true; + d->execute(); + } +} + +void ItemSync::setDisableAutomaticDeliveryDone(bool disable) +{ + Q_D(ItemSync); + d->mDisableAutomaticDeliveryDone = disable; +} + +void ItemSync::setIncrementalSyncItems(const Item::List &changedItems, const Item::List &removedItems) +{ + /* + * We received an incremental listing of items: + * * for each changed item: + * ** If locally available => modify + * ** else => create + * * removed items can be removed right away + */ + Q_D(ItemSync); + d->mIncremental = true; + if (!d->mStreaming) { + d->mDeliveryDone = true; + } + d->mRemoteItemQueue += changedItems; + d->mRemovedRemoteItemQueue += removedItems; + d->mTotalItemsProcessed += changedItems.count() + removedItems.count(); + qCDebug(AKONADICORE_LOG) << "Received: " << changedItems.count() << "Removed: " << removedItems.count() << "In total: " << d->mTotalItemsProcessed << " Wanted: " << d->mTotalItems; + if (!d->mDisableAutomaticDeliveryDone && (d->mTotalItemsProcessed == d->mTotalItems)) { + d->mDeliveryDone = true; + } + d->execute(); +} + +void ItemSync::setFetchScope(ItemFetchScope &fetchScope) +{ + Q_D(ItemSync); + d->mFetchScope = fetchScope; +} + +ItemFetchScope &ItemSync::fetchScope() +{ + Q_D(ItemSync); + return d->mFetchScope; +} + +void ItemSync::doStart() +{ +} + +void ItemSyncPrivate::fetchLocalItemsToDelete() +{ + Q_Q(ItemSync); + if (mIncremental) { + qFatal("This must not be called while in incremental mode"); + return; + } + ItemFetchJob *job = new ItemFetchJob(mSyncCollection, subjobParent()); + job->fetchScope().setFetchRemoteIdentification(true); + job->fetchScope().setFetchModificationTime(false); + job->setDeliveryOption(ItemFetchJob::EmitItemsIndividually); + // we only can fetch parts already in the cache, otherwise this will deadlock + job->fetchScope().setCacheOnly(true); + + QObject::connect(job, SIGNAL(itemsReceived(Akonadi::Item::List)), q, SLOT(slotItemsReceived(Akonadi::Item::List))); + QObject::connect(job, SIGNAL(result(KJob*)), q, SLOT(slotLocalListDone(KJob*))); + mPendingJobs++; +} + +void ItemSyncPrivate::slotItemsReceived(const Item::List &items) +{ + foreach (const Akonadi::Item &item, items) { + //Don't delete items that have not yet been synchronized + if (item.remoteId().isEmpty()) { + continue; + } + if (!mListedItems.contains(item.remoteId())) { + mItemsToDelete << Item(item.id()); + } + } +} + +void ItemSyncPrivate::slotLocalListDone(KJob *job) +{ + mPendingJobs--; + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + } + deleteItems(mItemsToDelete); + checkDone(); +} + +QString ItemSyncPrivate::jobDebuggingString() const /*Q_DECL_OVERRIDE*/ +{ + // TODO: also print out mIncremental and mTotalItemsProcessed, but they are set after the job + // started, so this requires passing jobDebuggingString to jobEnded(). + return QStringLiteral("Collection %1 (%2)").arg(mSyncCollection.id()).arg(mSyncCollection.name()); +} + +void ItemSyncPrivate::execute() +{ + Q_Q(ItemSync); + //shouldn't happen + if (mFinished) { + qCWarning(AKONADICORE_LOG) << "Call to execute() on finished job."; + Q_ASSERT(false); + return; + } + //not doing anything, start processing + if (!mProcessingBatch) { + if (mRemoteItemQueue.size() >= mBatchSize || mDeliveryDone) { + //we have a new batch to process + const int num = qMin(mBatchSize, mRemoteItemQueue.size()); + for (int i = 0; i < num; i++) { + mCurrentBatchRemoteItems << mRemoteItemQueue.takeFirst(); + } + mCurrentBatchRemovedRemoteItems += mRemovedRemoteItemQueue; + mRemovedRemoteItemQueue.clear(); + } else { + //nothing to do, let's wait for more data + return; + } + mProcessingBatch = true; + processBatch(); + return; + } + checkDone(); +} + +//process the current batch of items +void ItemSyncPrivate::processBatch() +{ + Q_Q(ItemSync); + if (mCurrentBatchRemoteItems.isEmpty() && !mDeliveryDone) { + return; + } + + //request a transaction, there are items that require processing + requestTransaction(); + + processItems(); + + // removed + if (!mIncremental && allProcessed()) { + //the full listing is done and we know which items to remove + fetchLocalItemsToDelete(); + } else { + deleteItems(mCurrentBatchRemovedRemoteItems); + mCurrentBatchRemovedRemoteItems.clear(); + } + + checkDone(); +} + +void ItemSyncPrivate::processItems() +{ + Q_Q(ItemSync); + // added / updated + foreach (const Item &remoteItem, mCurrentBatchRemoteItems) { + if (remoteItem.remoteId().isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Item " << remoteItem.id() << " does not have a remote identifier"; + continue; + } + if (!mIncremental) { + mListedItems << remoteItem.remoteId(); + } + createOrMerge(remoteItem); + } + mCurrentBatchRemoteItems.clear(); +} + +void ItemSyncPrivate::deleteItems(const Item::List &itemsToDelete) +{ + Q_Q(ItemSync); + // if in error state, better not change anything anymore + if (q->error()) { + return; + } + + if (itemsToDelete.isEmpty()) { + return; + } + + mPendingJobs++; + ItemDeleteJob *job = new ItemDeleteJob(itemsToDelete, subjobParent()); + q->connect(job, SIGNAL(result(KJob*)), q, SLOT(slotLocalDeleteDone(KJob*))); + + // It can happen that the groupware servers report us deleted items + // twice, in this case this item delete job will fail on the second try. + // To avoid a rollback of the complete transaction we gracefully allow the job + // to fail :) + TransactionSequence *transaction = qobject_cast(subjobParent()); + if (transaction) { + transaction->setIgnoreJobFailure(job); + } +} + +void ItemSyncPrivate::slotLocalDeleteDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Deleting items from the akonadi database failed:" << job->errorString(); + } + mPendingJobs--; + mProgress++; + + checkDone(); +} + +void ItemSyncPrivate::slotLocalChangeDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Creating/updating items from the akonadi database failed:" << job->errorString(); + } + mPendingJobs--; + mProgress++; + + checkDone(); +} + +void ItemSyncPrivate::slotTransactionResult(KJob *job) +{ + --mTransactionJobs; + if (mCurrentTransaction == job) { + mCurrentTransaction = 0; + } + + checkDone(); +} + +void ItemSyncPrivate::requestTransaction() +{ + Q_Q(ItemSync); + //we never want parallel transactions, single transaction just makes one big transaction, and multi transaction uses multiple transaction sequentially + if (!mCurrentTransaction) { + ++mTransactionJobs; + mCurrentTransaction = new TransactionSequence(q); + mCurrentTransaction->setAutomaticCommittingEnabled(false); + QObject::connect(mCurrentTransaction, SIGNAL(result(KJob*)), q, SLOT(slotTransactionResult(KJob*))); + } +} + +Job *ItemSyncPrivate::subjobParent() const +{ + Q_Q(const ItemSync); + if (mCurrentTransaction && mTransactionMode != ItemSync::NoTransaction) { + return mCurrentTransaction; + } + return const_cast(q); +} + +void ItemSync::setStreamingEnabled(bool enable) +{ + Q_D(ItemSync); + d->mStreaming = enable; +} + +void ItemSync::deliveryDone() +{ + Q_D(ItemSync); + Q_ASSERT(d->mStreaming); + d->mDeliveryDone = true; + d->execute(); +} + +void ItemSync::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error during ItemSync: " << job->errorString(); + // pretent there were no errors + Akonadi::Job::removeSubjob(job); + // propagate the first error we got but continue, we might still be fed with stuff from a resource + if (!error()) { + setError(job->error()); + setErrorText(job->errorText()); + } + } else { + Akonadi::Job::slotResult(job); + } +} + +void ItemSync::rollback() +{ + Q_D(ItemSync); + qCWarning(AKONADICORE_LOG) << "The item sync is being rolled-back."; + setError(UserCanceled); + if (d->mCurrentTransaction) { + d->mCurrentTransaction->rollback(); + } + d->mDeliveryDone = true; // user wont deliver more data + d->execute(); // end this in an ordered way, since we have an error set no real change will be done +} + +void ItemSync::setTransactionMode(ItemSync::TransactionMode mode) +{ + Q_D(ItemSync); + d->mTransactionMode = mode; +} + +int ItemSync::batchSize() const +{ + Q_D(const ItemSync); + return d->mBatchSize; +} + +void ItemSync::setBatchSize(int size) +{ + Q_D(ItemSync); + d->mBatchSize = size; +} + +ItemSync::MergeMode ItemSync::mergeMode() const +{ + Q_D(const ItemSync); + return d->mMergeMode; +} + +void ItemSync::setMergeMode(MergeMode mergeMode) +{ + Q_D(ItemSync); + d->mMergeMode = mergeMode; +} + +#include "moc_itemsync.cpp" diff --git a/src/core/itemsync.h b/src/core/itemsync.h new file mode 100644 index 0000000..cdba1cf --- /dev/null +++ b/src/core/itemsync.h @@ -0,0 +1,274 @@ +/* + Copyright (c) 2007 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_ITEMSYNC_H +#define AKONADI_ITEMSYNC_H + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class ItemFetchScope; +class ItemSyncPrivate; + +/** + * @short Syncs between items known to a client (usually a resource) and the Akonadi storage. + * + * Remote Id must only be set by the resource storing the item, other clients + * should leave it empty, since the resource responsible for the target collection + * will be notified about the addition and then create a suitable remote Id. + * + * There are two different forms of ItemSync usage: + * - Full-Sync: meaning the client provides all valid items, i.e. any item not + * part of the list but currently stored in Akonadi will be removed + * - Incremental-Sync: meaning the client provides two lists, one for items which + * are new or modified and one for items which should be removed. Any item not + * part of either list but currently stored in Akonadi will not be changed. + * + * @note This is provided for convenience to implement "save all" like behavior, + * however it is strongly recommended to use single item jobs whenever + * possible, e.g. ItemCreateJob, ItemModifyJob and ItemDeleteJob + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT ItemSync : public Job +{ + Q_OBJECT + +public: + enum MergeMode + { + RIDMerge, + GIDMerge + }; + + /** + * Creates a new item synchronizer. + * + * @param collection The collection we are syncing. + * @param parent The parent object. + */ + explicit ItemSync(const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Destroys the item synchronizer. + */ + ~ItemSync(); + + /** + * Sets the full item list for the collection. + * + * Usually the result of a full item listing. + * + * @warning If the client using this is a resource, all items must have + * a valid remote identifier. + * + * @param items A list of items. + */ + void setFullSyncItems(const Item::List &items); + + /** + * Set the amount of items which you are going to return in total + * by using the setFullSyncItems()/setIncrementalSyncItems() methods. + * + * @warning By default the item sync will automatically end once + * sufficient items have been provided. + * To disable this use setDisableAutomaticDeliveryDone + * + * @see setDisableAutomaticDeliveryDone + * @param amount The amount of items in total. + */ + void setTotalItems(int amount); + + /** + Enable item streaming. Item streaming means that the items delivered by setXItems() calls + are delivered in chunks and you manually indicate when all items have been delivered + by calling deliveryDone(). + @param enable @c true to enable item streaming + */ + void setStreamingEnabled(bool enable); + + /** + Notify ItemSync that all remote items have been delivered. + Only call this in streaming mode. + */ + void deliveryDone(); + + /** + * Sets the item lists for incrementally syncing the collection. + * + * Usually the result of an incremental remote item listing. + * + * @warning If the client using this is a resource, all items must have + * a valid remote identifier. + * + * @param changedItems A list of items added or changed by the client. + * @param removedItems A list of items deleted by the client. + */ + void setIncrementalSyncItems(const Item::List &changedItems, + const Item::List &removedItems); + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + + /** + * Aborts the sync process and rolls back all not yet committed transactions. + * Use this if an external error occurred during the sync process (such as the + * user canceling it). + * @since 4.5 + */ + void rollback(); + + /** + * Transaction mode used by ItemSync. + * @since 4.6 + */ + enum TransactionMode { + SingleTransaction, ///< Use a single transaction for the entire sync process (default), provides maximum consistency ("all or nothing") and best performance + MultipleTransactions, ///< Use one transaction per chunk of delivered items, good compromise between the other two when using streaming + NoTransaction ///< Use no transaction at all, provides highest responsiveness (might therefore feel faster even when actually taking slightly longer), no consistency guaranteed (can fail anywhere in the sync process) + }; + + /** + * Set the transaction mode to use for this sync. + * @note You must call this method before starting the sync, changes afterwards lead to undefined results. + * @param mode the transaction mode to use + * @since 4.6 + */ + void setTransactionMode(TransactionMode mode); + + /** + * Minimum number of items required to start processing in streaming mode. + * When MultipleTransactions is used, one transaction per batch will be created. + * + * @see setBatchSize() + * @since 4.14 + */ + int batchSize() const; + + /** + * Set the batch size. + * + * The default is 10. + * + * @note You must call this method before starting the sync, changes afterwards lead to undefined results. + * @see batchSize() + * @since 4.14 + */ + void setBatchSize(int); + + /** + * Disables the automatic completion of the item sync, + * based on the number of delivered items. + * + * This ensures that the item sync only finishes once deliveryDone() + * is called, while still making it possible to use the progress + * reporting of the ItemSync. + * + * @note You must call this method before starting the sync, changes afterwards lead to undefined results. + * @see setTotalItems + * @since 4.14 + */ + void setDisableAutomaticDeliveryDone(bool disable); + + /** + * Returns current merge mode + * + * @see setMergeMode() + * @since 5.1 + */ + MergeMode mergeMode() const; + + /** + * Set what merge method should be used for next ItemSync run + * + * By default ItemSync uses RIDMerge method. + * + * See ItemCreateJob for details on Item merging. + * + * @note You must call this method before starting the sync, changes afterwards lead to undefined results. + * @see mergeMode + * @since 4.14.11 + */ + void setMergeMode(MergeMode mergeMode); + +Q_SIGNALS: + /** + * Signals the resource that new items can be delivered. + * @param remainingBatchSize the number of items required to complete the batch (typically the same as batchSize()) + * + * @since 4.14 + */ + void readyForNextBatch(int remainingBatchSize); + + /** + * @internal + * Emitted whenever a transaction is committed. This is for testing only. + * + * @since 4.14 + */ + void transactionCommitted(); + +protected: + void doStart() Q_DECL_OVERRIDE; + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(ItemSync) + + Q_PRIVATE_SLOT(d_func(), void slotLocalListDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotLocalDeleteDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotLocalChangeDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotTransactionResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotItemsReceived(const Akonadi::Item::List &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/agentinstancecreatejob.cpp b/src/core/jobs/agentinstancecreatejob.cpp new file mode 100644 index 0000000..effa21f --- /dev/null +++ b/src/core/jobs/agentinstancecreatejob.cpp @@ -0,0 +1,223 @@ +/* + 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 "agentinstancecreatejob.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "agentmanager_p.h" +#include "controlinterface.h" +#include "KDBusConnectionPool" +#include "kjobprivatebase_p.h" +#include "servermanager.h" + +#include +#include + +#include + +#ifdef Q_OS_UNIX +#include +#include +#endif + +using namespace Akonadi; + +static const int safetyTimeout = 10000; // ms + +namespace Akonadi +{ +/** + * @internal + */ +class AgentInstanceCreateJobPrivate : public KJobPrivateBase +{ +public: + AgentInstanceCreateJobPrivate(AgentInstanceCreateJob *parent) + : q(parent) + , parentWidget(0) + , safetyTimer(new QTimer(parent)) + , doConfig(false) + , tooLate(false) + { + QObject::connect(AgentManager::self(), SIGNAL(instanceAdded(Akonadi::AgentInstance)), + q, SLOT(agentInstanceAdded(Akonadi::AgentInstance))); + QObject::connect(safetyTimer, SIGNAL(timeout()), q, SLOT(timeout())); + } + + void agentInstanceAdded(const AgentInstance &instance) + { + if (agentInstance == instance && !tooLate) { + safetyTimer->stop(); + if (doConfig) { + // return from dbus call first before doing the next one + QTimer::singleShot(0, q, SLOT(doConfigure())); + } else { + q->emitResult(); + } + } + } + + void doConfigure() + { + org::freedesktop::Akonadi::Agent::Control *agentControlIface = + new org::freedesktop::Akonadi::Agent::Control(ServerManager::agentServiceName(ServerManager::Agent, agentInstance.identifier()), + QStringLiteral("/"), KDBusConnectionPool::threadConnection(), q); + if (!agentControlIface || !agentControlIface->isValid()) { + delete agentControlIface; + + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to access D-Bus interface of created agent.")); + q->emitResult(); + return; + } + + q->connect(agentControlIface, SIGNAL(configurationDialogAccepted()), + q, SLOT(configurationDialogAccepted())); + q->connect(agentControlIface, SIGNAL(configurationDialogRejected()), + q, SLOT(configurationDialogRejected())); + + agentInstance.configure(parentWidget); + } + + void configurationDialogAccepted() + { + // The user clicked 'Ok' in the initial configuration dialog, so we assume + // he wants to keep the resource and the job is done. + q->emitResult(); + } + + void configurationDialogRejected() + { + // The user clicked 'Cancel' in the initial configuration dialog, so we assume + // he wants to abort the 'create new resource' job and the new resource will be + // removed again. + AgentManager::self()->removeInstance(agentInstance); + + q->emitResult(); + } + + void timeout() + { + tooLate = true; + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Agent instance creation timed out.")); + q->emitResult(); + } + + void emitResult() + { + q->emitResult(); + } + + void doStart() Q_DECL_OVERRIDE; + + AgentInstanceCreateJob *q; + AgentType agentType; + QString agentTypeId; + AgentInstance agentInstance; + QWidget *parentWidget; + QTimer *safetyTimer; + bool doConfig; + bool tooLate; +}; + +} + +AgentInstanceCreateJob::AgentInstanceCreateJob(const AgentType &agentType, QObject *parent) + : KJob(parent) + , d(new AgentInstanceCreateJobPrivate(this)) +{ + d->agentType = agentType; +} + +AgentInstanceCreateJob::AgentInstanceCreateJob(const QString &typeId, QObject *parent) + : KJob(parent) + , d(new AgentInstanceCreateJobPrivate(this)) +{ + d->agentTypeId = typeId; +} + +AgentInstanceCreateJob::~ AgentInstanceCreateJob() +{ + delete d; +} + +void AgentInstanceCreateJob::configure(QWidget *parent) +{ + d->parentWidget = parent; + d->doConfig = true; +} + +AgentInstance AgentInstanceCreateJob::instance() const +{ + return d->agentInstance; +} + +void AgentInstanceCreateJob::start() +{ + d->start(); +} + +void AgentInstanceCreateJobPrivate::doStart() +{ + if (!agentType.isValid() && !agentTypeId.isEmpty()) { + agentType = AgentManager::self()->type(agentTypeId); + } + + if (!agentType.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to obtain agent type '%1'.", agentTypeId)); + QTimer::singleShot(0, q, SLOT(emitResult())); + return; + } + + agentInstance = AgentManager::self()->d->createInstance(agentType); + if (!agentInstance.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to create agent instance.")); + QTimer::singleShot(0, q, SLOT(emitResult())); + } else { + int timeout = safetyTimeout; +#ifdef Q_OS_UNIX + // Increate the timeout when valgrinding the agent, because that slows down things a log. + QString agentValgrind = QString::fromLocal8Bit(qgetenv("AKONADI_VALGRIND")); + if (!agentValgrind.isEmpty() && agentType.identifier().contains(agentValgrind)) { + timeout *= 15; + } + + // change the timeout when debugging the agent, because we need time to start the debugger + const QString agentDebugging = QString::fromLocal8Bit(qgetenv("AKONADI_DEBUG_WAIT")); + if (!agentDebugging.isEmpty()) { + // we are debugging + const QString agentDebuggingTimeout = QString::fromLocal8Bit(qgetenv("AKONADI_DEBUG_TIMEOUT")); + if (agentDebuggingTimeout.isEmpty()) { + // use default value of 150 seconds (the same as "valgrinding", this has to be checked) + timeout = 15 * safetyTimeout; + } else { + // use own value + timeout = agentDebuggingTimeout.toInt(); + } + } +#endif + safetyTimer->start(timeout); + } +} + +#include "moc_agentinstancecreatejob.cpp" diff --git a/src/core/jobs/agentinstancecreatejob.h b/src/core/jobs/agentinstancecreatejob.h new file mode 100644 index 0000000..8ef340c --- /dev/null +++ b/src/core/jobs/agentinstancecreatejob.h @@ -0,0 +1,131 @@ +/* + 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_AGENTINSTANCECREATEJOB_H +#define AKONADI_AGENTINSTANCECREATEJOB_H + +#include "akonadicore_export.h" +#include "agenttype.h" + +#include + +namespace Akonadi +{ + +class AgentInstance; +class AgentInstanceCreateJobPrivate; + +/** + * @short Job for creating new agent instances. + * + * This class encapsulates the procedure of creating a new agent instance + * and optionally configuring it immediately. + * + * @code + * + * MyClass::MyClass( QWidget *parent ) + * : QWidget( parent ) + * { + * // Get agent type object + * Akonadi::AgentType type = Akonadi::AgentManager::self()->type( "akonadi_vcard_resource" ); + * + * Akonadi::AgentInstanceCreateJob *job = new Akonadi::AgentInstanceCreateJob( type ); + * connect( job, SIGNAL(result(KJob*)), + * this, SLOT(slotCreated(KJob*)) ); + * + * // use this widget as parent for the config dialog + * job->configure( this ); + * + * job->start(); + * } + * + * ... + * + * void MyClass::slotCreated( KJob *job ) + * { + * Akonadi::AgentInstanceCreateJob *createJob = static_cast( job ); + * + * qDebug() << "Created agent instance:" << createJob->instance().identifier(); + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT AgentInstanceCreateJob : public KJob +{ + Q_OBJECT + +public: + /** + * Creates a new agent instance create job. + * + * @param type The type of the agent to create. + * @param parent The parent object. + */ + explicit AgentInstanceCreateJob(const AgentType &type, QObject *parent = Q_NULLPTR); + + /** + * Creates a new agent instance create job. + * + * @param typeId The identifier of type of the agent to create. + * @param parent The parent object. + * @since 4.5 + */ + explicit AgentInstanceCreateJob(const QString &typeId, QObject *parent = Q_NULLPTR); + + /** + * Destroys the agent instance create job. + */ + ~AgentInstanceCreateJob(); + + /** + * Setup the job to show agent configuration dialog once the agent instance + * has been successfully started. + * @param parent The parent window for the configuration dialog. + */ + void configure(QWidget *parent = Q_NULLPTR); + + /** + * Returns the AgentInstance object of the newly created agent instance. + */ + AgentInstance instance() const; + + /** + * Starts the instance creation. + */ + void start() Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + friend class Akonadi::AgentInstanceCreateJobPrivate; + AgentInstanceCreateJobPrivate *const d; + + Q_PRIVATE_SLOT(d, void agentInstanceAdded(const Akonadi::AgentInstance &)) + Q_PRIVATE_SLOT(d, void doConfigure()) + Q_PRIVATE_SLOT(d, void timeout()) + Q_PRIVATE_SLOT(d, void emitResult()) + Q_PRIVATE_SLOT(d, void configurationDialogAccepted()) + Q_PRIVATE_SLOT(d, void configurationDialogRejected()) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/collectionattributessynchronizationjob.cpp b/src/core/jobs/collectionattributessynchronizationjob.cpp new file mode 100644 index 0000000..e894db7 --- /dev/null +++ b/src/core/jobs/collectionattributessynchronizationjob.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2009 Volker Krause + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "collectionattributessynchronizationjob.h" +#include "KDBusConnectionPool" +#include "kjobprivatebase_p.h" +#include "servermanager.h" +#include "akonadicore_debug.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collection.h" + +#include + +#include +#include + +namespace Akonadi +{ + +class CollectionAttributesSynchronizationJobPrivate : public KJobPrivateBase +{ +public: + CollectionAttributesSynchronizationJobPrivate(CollectionAttributesSynchronizationJob *parent) + : q(parent) + , interface(0) + , safetyTimer(0) + , timeoutCount(0) + { + } + + void doStart() Q_DECL_OVERRIDE; + + CollectionAttributesSynchronizationJob *q; + AgentInstance instance; + Collection collection; + QDBusInterface *interface; + QTimer *safetyTimer; + int timeoutCount; + static const int timeoutCountLimit; + + void slotSynchronized(qlonglong); + void slotTimeout(); +}; + +const int CollectionAttributesSynchronizationJobPrivate::timeoutCountLimit = 2; + +CollectionAttributesSynchronizationJob::CollectionAttributesSynchronizationJob(const Collection &collection, QObject *parent) + : KJob(parent) + , d(new CollectionAttributesSynchronizationJobPrivate(this)) +{ + d->instance = AgentManager::self()->instance(collection.resource()); + d->collection = collection; + d->safetyTimer = new QTimer(this); + connect(d->safetyTimer, SIGNAL(timeout()), SLOT(slotTimeout())); + d->safetyTimer->setInterval(5 * 1000); + d->safetyTimer->setSingleShot(false); +} + +CollectionAttributesSynchronizationJob::~CollectionAttributesSynchronizationJob() +{ + delete d; +} + +void CollectionAttributesSynchronizationJob::start() +{ + d->start(); +} + +void CollectionAttributesSynchronizationJobPrivate::doStart() +{ + if (!collection.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Invalid collection instance.")); + q->emitResult(); + return; + } + + if (!instance.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Invalid resource instance.")); + q->emitResult(); + return; + } + + interface = new QDBusInterface(ServerManager::agentServiceName(ServerManager::Resource, instance.identifier()), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.Akonadi.Resource"), + KDBusConnectionPool::threadConnection(), this); + connect(interface, SIGNAL(attributesSynchronized(qlonglong)), q, SLOT(slotSynchronized(qlonglong))); + + if (interface->isValid()) { + const QDBusMessage reply = interface->call(QStringLiteral("synchronizeCollectionAttributes"), collection.id()); + if (reply.type() == QDBusMessage::ErrorMessage) { + // This means that the resource doesn't provide a synchronizeCollectionAttributes method, so we just finish the job + q->emitResult(); + return; + } + safetyTimer->start(); + } else { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to obtain D-Bus interface for resource '%1'", instance.identifier())); + q->emitResult(); + return; + } +} + +void CollectionAttributesSynchronizationJobPrivate::slotSynchronized(qlonglong id) +{ + if (id == collection.id()) { + q->disconnect(interface, SIGNAL(attributesSynchronized(qlonglong)), q, SLOT(slotSynchronized(qlonglong))); + safetyTimer->stop(); + q->emitResult(); + } +} + +void CollectionAttributesSynchronizationJobPrivate::slotTimeout() +{ + instance = AgentManager::self()->instance(instance.identifier()); + timeoutCount++; + + if (timeoutCount > timeoutCountLimit) { + safetyTimer->stop(); + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Collection attributes synchronization timed out.")); + q->emitResult(); + return; + } + + if (instance.status() == AgentInstance::Idle) { + // try again, we might have lost the synchronized() signal + qCDebug(AKONADICORE_LOG) << "collection attributes" << collection.id() << instance.identifier(); + interface->call(QStringLiteral("synchronizeCollectionAttributes"), collection.id()); + } +} + +} + +#include "moc_collectionattributessynchronizationjob.cpp" diff --git a/src/core/jobs/collectionattributessynchronizationjob.h b/src/core/jobs/collectionattributessynchronizationjob.h new file mode 100644 index 0000000..cd25fe9 --- /dev/null +++ b/src/core/jobs/collectionattributessynchronizationjob.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2009 Volker Krause + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#ifndef AKONADI_COLLECTIONATTRIBUTESSYNCHRONIZATIONJOB_H +#define AKONADI_COLLECTIONATTRIBUTESSYNCHRONIZATIONJOB_H + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ + +class Collection; +class CollectionAttributesSynchronizationJobPrivate; + +/** + * @short Job that synchronizes the attributes of a collection. + * + * This job will trigger a resource to synchronize the attributes of + * a collection based on what the backend is reporting to store them in the + * Akonadi storage. + * + * Example: + * + * @code + * using namespace Akonadi; + * + * const Collection collection = ...; + * + * CollectionAttributesSynchronizationJob *job = new CollectionAttributesSynchronizationJob( collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(synchronizationFinished(KJob*)) ); + * + * @endcode + * + * @note This is a KJob not an Akonadi::Job, so it wont auto-start! + * + * @author Volker Krause + * @since 4.6 + */ +class AKONADICORE_EXPORT CollectionAttributesSynchronizationJob : public KJob +{ + Q_OBJECT + +public: + /** + * Creates a new synchronization job for the given collection. + * + * @param collection The collection to synchronize. + */ + explicit CollectionAttributesSynchronizationJob(const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Destroys the synchronization job. + */ + ~CollectionAttributesSynchronizationJob(); + + /* reimpl */ + void start() Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + CollectionAttributesSynchronizationJobPrivate *const d; + friend class CollectionAttributesSynchronizationJobPrivate; + + Q_PRIVATE_SLOT(d, void slotSynchronized(qlonglong)) + Q_PRIVATE_SLOT(d, void slotTimeout()) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/collectioncopyjob.cpp b/src/core/jobs/collectioncopyjob.cpp new file mode 100644 index 0000000..3ba2ef0 --- /dev/null +++ b/src/core/jobs/collectioncopyjob.cpp @@ -0,0 +1,82 @@ +/* + 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 "collectioncopyjob.h" +#include "collection.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +#include + + +using namespace Akonadi; + +class Akonadi::CollectionCopyJobPrivate : public JobPrivate +{ +public: + CollectionCopyJobPrivate(CollectionCopyJob *parent) + : JobPrivate(parent) + { + } + + Collection mSource; + Collection mTarget; +}; + +CollectionCopyJob::CollectionCopyJob(const Collection &source, const Collection &target, QObject *parent) + : Job(new CollectionCopyJobPrivate(this), parent) +{ + Q_D(CollectionCopyJob); + + d->mSource = source; + d->mTarget = target; +} + +CollectionCopyJob::~CollectionCopyJob() +{ +} + +void CollectionCopyJob::doStart() +{ + Q_D(CollectionCopyJob); + + if (!d->mSource.isValid() && d->mSource.remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid collection to copy")); + emitResult(); + return; + } + if (!d->mTarget.isValid() && d->mTarget.remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid destination collection")); + emitResult(); + return; + } + d->sendCommand(Protocol::CopyCollectionCommand(d->mSource.id(), d->mTarget.id())); +} + +bool CollectionCopyJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::CopyCollection) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/collectioncopyjob.h b/src/core/jobs/collectioncopyjob.h new file mode 100644 index 0000000..82c9309 --- /dev/null +++ b/src/core/jobs/collectioncopyjob.h @@ -0,0 +1,88 @@ +/* + 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_COLLECTIONCOPYJOB_H +#define AKONADI_COLLECTIONCOPYJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class CollectionCopyJobPrivate; + +/** + * @short Job that copies a collection into another collection in the Akonadi storage. + * + * This job copies a single collection into a specified target collection. + * + * @code + * + * Akonadi::Collection source = ... + * Akonadi::Collection target = ... + * + * Akonadi::CollectionCopyJob *job = new Akonadi::CollectionCopyJob( source, target ); + * connect( job, SIGNAL(result(KJob*)), SLOT(copyFinished(KJob*)) ); + * + * ... + * + * MyClass::copyFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Copied successfully"; + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionCopyJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection copy job to copy the given @p source collection into @p target. + * + * @param source The collection to copy. + * @param target The target collection. + * @param parent The parent object. + */ + CollectionCopyJob(const Collection &source, const Collection &target, QObject *parent = Q_NULLPTR); + + /** + * Destroys the collection copy job. + */ + ~CollectionCopyJob(); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(CollectionCopyJob) +}; + +} + +#endif diff --git a/src/core/jobs/collectioncreatejob.cpp b/src/core/jobs/collectioncreatejob.cpp new file mode 100644 index 0000000..446c825 --- /dev/null +++ b/src/core/jobs/collectioncreatejob.cpp @@ -0,0 +1,121 @@ +/* + 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 "collectioncreatejob.h" +#include "protocolhelper_p.h" +#include "job_p.h" +#include "private/protocol_p.h" + +#include + +using namespace Akonadi; + +class Akonadi::CollectionCreateJobPrivate : public JobPrivate +{ +public: + CollectionCreateJobPrivate(CollectionCreateJob *parent) + : JobPrivate(parent) + { + } + + Collection mCollection; +}; + +CollectionCreateJob::CollectionCreateJob(const Collection &collection, QObject *parent) + : Job(new CollectionCreateJobPrivate(this), parent) +{ + Q_D(CollectionCreateJob); + + d->mCollection = collection; +} + +CollectionCreateJob::~CollectionCreateJob() +{ +} + +void CollectionCreateJob::doStart() +{ + Q_D(CollectionCreateJob); + if (d->mCollection.parentCollection().id() < 0 && d->mCollection.parentCollection().remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid parent")); + emitResult(); + return; + } + + Protocol::CreateCollectionCommand cmd; + cmd.setName(d->mCollection.name()); + cmd.setParent(ProtocolHelper::entityToScope(d->mCollection.parentCollection())); + cmd.setMimeTypes(d->mCollection.contentMimeTypes()); + cmd.setRemoteId(d->mCollection.remoteId()); + cmd.setRemoteRevision(d->mCollection.remoteRevision()); + cmd.setIsVirtual(d->mCollection.isVirtual()); + cmd.setEnabled(d->mCollection.enabled()); + cmd.setDisplayPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListDisplay))); + cmd.setSyncPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListDisplay))); + cmd.setIndexPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListIndex))); + cmd.setCachePolicy(ProtocolHelper::cachePolicyToProtocol(d->mCollection.cachePolicy())); + Protocol::Attributes attrs; + Q_FOREACH (Attribute *attr, d->mCollection.attributes()) { + attrs.insert(attr->type(), attr->serialized()); + } + cmd.setAttributes(attrs); + + d->sendCommand(cmd); + emitWriteFinished(); +} + +Collection CollectionCreateJob::collection() const +{ + Q_D(const CollectionCreateJob); + + return d->mCollection; +} + +bool CollectionCreateJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(CollectionCreateJob); + + if (!response.isResponse()) { + return Job::doHandleResponse(tag, response); + } + + if (response.type() == Protocol::Command::FetchCollections) { + Protocol::FetchCollectionsResponse resp(response); + Collection col = ProtocolHelper::parseCollection(resp); + if (!col.isValid()) { + setError(Unknown); + setErrorText(i18n("Failed to parse Collection from response")); + return true; + } + col.setParentCollection(d->mCollection.parentCollection()); + col.setName(d->mCollection.name()); + col.setRemoteId(d->mCollection.remoteId()); + col.setRemoteRevision(d->mCollection.remoteRevision()); + col.setVirtual(d->mCollection.isVirtual()); + d->mCollection = col; + return false; + } + + if (response.type() == Protocol::Command::CreateCollection) { + return true; + } + + return Job::doHandleResponse(tag, response); +} diff --git a/src/core/jobs/collectioncreatejob.h b/src/core/jobs/collectioncreatejob.h new file mode 100644 index 0000000..ca0e289 --- /dev/null +++ b/src/core/jobs/collectioncreatejob.h @@ -0,0 +1,89 @@ +/* + 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. +*/ + +#ifndef AKONADI_COLLECTIONCREATEJOB_H +#define AKONADI_COLLECTIONCREATEJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class CollectionCreateJobPrivate; + +/** + * @short Job that creates a new collection in the Akonadi storage. + * + * This job creates a new collection with all the set properties. + * You have to use setParentCollection() to define the collection the + * new collection shall be located in. + * + * @code + * + * // create a new top-level collection + * Akonadi::Collection collection; + * collection.setParentCollection( Collection::root() ); + * collection.setName( "Events" ); + * collection.setContentMimeTypes( QStringList( "text/calendar" ) ); + * + * Akonadi::CollectionCreateJob *job = new Akonadi::CollectionCreateJob( collection ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(createResult(KJob*)) ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionCreateJob : public Job +{ + Q_OBJECT +public: + /** + * Creates a new collection create job. + * + * @param collection The new collection. @p collection must have a parent collection + * set with a unique identifier. If a resource context is specified in the current session + * (that is you are using it within Akonadi::ResourceBase), the parent collection can be + * identified by its remote identifier as well. + * @param parent The parent object. + */ + explicit CollectionCreateJob(const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Destroys the collection create job. + */ + virtual ~CollectionCreateJob(); + + /** + * Returns the created collection if the job was executed successfully. + */ + Collection collection() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(CollectionCreateJob) +}; + +} + +#endif diff --git a/src/core/jobs/collectiondeletejob.cpp b/src/core/jobs/collectiondeletejob.cpp new file mode 100644 index 0000000..6b8c905 --- /dev/null +++ b/src/core/jobs/collectiondeletejob.cpp @@ -0,0 +1,74 @@ +/* + 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 "collectiondeletejob.h" +#include "collection.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +#include + +using namespace Akonadi; + +class Akonadi::CollectionDeleteJobPrivate : public JobPrivate +{ +public: + CollectionDeleteJobPrivate(CollectionDeleteJob *parent) + : JobPrivate(parent) + { + } + + Collection mCollection; +}; + +CollectionDeleteJob::CollectionDeleteJob(const Collection &collection, QObject *parent) + : Job(new CollectionDeleteJobPrivate(this), parent) +{ + Q_D(CollectionDeleteJob); + + d->mCollection = collection; +} + +CollectionDeleteJob::~CollectionDeleteJob() +{ +} + +void CollectionDeleteJob::doStart() +{ + Q_D(CollectionDeleteJob); + + if (!d->mCollection.isValid() && d->mCollection.remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid collection")); + emitResult(); + return; + } + + d->sendCommand(Protocol::DeleteCollectionCommand(ProtocolHelper::entityToScope(d->mCollection))); +} + +bool CollectionDeleteJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::DeleteCollection) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/collectiondeletejob.h b/src/core/jobs/collectiondeletejob.h new file mode 100644 index 0000000..b76edba --- /dev/null +++ b/src/core/jobs/collectiondeletejob.h @@ -0,0 +1,96 @@ +/* + 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. +*/ + +#ifndef AKONADI_COLLECTIONDELETEJOB_H +#define AKONADI_COLLECTIONDELETEJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class CollectionDeleteJobPrivate; + +/** + * @short Job that deletes a collection in the Akonadi storage. + * + * This job deletes a collection and all its sub-collections as well as all associated content. + * + * @code + * + * Akonadi::Collection collection = ... + * + * Akonadi::CollectionDeleteJob *job = new Akonadi::CollectionDeleteJob( collection ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(deletionResult(KJob*)) ); + * + * @endcode + * + * @note This job deletes the data from the backend storage. To delete the collection + * from the Akonadi storage only, leaving the backend storage unchanged, delete + * the Agent instead, as follows. (Note that if it's a sub-collection, deleting + * the agent will also delete its parent collection; in this case the only + * option is to delete the sub-collection data in both Akonadi and backend + * storage.) + * + * @code + * + * const Akonadi::AgentInstance instance = + * Akonadi::AgentManager::self()->instance( collection.resource() ); + * if ( instance.isValid() ) { + * Akonadi::AgentManager::self()->removeInstance( instance ); + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionDeleteJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection delete job. The collection needs to either have a unique + * identifier or a remote identifier set. Note that using a remote identifier only works + * in a resource context (that is from within ResourceBase), as remote identifiers + * are not guaranteed to be globally unique. + * + * @param collection The collection to delete. + * @param parent The parent object. + */ + explicit CollectionDeleteJob(const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Destroys the collection delete job. + */ + ~CollectionDeleteJob(); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(CollectionDeleteJob) +}; + +} + +#endif diff --git a/src/core/jobs/collectionfetchjob.cpp b/src/core/jobs/collectionfetchjob.cpp new file mode 100644 index 0000000..157a4c8 --- /dev/null +++ b/src/core/jobs/collectionfetchjob.cpp @@ -0,0 +1,428 @@ +/* + 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 "job_p.h" +#include "protocolhelper_p.h" +#include "collection_p.h" +#include "collectionfetchscope.h" +#include "collectionutils.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +#include "akonadicore_debug.h" + +#include + +#include +#include +#include +#include + +using namespace Akonadi; + +class Akonadi::CollectionFetchJobPrivate : public JobPrivate +{ +public: + CollectionFetchJobPrivate(CollectionFetchJob *parent) + : JobPrivate(parent) + , mEmitTimer(0) + , mBasePrefetch(false) + { + + } + + void init() + { + mEmitTimer = new QTimer(q_ptr); + mEmitTimer->setSingleShot(true); + mEmitTimer->setInterval(100); + q_ptr->connect(mEmitTimer, SIGNAL(timeout()), q_ptr, SLOT(timeout())); + } + + Q_DECLARE_PUBLIC(CollectionFetchJob) + + CollectionFetchJob::Type mType; + Collection mBase; + Collection::List mBaseList; + Collection::List mCollections; + CollectionFetchScope mScope; + Collection::List mPendingCollections; + QTimer *mEmitTimer; + bool mBasePrefetch; + Collection::List mPrefetchList; + + void aboutToFinish() Q_DECL_OVERRIDE { + timeout(); + } + + void timeout() + { + Q_Q(CollectionFetchJob); + + mEmitTimer->stop(); // in case we are called by result() + if (!mPendingCollections.isEmpty()) { + if (!q->error() || mScope.ignoreRetrievalErrors()) { + emit q->collectionsReceived(mPendingCollections); + } + mPendingCollections.clear(); + } + } + + void subJobCollectionReceived(const Akonadi::Collection::List &collections) + { + mPendingCollections += collections; + if (!mEmitTimer->isActive()) { + mEmitTimer->start(); + } + } + + QString jobDebuggingString() const Q_DECL_OVERRIDE + { + if (mBase.isValid()) { + return QStringLiteral("Collection Id %1").arg(mBase.id()); + } else if (CollectionUtils::hasValidHierarchicalRID(mBase)) { + //return QLatin1String("(") + ProtocolHelper::hierarchicalRidToScope(mBase).hridChain().join(QLatin1String(", ")) + QLatin1String(")"); + return QStringLiteral("HRID chain"); + } else { + return QStringLiteral("Collection RemoteId %1").arg(mBase.remoteId()); + } + } + + bool jobFailed(KJob *job) + { + Q_Q(CollectionFetchJob); + if (mScope.ignoreRetrievalErrors()) { + int error = job->error(); + if (error && !q->error()) { + q->setError(error); + q->setErrorText(job->errorText()); + } + + if (error == Job::ConnectionFailed || + error == Job::ProtocolVersionMismatch || + error == Job::UserCanceled) { + return true; + } + return false; + } else { + return job->error(); + } + } +}; + +CollectionFetchJob::CollectionFetchJob(const Collection &collection, Type type, QObject *parent) + : Job(new CollectionFetchJobPrivate(this), parent) +{ + Q_D(CollectionFetchJob); + d->init(); + + d->mBase = collection; + d->mType = type; +} + +CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, QObject *parent) + : Job(new CollectionFetchJobPrivate(this), parent) +{ + Q_D(CollectionFetchJob); + d->init(); + + Q_ASSERT(!cols.isEmpty()); + if (cols.size() == 1) { + d->mBase = cols.first(); + } else { + d->mBaseList = cols; + } + d->mType = CollectionFetchJob::Base; +} + +CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, Type type, QObject *parent) + : Job(new CollectionFetchJobPrivate(this), parent) +{ + Q_D(CollectionFetchJob); + d->init(); + + Q_ASSERT(!cols.isEmpty()); + if (cols.size() == 1) { + d->mBase = cols.first(); + } else { + d->mBaseList = cols; + } + d->mType = type; +} + +CollectionFetchJob::CollectionFetchJob(const QList &cols, Type type, QObject *parent) + : Job(new CollectionFetchJobPrivate(this), parent) +{ + Q_D(CollectionFetchJob); + d->init(); + + Q_ASSERT(!cols.isEmpty()); + if (cols.size() == 1) { + d->mBase = Collection(cols.first()); + } else { + foreach (Collection::Id id, cols) { + d->mBaseList.append(Collection(id)); + } + } + d->mType = type; +} + +CollectionFetchJob::~CollectionFetchJob() +{ +} + +Akonadi::Collection::List CollectionFetchJob::collections() const +{ + Q_D(const CollectionFetchJob); + + return d->mCollections; +} + +void CollectionFetchJob::doStart() +{ + Q_D(CollectionFetchJob); + + if (!d->mBaseList.isEmpty()) { + if (d->mType == Recursive) { + // Because doStart starts several subjobs and @p cols could contain descendants of + // other elements in the list, if type is Recusrive, we could end up with duplicates in the result. + // To fix this we require an initial fetch of @p cols with Base and RetrieveAncestors, + // Iterate over that result removing intersections and then perform the Recursive fetch on + // the remainder. + d->mBasePrefetch = true; + // No need to connect to the collectionsReceived signal here. This job is internal. The + // result needs to be filtered through filterDescendants before it is useful. + new CollectionFetchJob(d->mBaseList, NonOverlappingRoots, this); + } else if (d->mType == NonOverlappingRoots) { + foreach (const Collection &col, d->mBaseList) { + // No need to connect to the collectionsReceived signal here. This job is internal. The (aggregated) + // result needs to be filtered through filterDescendants before it is useful. + CollectionFetchJob *subJob = new CollectionFetchJob(col, Base, this); + subJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); + } + } else { + foreach (const Collection &col, d->mBaseList) { + CollectionFetchJob *subJob = new CollectionFetchJob(col, d->mType, this); + connect(subJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(subJobCollectionReceived(Akonadi::Collection::List))); + subJob->setFetchScope(fetchScope()); + } + } + return; + } + + if (!d->mBase.isValid() && d->mBase.remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid collection given.")); + emitResult(); + return; + } + + Protocol::FetchCollectionsCommand cmd(ProtocolHelper::entityToScope(d->mBase)); + switch (d->mType) { + case Base: + cmd.setDepth(Protocol::FetchCollectionsCommand::BaseCollection); + break; + case Akonadi::CollectionFetchJob::FirstLevel: + cmd.setDepth(Protocol::FetchCollectionsCommand::ParentCollection); + break; + case Akonadi::CollectionFetchJob::Recursive: + cmd.setDepth(Protocol::FetchCollectionsCommand::AllCollections); + break; + default: + Q_ASSERT(false); + } + cmd.setResource(d->mScope.resource()); + cmd.setMimeTypes(d->mScope.contentMimeTypes()); + + switch (d->mScope.listFilter()) { + case CollectionFetchScope::Display: + cmd.setDisplayPref(true); + break; + case CollectionFetchScope::Sync: + cmd.setSyncPref(true); + break; + case CollectionFetchScope::Index: + cmd.setIndexPref(true); + break; + case CollectionFetchScope::Enabled: + cmd.setEnabled(true); + break; + case CollectionFetchScope::NoFilter: + break; + default: + Q_ASSERT(false); + } + + cmd.setFetchStats(d->mScope.includeStatistics()); + switch (d->mScope.ancestorRetrieval()) { + case CollectionFetchScope::None: + cmd.setAncestorsDepth(Protocol::Ancestor::NoAncestor); + break; + case CollectionFetchScope::Parent: + cmd.setAncestorsDepth(Protocol::Ancestor::ParentAncestor); + break; + case CollectionFetchScope::All: + cmd.setAncestorsDepth(Protocol::Ancestor::AllAncestors); + break; + } + if (d->mScope.ancestorRetrieval() != CollectionFetchScope::None) { + cmd.setAncestorsAttributes(d->mScope.ancestorFetchScope().attributes()); + } + + d->sendCommand(cmd); +} + +bool CollectionFetchJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(CollectionFetchJob); + + if (d->mBasePrefetch || d->mType == NonOverlappingRoots) { + return false; + } + + if (!response.isResponse() || response.type() != Protocol::Command::FetchCollections) { + return Job::doHandleResponse(tag, response); + } + + Protocol::FetchCollectionsResponse resp(response); + // Invalid response (no ID) means this was the last response + if (resp.id() == -1) { + return true; + } + + Collection collection = ProtocolHelper::parseCollection(resp, true); + if (!collection.isValid()) { + return false; + } + + collection.d_ptr->resetChangeLog(); + d->mCollections.append(collection); + d->mPendingCollections.append(collection); + if (!d->mEmitTimer->isActive()) { + d->mEmitTimer->start(); + } + + return false; +} + +static Collection::List filterDescendants(const Collection::List &list) +{ + Collection::List result; + + QVector > ids; + ids.reserve(list.count()); + foreach (const Collection &collection, list) { + QList ancestors; + Collection parent = collection.parentCollection(); + ancestors << parent.id(); + if (parent != Collection::root()) { + while (parent.parentCollection() != Collection::root()) { + parent = parent.parentCollection(); + QList::iterator i = qLowerBound(ancestors.begin(), ancestors.end(), parent.id()); + ancestors.insert(i, parent.id()); + } + } + ids << ancestors; + } + + QSet excludeList; + foreach (const Collection &collection, list) { + int i = 0; + foreach (const QList &ancestors, ids) { + if (qBinaryFind(ancestors, collection.id()) != ancestors.end()) { + excludeList.insert(list.at(i).id()); + } + ++i; + } + } + + foreach (const Collection &collection, list) { + if (!excludeList.contains(collection.id())) { + result.append(collection); + } + } + + return result; +} + +void CollectionFetchJob::slotResult(KJob *job) +{ + Q_D(CollectionFetchJob); + + CollectionFetchJob *list = qobject_cast(job); + Q_ASSERT(job); + + if (d->mType == NonOverlappingRoots) { + d->mPrefetchList += list->collections(); + } else if (!d->mBasePrefetch) { + d->mCollections += list->collections(); + } + + if (d_ptr->mCurrentSubJob == job && !d->jobFailed(job)) { + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error during CollectionFetchJob: " << job->errorString(); + } + d_ptr->mCurrentSubJob = 0; + removeSubjob(job); + QTimer::singleShot(0, this, SLOT(startNext())); + } else { + Job::slotResult(job); + } + + if (d->mBasePrefetch) { + d->mBasePrefetch = false; + const Collection::List roots = list->collections(); + Q_ASSERT(!hasSubjobs()); + if (!job->error()) { + foreach (const Collection &col, roots) { + CollectionFetchJob *subJob = new CollectionFetchJob(col, d->mType, this); + connect(subJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(subJobCollectionReceived(Akonadi::Collection::List))); + subJob->setFetchScope(fetchScope()); + } + } + // No result yet. + } else if (d->mType == NonOverlappingRoots) { + if (!d->jobFailed(job) && !hasSubjobs()) { + const Collection::List result = filterDescendants(d->mPrefetchList); + d->mPendingCollections += result; + d->mCollections = result; + d->delayedEmitResult(); + } + } else { + if (!d->jobFailed(job) && !hasSubjobs()) { + d->delayedEmitResult(); + } + } +} + +void CollectionFetchJob::setFetchScope(const CollectionFetchScope &scope) +{ + Q_D(CollectionFetchJob); + d->mScope = scope; +} + +CollectionFetchScope &CollectionFetchJob::fetchScope() +{ + Q_D(CollectionFetchJob); + return d->mScope; +} + +#include "moc_collectionfetchjob.cpp" diff --git a/src/core/jobs/collectionfetchjob.h b/src/core/jobs/collectionfetchjob.h new file mode 100644 index 0000000..d6cc282 --- /dev/null +++ b/src/core/jobs/collectionfetchjob.h @@ -0,0 +1,200 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_COLLECTIONFETCHJOB_H +#define AKONADI_COLLECTIONFETCHJOB_H + +#include "akonadicore_export.h" +#include "collection.h" +#include "job.h" + +namespace Akonadi +{ + +class CollectionFetchScope; +class CollectionFetchJobPrivate; + +/** + * @short Job that fetches collections from the Akonadi storage. + * + * This class can be used to retrieve the complete or partial collection tree + * from the Akonadi storage. This fetches collection data, not item data. + * + * @code + * + * using namespace Akonadi; + * + * // fetching all collections containing emails recursively, starting at the root collection + * CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + * job->fetchScope().setContentMimeTypes(QStringList() << "message/rfc822"); + * connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + * this, SLOT(myCollectionsReceived(Akonadi::Collection::List))); + * connect(job, SIGNAL(result(KJob*)), this, SLOT(collectionFetchResult(KJob*))); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionFetchJob : public Job +{ + Q_OBJECT + +public: + /** + * Describes the type of fetch depth. + */ + enum Type { + Base, ///< Only fetch the base collection. + FirstLevel, ///< Only list direct sub-collections of the base collection. + Recursive, ///< List all sub-collections. + NonOverlappingRoots ///< List the roots of a list of fetched collections. @since 4.7 + }; + + /** + * Creates a new collection fetch job. If the given base collection + * has a unique identifier, this is used to identify the collection in the + * Akonadi server. If only a remote identifier is available the collection + * is identified using that, provided that a resource search context has + * been specified by calling setResource(). + * + * @internal + * For internal use only, if a remote identifier is set, the resource + * search context can be set globally using ResourceSelectJob. + * @endinternal + * + * @param collection The base collection for the listing. + * @param type The type of fetch depth. + * @param parent The parent object. + */ + explicit CollectionFetchJob(const Collection &collection, Type type = FirstLevel, QObject *parent = Q_NULLPTR); + + /** + * Creates a new collection fetch job to retrieve a list of collections. + * If a given collection has a unique identifier, this is used to identify + * the collection in the Akonadi server. If only a remote identifier is + * available the collection is identified using that, provided that a + * resource search context has been specified by calling setResource(). + * + * @internal + * For internal use only, if a remote identifier is set, the resource + * search context can be set globally using ResourceSelectJob. + * @endinternal + * + * @param collections A list of collections to fetch. Must not be empty. + * @param parent The parent object. + */ + explicit CollectionFetchJob(const Collection::List &collections, QObject *parent = Q_NULLPTR); + + /** + * Creates a new collection fetch job to retrieve a list of collections. + * If a given collection has a unique identifier, this is used to identify + * the collection in the Akonadi server. If only a remote identifier is + * available the collection is identified using that, provided that a + * resource search context has been specified by calling setResource(). + * + * @internal + * For internal use only, if a remote identifier is set, the resource + * search context can be set globally using ResourceSelectJob. + * @endinternal + * + * @param collections A list of collections to fetch. Must not be empty. + * @param type The type of fetch depth. + * @param parent The parent object. + * @todo KDE5 merge with ctor above. + * @since 4.7 + */ + CollectionFetchJob(const Collection::List &collections, Type type, QObject *parent = Q_NULLPTR); + + /** + * Convenience ctor equivalent to CollectionFetchJob(const Collection::List &collections, Type type, QObject *parent = Q_NULLPTR) + * @since 4.8 + * @param collections list of collection ids + * @param type fetch job type + * @param parent parent object + */ + explicit CollectionFetchJob(const QList &collections, Type type = Base, QObject *parent = Q_NULLPTR); + + /** + * Destroys the collection fetch job. + */ + virtual ~CollectionFetchJob(); + + /** + * Returns the list of fetched collection. + */ + Collection::List collections() const; + + /** + * Sets the collection fetch scope. + * + * The CollectionFetchScope controls how much of a collection's data is + * fetched from the server as well as a filter to select which collections + * to fetch. + * + * @param fetchScope The new scope for collection fetch operations. + * + * @see fetchScope() + * @since 4.4 + */ + void setFetchScope(const CollectionFetchScope &fetchScope); + + /** + * Returns the collection fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the CollectionFetchScope documentation + * for an example. + * + * @return a reference to the current collection fetch scope + * + * @see setFetchScope() for replacing the current collection fetch scope + * @since 4.4 + */ + CollectionFetchScope &fetchScope(); + +Q_SIGNALS: + /** + * This signal is emitted whenever the job has received collections. + * + * @param collections The received collections. + */ + void collectionsReceived(const Akonadi::Collection::List &collections); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +protected Q_SLOTS: + //@cond PRIVATE + void slotResult(KJob *job) Q_DECL_OVERRIDE; + //@endcond + +private: + Q_DECLARE_PRIVATE(CollectionFetchJob) + + //@cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void timeout()) + Q_PRIVATE_SLOT(d_func(), void subJobCollectionReceived(const Akonadi::Collection::List &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/collectionmodifyjob.cpp b/src/core/jobs/collectionmodifyjob.cpp new file mode 100644 index 0000000..a798371 --- /dev/null +++ b/src/core/jobs/collectionmodifyjob.cpp @@ -0,0 +1,134 @@ +/* + 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 "collectionmodifyjob.h" + +#include "changemediator_p.h" +#include "collection_p.h" +#include "collectionstatistics.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::CollectionModifyJobPrivate : public JobPrivate +{ +public: + CollectionModifyJobPrivate(CollectionModifyJob *parent) + : JobPrivate(parent) + { + } + + QString jobDebuggingString() const Q_DECL_OVERRIDE + { + return QStringLiteral("Collection Id %1").arg(mCollection.id()); + } + + Collection mCollection; +}; + +CollectionModifyJob::CollectionModifyJob(const Collection &collection, QObject *parent) + : Job(new CollectionModifyJobPrivate(this), parent) +{ + Q_D(CollectionModifyJob); + d->mCollection = collection; +} + +CollectionModifyJob::~CollectionModifyJob() +{ +} + +void CollectionModifyJob::doStart() +{ + Q_D(CollectionModifyJob); + + Protocol::ModifyCollectionCommand cmd; + try { + cmd = Protocol::ModifyCollectionCommand(ProtocolHelper::entityToScope(d->mCollection)); + } catch (const std::exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } + + if (d->mCollection.d_ptr->contentTypesChanged) { + cmd.setMimeTypes(d->mCollection.contentMimeTypes()); + } + if (d->mCollection.parentCollection().id() >= 0) { + cmd.setParentId(d->mCollection.parentCollection().id()); + } + if (!d->mCollection.name().isEmpty()) { + cmd.setName(d->mCollection.name()); + } + if (!d->mCollection.remoteId().isNull()) { + cmd.setRemoteId(d->mCollection.remoteId()); + } + if (!d->mCollection.remoteRevision().isNull()) { + cmd.setRemoteRevision(d->mCollection.remoteRevision()); + } + if (d->mCollection.d_ptr->cachePolicyChanged) { + cmd.setCachePolicy(ProtocolHelper::cachePolicyToProtocol(d->mCollection.cachePolicy())); + } + if (d->mCollection.d_ptr->enabledChanged) { + cmd.setEnabled(d->mCollection.enabled()); + } + if (d->mCollection.d_ptr->listPreferenceChanged) { + cmd.setDisplayPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListDisplay))); + cmd.setSyncPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListSync))); + cmd.setIndexPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListIndex))); + } + if (d->mCollection.d_ptr->referencedChanged) { + cmd.setReferenced(d->mCollection.referenced()); + } + if (!d->mCollection.attributes().isEmpty()) { + cmd.setAttributes(ProtocolHelper::attributesToProtocol(d->mCollection)); + } + if (!d->mCollection.d_ptr->mDeletedAttributes.isEmpty()) { + cmd.setRemovedAttributes(d->mCollection.d_ptr->mDeletedAttributes); + } + + if (cmd.modifiedParts() == Protocol::ModifyCollectionCommand::None) { + emitResult(); + return; + } + + d->sendCommand(cmd); + + ChangeMediator::invalidateCollection(d->mCollection); +} + +bool CollectionModifyJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(CollectionModifyJob); + + if (!response.isResponse() || response.type() != Protocol::Command::ModifyCollection) { + return Job::doHandleResponse(tag, response); + } + + d->mCollection.d_ptr->resetChangeLog(); + return true; +} + +Collection CollectionModifyJob::collection() const +{ + const Q_D(CollectionModifyJob); + return d->mCollection; +} diff --git a/src/core/jobs/collectionmodifyjob.h b/src/core/jobs/collectionmodifyjob.h new file mode 100644 index 0000000..cac860b --- /dev/null +++ b/src/core/jobs/collectionmodifyjob.h @@ -0,0 +1,121 @@ +/* + 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. +*/ + +#ifndef AKONADI_COLLECTIONMODIFYJOB_H +#define AKONADI_COLLECTIONMODIFYJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class CachePolicy; +class Collection; +class CollectionModifyJobPrivate; + +/** + * @short Job that modifies a collection in the Akonadi storage. + * + * This job modifies the properties of an existing collection. + * + * @code + * + * Akonadi::Collection collection = ... + * + * Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob( collection ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(modifyResult(KJob*)) ); + * + * @endcode + * + * If the collection has attributes, it is recommended only to supply values for + * any attributes whose values are to be updated. This will help to avoid + * potential clashes with other resources or applications which may happen to + * update the collection simultaneously. To avoid supplying attribute values which + * are not needed, create a new instance of the collection and explicitly set + * attributes to be updated, e.g. + * + * @code + * + * // Update the 'MyAttribute' attribute of 'collection'. + * Akonadi::Collection c( collection.id() ); + * MyAttribute *attribute = c.attribute( Collection::AddIfMissing ); + * if ( collection.hasAttribute() ) { + * *attribute = *collection.attribute(); + * } + * // Update the value of 'attribute' ... + * Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob( c ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(modifyResult(KJob*)) ); + * + * @endcode + * + * To update only the collection, and not change any attributes: + * + * @code + * + * // Update the cache policy for 'collection' to 'newPolicy'. + * Akonadi::Collection c( collection.id() ); + * c.setCachePolicy( newPolicy ); + * Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob( c ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(modifyResult(KJob*)) ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionModifyJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection modify job for the given collection. The collection can be + * identified either by its unique identifier or its remote identifier. Since the remote + * identifier is not necessarily globally unique, identification by remote identifier only + * works inside a resource context (that is from within ResourceBase) and is therefore + * limited to one resource. + * + * @param collection The collection to modify. + * @param parent The parent object. + */ + explicit CollectionModifyJob(const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Destroys the collection modify job. + */ + ~CollectionModifyJob(); + + /** + * Returns the modified collection. + * + * @since 4.4 + */ + Collection collection() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(CollectionModifyJob) +}; + +} + +#endif diff --git a/src/core/jobs/collectionmovejob.cpp b/src/core/jobs/collectionmovejob.cpp new file mode 100644 index 0000000..41423c7 --- /dev/null +++ b/src/core/jobs/collectionmovejob.cpp @@ -0,0 +1,84 @@ +/* + 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 "collectionmovejob.h" +#include "collection.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +#include + + +using namespace Akonadi; + +class Akonadi::CollectionMoveJobPrivate : public JobPrivate +{ +public: + CollectionMoveJobPrivate(CollectionMoveJob *parent) + : JobPrivate(parent) + { + } + + Collection destination; + Collection collection; + + Q_DECLARE_PUBLIC(CollectionMoveJob) +}; + +CollectionMoveJob::CollectionMoveJob(const Collection &collection, const Collection &destination, QObject *parent) + : Job(new CollectionMoveJobPrivate(this), parent) +{ + Q_D(CollectionMoveJob); + d->destination = destination; + d->collection = collection; +} + +void CollectionMoveJob::doStart() +{ + Q_D(CollectionMoveJob); + + if (!d->collection.isValid()) { + setError(Job::Unknown); + setErrorText(i18n("No objects specified for moving")); + emitResult(); + return; + } + + if (!d->destination.isValid() && d->destination.remoteId().isEmpty()) { + setError(Job::Unknown); + setErrorText(i18n("No valid destination specified")); + emitResult(); + return; + } + + const Scope colScope = ProtocolHelper::entitySetToScope(Collection::List() << d->collection); + const Scope destScope = ProtocolHelper::entitySetToScope(Collection::List() << d->destination); + + d->sendCommand(Protocol::MoveCollectionCommand(colScope, destScope)); +} + +bool CollectionMoveJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::MoveCollection) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/collectionmovejob.h b/src/core/jobs/collectionmovejob.h new file mode 100644 index 0000000..a03b76c --- /dev/null +++ b/src/core/jobs/collectionmovejob.h @@ -0,0 +1,74 @@ +/* + 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. +*/ + +#ifndef AKONADI_COLLECTIONMOVEJOB_H +#define AKONADI_COLLECTIONMOVEJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class CollectionMoveJobPrivate; + +/** + * @short Job that moves a collection in the Akonadi storage to a new parent collection. + * + * This job moves an existing collection to a new parent collection. + * + * @code + * + * const Akonadi::Collection collection = ... + * const Akonadi::Collection newParent = ... + * + * Akonadi::CollectionMoveJob *job = new Akonadi::CollectionMoveJob( collection, newParent ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(moveResult(KJob*)) ); + * + * @endcode + * + * @since 4.4 + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionMoveJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection move job for the given collection and destination + * + * @param collection The collection to move. + * @param destination The destination collection where @p collection should be moved to. + * @param parent The parent object. + */ + CollectionMoveJob(const Collection &collection, const Collection &destination, QObject *parent = Q_NULLPTR); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(CollectionMoveJob) +}; + +} + +#endif diff --git a/src/core/jobs/collectionstatisticsjob.cpp b/src/core/jobs/collectionstatisticsjob.cpp new file mode 100644 index 0000000..86cbc5f --- /dev/null +++ b/src/core/jobs/collectionstatisticsjob.cpp @@ -0,0 +1,97 @@ +/* + 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 "collectionstatisticsjob.h" + +#include "collection.h" +#include "collectionstatistics.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::CollectionStatisticsJobPrivate : public JobPrivate +{ +public: + CollectionStatisticsJobPrivate(CollectionStatisticsJob *parent) + : JobPrivate(parent) + { + } + + QString jobDebuggingString() const Q_DECL_OVERRIDE + { + return QStringLiteral("Collection Id %1").arg(mCollection.id()); + } + + Collection mCollection; + CollectionStatistics mStatistics; +}; + +CollectionStatisticsJob::CollectionStatisticsJob(const Collection &collection, QObject *parent) + : Job(new CollectionStatisticsJobPrivate(this), parent) +{ + Q_D(CollectionStatisticsJob); + + d->mCollection = collection; +} + +CollectionStatisticsJob::~CollectionStatisticsJob() +{ +} + +void CollectionStatisticsJob::doStart() +{ + Q_D(CollectionStatisticsJob); + + try { + d->sendCommand(Protocol::FetchCollectionStatsCommand(ProtocolHelper::entityToScope(d->mCollection))); + } catch (const std::exception &e) { + setError(Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } +} + +bool CollectionStatisticsJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(CollectionStatisticsJob); + + if (!response.isResponse() || response.type() != Protocol::Command::FetchCollectionStats) { + return Job::doHandleResponse(tag, response); + } + + d->mStatistics = ProtocolHelper::parseCollectionStatistics(response); + return true; +} + +Collection CollectionStatisticsJob::collection() const +{ + Q_D(const CollectionStatisticsJob); + + return d->mCollection; +} + +CollectionStatistics Akonadi::CollectionStatisticsJob::statistics() const +{ + Q_D(const CollectionStatisticsJob); + + return d->mStatistics; +} diff --git a/src/core/jobs/collectionstatisticsjob.h b/src/core/jobs/collectionstatisticsjob.h new file mode 100644 index 0000000..ca259aa --- /dev/null +++ b/src/core/jobs/collectionstatisticsjob.h @@ -0,0 +1,105 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_COLLECTIONSTATISTICSJOB_H +#define AKONADI_COLLECTIONSTATISTICSJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class CollectionStatistics; +class CollectionStatisticsJobPrivate; + +/** + * @short Job that fetches collection statistics from the Akonadi storage. + * + * This class fetches the CollectionStatistics object for a given collection. + * + * Example: + * + * @code + * + * Akonadi::Collection collection = ... + * + * Akonadi::CollectionStatisticsJob *job = new Akonadi::CollectionStatisticsJob( collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * CollectionStatisticsJob *statisticsJob = qobject_cast( job ); + * + * const Akonadi::CollectionStatistics statistics = statisticsJob->statistics(); + * qDebug() << "Unread items:" << statistics.unreadCount(); + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionStatisticsJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection statistics job. + * + * @param collection The collection to fetch the statistics from. + * @param parent The parent object. + */ + explicit CollectionStatisticsJob(const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Destroys the collection statistics job. + */ + virtual ~CollectionStatisticsJob(); + + /** + * Returns the fetched collection statistics. + */ + CollectionStatistics statistics() const; + + /** + * Returns the corresponding collection, if the job was executed successfully, + * the collection is already updated. + */ + Collection collection() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(CollectionStatisticsJob) +}; + +} + +#endif diff --git a/src/core/jobs/invalidatecachejob.cpp b/src/core/jobs/invalidatecachejob.cpp new file mode 100644 index 0000000..c3f1d7b --- /dev/null +++ b/src/core/jobs/invalidatecachejob.cpp @@ -0,0 +1,120 @@ +/* + Copyright (c) 2011 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 "invalidatecachejob_p.h" +#include "job_p.h" +#include "collectionfetchjob.h" +#include "itemfetchjob.h" +#include "itemmodifyjob.h" + +#include + +using namespace Akonadi; + +namespace Akonadi +{ + +class InvalidateCacheJobPrivate : JobPrivate +{ +public: + InvalidateCacheJobPrivate(InvalidateCacheJob *qq) + : JobPrivate(qq) + { + } + Collection collection; + + void collectionFetchResult(KJob *job); + void itemFetchResult(KJob *job); + void itemStoreResult(KJob *job); + + Q_DECLARE_PUBLIC(InvalidateCacheJob) +}; + +} + +void InvalidateCacheJobPrivate::collectionFetchResult(KJob *job) +{ + Q_Q(InvalidateCacheJob); + if (job->error()) { + return; // handled by KCompositeJob + } + + CollectionFetchJob *fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + if (fetchJob->collections().size() == 1) { + collection = fetchJob->collections().at(0); + } + + if (!collection.isValid()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid collection.")); + q->emitResult(); + return; + } + + ItemFetchJob *itemFetch = new ItemFetchJob(collection, q); + QObject::connect(itemFetch, SIGNAL(result(KJob*)), q, SLOT(itemFetchResult(KJob*))); +} + +void InvalidateCacheJobPrivate::itemFetchResult(KJob *job) +{ + Q_Q(InvalidateCacheJob); + if (job->error()) { + return; + } + ItemFetchJob *fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + if (fetchJob->items().size() == 0) { + q->emitResult(); + return; + } + + ItemModifyJob *modJob = 0; + foreach (Item item, fetchJob->items()) { //krazy:exclude=foreach, item cannot be const + item.clearPayload(); + modJob = new ItemModifyJob(item, q); + } + QObject::connect(modJob, SIGNAL(result(KJob*)), q, SLOT(itemStoreResult(KJob*))); +} + +void InvalidateCacheJobPrivate::itemStoreResult(KJob *job) +{ + Q_Q(InvalidateCacheJob); + if (job->error()) { + return; + } + q->emitResult(); +} + +InvalidateCacheJob::InvalidateCacheJob(const Collection &collection, QObject *parent) + : Job(new InvalidateCacheJobPrivate(this), parent) +{ + Q_D(InvalidateCacheJob); + d->collection = collection; +} + +void InvalidateCacheJob::doStart() +{ + Q_D(InvalidateCacheJob); + // resolve RID-only collections + CollectionFetchJob *job = new CollectionFetchJob(d->collection, Akonadi::CollectionFetchJob::Base, this); + connect(job, SIGNAL(result(KJob*)), SLOT(collectionFetchResult(KJob*))); +} + +#include "moc_invalidatecachejob_p.cpp" diff --git a/src/core/jobs/invalidatecachejob_p.h b/src/core/jobs/invalidatecachejob_p.h new file mode 100644 index 0000000..62e9d9f --- /dev/null +++ b/src/core/jobs/invalidatecachejob_p.h @@ -0,0 +1,57 @@ +/* + Copyright (c) 2011 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_INVALIDATECACHEJOB_P_H +#define AKONADI_INVALIDATECACHEJOB_P_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class InvalidateCacheJobPrivate; + +/** + * Helper job to invalidate item cache for an entire collection. + * @since 4.8 + */ +class AKONADICORE_EXPORT InvalidateCacheJob : public Akonadi::Job +{ + Q_OBJECT +public: + /** + * Create a job to invalidate all cached content in @p collection. + */ + explicit InvalidateCacheJob(const Collection &collection, QObject *parent); + +protected: + void doStart() Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(InvalidateCacheJob) + Q_PRIVATE_SLOT(d_func(), void collectionFetchResult(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void itemFetchResult(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void itemStoreResult(KJob *job)) +}; + +} + +#endif // AKONADI_INVALIDATECACHEJOB_P_H diff --git a/src/core/jobs/itemcopyjob.cpp b/src/core/jobs/itemcopyjob.cpp new file mode 100644 index 0000000..dc676be --- /dev/null +++ b/src/core/jobs/itemcopyjob.cpp @@ -0,0 +1,84 @@ +/* + 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 "itemcopyjob.h" + +#include "collection.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::ItemCopyJobPrivate : public JobPrivate +{ +public: + ItemCopyJobPrivate(ItemCopyJob *parent) + : JobPrivate(parent) + { + } + + Item::List mItems; + Collection mTarget; +}; + +ItemCopyJob::ItemCopyJob(const Item &item, const Collection &target, QObject *parent) + : Job(new ItemCopyJobPrivate(this), parent) +{ + Q_D(ItemCopyJob); + + d->mItems << item; + d->mTarget = target; +} + +ItemCopyJob::ItemCopyJob(const Item::List &items, const Collection &target, QObject *parent) + : Job(new ItemCopyJobPrivate(this), parent) +{ + Q_D(ItemCopyJob); + + d->mItems = items; + d->mTarget = target; +} + +ItemCopyJob::~ItemCopyJob() +{ +} + +void ItemCopyJob::doStart() +{ + Q_D(ItemCopyJob); + + try { + d->sendCommand(Protocol::CopyItemsCommand(ProtocolHelper::entitySetToScope(d->mItems), + ProtocolHelper::entityToScope(d->mTarget))); + } catch (std::exception &e) { + setError(Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + } +} + +bool ItemCopyJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::CopyItems) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/itemcopyjob.h b/src/core/jobs/itemcopyjob.h new file mode 100644 index 0000000..de051d4 --- /dev/null +++ b/src/core/jobs/itemcopyjob.h @@ -0,0 +1,100 @@ +/* + 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_ITEMCOPYJOB_H +#define AKONADI_ITEMCOPYJOB_H + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class ItemCopyJobPrivate; + +/** + * @short Job that copies a set of items to a target collection in the Akonadi storage. + * + * The job can be used to copy one or several Item objects to another collection. + * + * Example: + * + * @code + * + * Akonadi::Item::List items = ... + * Akonadi::Collection collection = ... + * + * Akonadi::ItemCopyJob *job = new Akonadi::ItemCopyJob( items, collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Items copied successfully"; + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemCopyJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new item copy job. + * + * @param item The item to copy. + * @param target The target collection. + * @param parent The parent object. + */ + ItemCopyJob(const Item &item, const Collection &target, QObject *parent = Q_NULLPTR); + + /** + * Creates a new item copy job. + * + * @param items A list of items to copy. + * @param target The target collection. + * @param parent The parent object. + */ + ItemCopyJob(const Item::List &items, const Collection &target, QObject *parent = Q_NULLPTR); + + /** + * Destroys the item copy job. + */ + ~ItemCopyJob(); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(ItemCopyJob) +}; + +} + +#endif diff --git a/src/core/jobs/itemcreatejob.cpp b/src/core/jobs/itemcreatejob.cpp new file mode 100644 index 0000000..e03e47f --- /dev/null +++ b/src/core/jobs/itemcreatejob.cpp @@ -0,0 +1,225 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + Copyright (c) 2007 Robert Zwerus + Copyright (c) 2014 Daniel Vrátil + + 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 "itemcreatejob.h" + +#include "collection.h" +#include "item.h" +#include "item_p.h" +#include "itemserializer_p.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "gidextractor_p.h" +#include "private/protocol_p.h" + +#include +#include + +#include + + +using namespace Akonadi; + +class Akonadi::ItemCreateJobPrivate : public JobPrivate +{ +public: + ItemCreateJobPrivate(ItemCreateJob *parent) + : JobPrivate(parent) + , mMergeOptions(ItemCreateJob::NoMerge) + , mItemReceived(false) + { + } + + Protocol::PartMetaData preparePart(const QByteArray &part); + + Collection mCollection; + Item mItem; + QSet mParts; + Item::Id mUid; + QDateTime mDatetime; + QByteArray mPendingData; + ItemCreateJob::MergeOptions mMergeOptions; + bool mItemReceived; +}; + +Protocol::PartMetaData ItemCreateJobPrivate::preparePart(const QByteArray &partName) +{ + ProtocolHelper::PartNamespace ns; //dummy + const QByteArray partLabel = ProtocolHelper::decodePartIdentifier(partName, ns); + if (!mParts.remove(partLabel)) { + // ERROR? + return Protocol::PartMetaData(); + } + + mPendingData.clear(); + int version = 0; + ItemSerializer::serialize(mItem, partLabel, mPendingData, version); + + return Protocol::PartMetaData(partName, mPendingData.size(), version); +} + +ItemCreateJob::ItemCreateJob(const Item &item, const Collection &collection, QObject *parent) + : Job(new ItemCreateJobPrivate(this), parent) +{ + Q_D(ItemCreateJob); + + Q_ASSERT(!item.mimeType().isEmpty()); + d->mItem = item; + d->mParts = d->mItem.loadedPayloadParts(); + d->mCollection = collection; +} + +ItemCreateJob::~ItemCreateJob() +{ +} + +void ItemCreateJob::doStart() +{ + Q_D(ItemCreateJob); + + if (!d->mCollection.isValid()) { + setError(Unknown); + setErrorText(i18n("Invalid parent collection")); + emitResult(); + return; + } + + Protocol::CreateItemCommand cmd; + cmd.setMimeType(d->mItem.mimeType()); + cmd.setGID(d->mItem.gid()); + cmd.setRemoteId(d->mItem.remoteId()); + cmd.setRemoteRevision(d->mItem.remoteRevision()); + + Protocol::CreateItemCommand::MergeModes mergeModes = Protocol::CreateItemCommand::None; + if ((d->mMergeOptions & GID) && !d->mItem.gid().isEmpty()) { + mergeModes |= Protocol::CreateItemCommand::GID; + } + if ((d->mMergeOptions & RID) && !d->mItem.remoteId().isEmpty()) { + mergeModes |= Protocol::CreateItemCommand::RemoteID; + } + if ((d->mMergeOptions & Silent)) { + mergeModes |= Protocol::CreateItemCommand::Silent; + } + const bool merge = (mergeModes & Protocol::CreateItemCommand::GID) + || (mergeModes & Protocol::CreateItemCommand::RemoteID); + cmd.setMergeModes(mergeModes); + + if (d->mItem.d_ptr->mFlagsOverwritten || !merge) { + cmd.setFlags(d->mItem.flags()); + } else { + auto addedFlags = ItemChangeLog::instance()->addedFlags(d->mItem.d_ptr); + auto deletedFlags = ItemChangeLog::instance()->deletedFlags(d->mItem.d_ptr); + cmd.setAddedFlags(addedFlags); + cmd.setRemovedFlags(deletedFlags); + } + auto addedTags = ItemChangeLog::instance()->addedTags(d->mItem.d_ptr); + auto deletedTags = ItemChangeLog::instance()->deletedTags(d->mItem.d_ptr); + if (!addedTags.isEmpty() && (d->mItem.d_ptr->mTagsOverwritten || !merge)) { + cmd.setTags(ProtocolHelper::entitySetToScope(addedTags)); + } else { + if (!addedTags.isEmpty()) { + cmd.setAddedTags(ProtocolHelper::entitySetToScope(addedTags)); + } + if (!deletedTags.isEmpty()) { + cmd.setRemovedTags(ProtocolHelper::entitySetToScope(deletedTags)); + } + } + + cmd.setCollection(ProtocolHelper::entityToScope(d->mCollection)); + cmd.setItemSize(d->mItem.size()); + + cmd.setAttributes(ProtocolHelper::attributesToProtocol(d->mItem)); + QSet parts; + parts.reserve(d->mParts.size()); + Q_FOREACH (const QByteArray &part, d->mParts) { + parts.insert(ProtocolHelper::encodePartIdentifier(ProtocolHelper::PartPayload, part)); + } + cmd.setParts(parts); + + d->sendCommand(cmd); +} + +bool ItemCreateJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(ItemCreateJob); + + if (!response.isResponse() && response.type() == Protocol::Command::StreamPayload) { + const Protocol::StreamPayloadCommand streamCmd(response); + Protocol::StreamPayloadResponse streamResp; + streamResp.setPayloadName(streamCmd.payloadName()); + if (streamCmd.request() == Protocol::StreamPayloadCommand::MetaData) { + streamResp.setMetaData(d->preparePart(streamCmd.payloadName())); + } else { + if (streamCmd.destination().isEmpty()) { + streamResp.setData(d->mPendingData); + } else { + QByteArray error; + if (!ProtocolHelper::streamPayloadToFile(streamCmd.destination(), d->mPendingData, error)) { + // Error? + } + } + } + d->sendCommand(tag, streamResp); + return false; + } + + if (response.isResponse() && response.type() == Protocol::Command::FetchItems) { + Protocol::FetchItemsResponse fetchResp(response); + Item item = ProtocolHelper::parseItemFetchResult(fetchResp); + if (!item.isValid()) { + // Error, maybe? + return false; + } + d->mItem = item; + return false; + } + + if (response.isResponse() && response.type() == Protocol::Command::CreateItem) { + return true; + } + + return Job::doHandleResponse(tag, response); +} + +void ItemCreateJob::setMerge(ItemCreateJob::MergeOptions options) +{ + Q_D(ItemCreateJob); + + d->mMergeOptions = options; +} + +Item ItemCreateJob::item() const +{ + Q_D(const ItemCreateJob); + + // Parent collection is available only with non-silent merge/create + if (d->mItem.parentCollection().isValid()) { + return d->mItem; + } + + Item item(d->mItem); + item.setRevision(0); + item.setModificationTime(d->mDatetime); + item.setParentCollection(d->mCollection); + item.setStorageCollectionId(d->mCollection.id()); + + return item; +} diff --git a/src/core/jobs/itemcreatejob.h b/src/core/jobs/itemcreatejob.h new file mode 100644 index 0000000..1ba7da3 --- /dev/null +++ b/src/core/jobs/itemcreatejob.h @@ -0,0 +1,140 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_ITEMCREATEJOB_H +#define AKONADI_ITEMCREATEJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class Item; +class ItemCreateJobPrivate; + +/** + * @short Job that creates a new item in the Akonadi storage. + * + * This job creates a new item with all the set properties in the + * given target collection. + * + * Note that items can not be created in the root collection (Collection::root()) + * and the collection must have Collection::contentMimeTypes() that match the mimetype + * of the item being created. + * + * Example: + * + * @code + * + * // Create a contact item in the root collection + * + * KContacts::Addressee addr; + * addr.setNameFromString( "Joe Jr. Miller" ); + * + * Akonadi::Item item; + * item.setMimeType( "text/directory" ); + * item.setPayload( addr ); + * + * Akonadi::Collection collection = getCollection(); + * + * Akonadi::ItemCreateJob *job = new Akonadi::ItemCreateJob( item, collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Contact item created successfully"; + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemCreateJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new item create job. + * + * @param item The item to create. + * @note It must have a mime type set. + * @param collection The parent collection where the new item shall be located in. + * @param parent The parent object. + */ + ItemCreateJob(const Item &item, const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Destroys the item create job. + */ + ~ItemCreateJob(); + + /** + * Returns the created item with the new unique id, or an invalid item if the job failed. + */ + Item item() const; + + enum MergeOption { + NoMerge = 0, ///< Don't merge + RID = 1, ///< Merge by remote id + GID = 2, ///< Merge by GID + Silent = 4 ///< Only return the id of the merged/created item. + }; + Q_DECLARE_FLAGS(MergeOptions, MergeOption) + + /** + * Merge this item into an existing one if available. + * + * If an item with same GID and/or remote ID as the created item exists in + * specified collection (depending on the provided options), the new item will + * be merged into the existing one and the merged item will be returned + * (unless the Silent option is used). + * + * If no matching item is found a new item is created. + * + * If the item does not have a GID or RID, this option will be + * ignored and a new item will be created. + * + * By default, merging is disabled. + * + * @param options Merge options. + * @since 4.14 + */ + void setMerge(MergeOptions options); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(ItemCreateJob) +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(ItemCreateJob::MergeOptions) + +} + +#endif diff --git a/src/core/jobs/itemdeletejob.cpp b/src/core/jobs/itemdeletejob.cpp new file mode 100644 index 0000000..6a96e0e --- /dev/null +++ b/src/core/jobs/itemdeletejob.cpp @@ -0,0 +1,113 @@ +/* + 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 "itemdeletejob.h" + +#include "collection.h" +#include "item.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::ItemDeleteJobPrivate : public JobPrivate +{ +public: + ItemDeleteJobPrivate(ItemDeleteJob *parent) + : JobPrivate(parent) + { + } + + Q_DECLARE_PUBLIC(ItemDeleteJob) + + Item::List mItems; + Collection mCollection; + Tag mTag; +}; + +ItemDeleteJob::ItemDeleteJob(const Item &item, QObject *parent) + : Job(new ItemDeleteJobPrivate(this), parent) +{ + Q_D(ItemDeleteJob); + + d->mItems << item; +} + +ItemDeleteJob::ItemDeleteJob(const Item::List &items, QObject *parent) + : Job(new ItemDeleteJobPrivate(this), parent) +{ + Q_D(ItemDeleteJob); + + d->mItems = items; +} + +ItemDeleteJob::ItemDeleteJob(const Collection &collection, QObject *parent) + : Job(new ItemDeleteJobPrivate(this), parent) +{ + Q_D(ItemDeleteJob); + + d->mCollection = collection; +} + +ItemDeleteJob::ItemDeleteJob(const Tag &tag, QObject *parent) + : Job(new ItemDeleteJobPrivate(this), parent) +{ + Q_D(ItemDeleteJob); + + d->mTag = tag; +} + +ItemDeleteJob::~ItemDeleteJob() +{ +} + +Item::List ItemDeleteJob::deletedItems() const +{ + Q_D(const ItemDeleteJob); + + return d->mItems; +} + +void ItemDeleteJob::doStart() +{ + Q_D(ItemDeleteJob); + + try { + d->sendCommand(Protocol::DeleteItemsCommand( + d->mItems.isEmpty() ? Scope() : ProtocolHelper::entitySetToScope(d->mItems), + ProtocolHelper::commandContextToProtocol(d->mCollection, d->mTag, d->mItems))); + } catch (const Akonadi::Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } +} + +bool ItemDeleteJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::DeleteItems) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +#include "moc_itemdeletejob.cpp" diff --git a/src/core/jobs/itemdeletejob.h b/src/core/jobs/itemdeletejob.h new file mode 100644 index 0000000..b1e8fb2 --- /dev/null +++ b/src/core/jobs/itemdeletejob.h @@ -0,0 +1,151 @@ +/* + 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. +*/ + +#ifndef AKONADI_ITEMDELETEJOB_H +#define AKONADI_ITEMDELETEJOB_H + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class ItemDeleteJobPrivate; + +/** + * @short Job that deletes items from the Akonadi storage. + * + * This job removes the given items from the Akonadi storage. + * + * Example: + * + * @code + * + * const Akonadi::Item item = ... + * + * ItemDeleteJob *job = new ItemDeleteJob(item); + * connect(job, SIGNAL(result(KJob*)), this, SLOT(deletionResult(KJob*))); + * + * @endcode + * + * Example: + * + * @code + * + * const Akonadi::Item::List items = ... + * + * ItemDeleteJob *job = new ItemDeleteJob(items); + * connect(job, SIGNAL(result(KJob*)), this, SLOT(deletionResult(KJob*))); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemDeleteJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new item delete job that deletes @p item. The item + * needs to have a unique identifier set. + * + * @internal + * For internal use only, the item may have a remote identifier set instead + * of a unique identifier. In this case, a collection or resource context + * needs to be selected using ResourceSelectJob. + * @endinternal + * + * @param item The item to delete. + * @param parent The parent object. + */ + explicit ItemDeleteJob(const Item &item, QObject *parent = Q_NULLPTR); + + /** + * Creates a new item delete job that deletes all items in the list + * @p items. Each item needs to have a unique identifier set. These items + * can be located in any collection. + * + * @internal + * For internal use only, the items may have remote identifiers set instead + * of unique identifiers. In this case, a collection or resource context + * needs to be selected using ResourceSelectJob. + * @endinternal + * + * @param items The items to delete. + * @param parent The parent object. + * + * @since 4.3 + */ + explicit ItemDeleteJob(const Item::List &items, QObject *parent = Q_NULLPTR); + + /** + * Creates a new item delete job that deletes all items in the collection + * @p collection. The collection needs to have a unique identifier set. + * + * @internal + * For internal use only, the collection may have a remote identifier set + * instead of a unique identifier. In this case, a resource context needs + * to be selected using ResourceSelectJob. + * @endinternal + * + * @param collection The collection which content should be deleted. + * @param parent The parent object. + * + * @since 4.3 + */ + explicit ItemDeleteJob(const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Creates a new item delete job that deletes all items that have assigned + * the tag @p tag. + * + * @param tag The tag which content should be deleted. + * @param parent The parent object. + * + * @since 4.14 + */ + explicit ItemDeleteJob(const Tag &tag, QObject *parent = Q_NULLPTR); + + /** + * Destroys the item delete job. + */ + ~ItemDeleteJob(); + + /** + * Returns the items passed on in the constructor. + * @since 4.4 + */ + Item::List deletedItems() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(ItemDeleteJob) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/itemfetchjob.cpp b/src/core/jobs/itemfetchjob.cpp new file mode 100644 index 0000000..2e2b841 --- /dev/null +++ b/src/core/jobs/itemfetchjob.cpp @@ -0,0 +1,275 @@ +/* + 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 "itemfetchjob.h" + +#include "attributefactory.h" +#include "collection.h" +#include "itemfetchscope.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "session_p.h" +#include "private/protocol_p.h" + +#include + +#include +#include + +using namespace Akonadi; + +class Akonadi::ItemFetchJobPrivate : public JobPrivate +{ +public: + ItemFetchJobPrivate(ItemFetchJob *parent) + : JobPrivate(parent) + , mEmitTimer(0) + , mValuePool(0) + , mCount(0) + { + mCollection = Collection::root(); + mDeliveryOptions = ItemFetchJob::Default; + } + + ~ItemFetchJobPrivate() + { + delete mValuePool; + } + + void init() + { + Q_Q(ItemFetchJob); + mEmitTimer = new QTimer(q); + mEmitTimer->setSingleShot(true); + mEmitTimer->setInterval(100); + q->connect(mEmitTimer, SIGNAL(timeout()), q, SLOT(timeout())); + } + + void aboutToFinish() Q_DECL_OVERRIDE { + timeout(); + } + + void timeout() + { + Q_Q(ItemFetchJob); + + mEmitTimer->stop(); // in case we are called by result() + if (!mPendingItems.isEmpty()) { + if (!q->error()) { + emit q->itemsReceived(mPendingItems); + } + mPendingItems.clear(); + } + } + + QString jobDebuggingString() const Q_DECL_OVERRIDE /*Q_DECL_OVERRIDE*/ + { + if (mRequestedItems.isEmpty()) { + QString str = QStringLiteral("All items from collection %1").arg(mCollection.id()); + if (mFetchScope.fetchChangedSince().isValid()) { + str += QStringLiteral(" changed since %1").arg(mFetchScope.fetchChangedSince().toString()); + } + return str; + + } else { + try { + return QString(); //QString::fromLatin1(ProtocolHelper::entitySetToScope(mRequestedItems)); + } catch (const Exception &e) { + return QString::fromUtf8(e.what()); + } + } + } + + Q_DECLARE_PUBLIC(ItemFetchJob) + + Collection mCollection; + Tag mTag; + Item::List mRequestedItems; + Item::List mResultItems; + ItemFetchScope mFetchScope; + Item::List mPendingItems; // items pending for emitting itemsReceived() + QTimer *mEmitTimer; + ProtocolHelperValuePool *mValuePool; + ItemFetchJob::DeliveryOptions mDeliveryOptions; + int mCount; +}; + +ItemFetchJob::ItemFetchJob(const Collection &collection, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + + d->init(); + d->mCollection = collection; + d->mValuePool = new ProtocolHelperValuePool; // only worth it for lots of results +} + +ItemFetchJob::ItemFetchJob(const Item &item, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + + d->init(); + d->mRequestedItems.append(item); +} + +ItemFetchJob::ItemFetchJob(const Akonadi::Item::List &items, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + + d->init(); + d->mRequestedItems = items; +} + +ItemFetchJob::ItemFetchJob(const QList &items, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + + d->init(); + foreach (Item::Id id, items) { + d->mRequestedItems.append(Item(id)); + } +} + +ItemFetchJob::ItemFetchJob(const Tag &tag, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + + d->init(); + d->mTag = tag; + d->mValuePool = new ProtocolHelperValuePool; +} + +ItemFetchJob::~ItemFetchJob() +{ +} + +void ItemFetchJob::doStart() +{ + Q_D(ItemFetchJob); + + try { + d->sendCommand(Protocol::FetchItemsCommand( + d->mRequestedItems.isEmpty() ? Scope() : ProtocolHelper::entitySetToScope(d->mRequestedItems), + ProtocolHelper::commandContextToProtocol(d->mCollection, d->mTag, d->mRequestedItems), + ProtocolHelper::itemFetchScopeToProtocol(d->mFetchScope))); + } catch (const Akonadi::Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } +} + +bool ItemFetchJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(ItemFetchJob); + + if (!response.isResponse() || response.type() != Protocol::Command::FetchItems) { + return Job::doHandleResponse(tag, response); + } + + Protocol::FetchItemsResponse resp(response); + // Invalid ID marks the last part of the response + if (resp.id() < 0) { + return true; + } + + const Item item = ProtocolHelper::parseItemFetchResult(resp, d->mValuePool); + if (!item.isValid()) { + return false; + } + + d->mCount++; + + if (d->mDeliveryOptions & ItemGetter) { + d->mResultItems.append(item); + } + + if (d->mDeliveryOptions & EmitItemsInBatches) { + d->mPendingItems.append(item); + if (!d->mEmitTimer->isActive()) { + d->mEmitTimer->start(); + } + } else if (d->mDeliveryOptions & EmitItemsIndividually) { + emit itemsReceived(Item::List() << item); + } + + return false; +} + +Item::List ItemFetchJob::items() const +{ + Q_D(const ItemFetchJob); + + return d->mResultItems; +} + +void ItemFetchJob::clearItems() +{ + Q_D(ItemFetchJob); + + d->mResultItems.clear(); +} + +void ItemFetchJob::setFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(ItemFetchJob); + + d->mFetchScope = fetchScope; +} + +ItemFetchScope &ItemFetchJob::fetchScope() +{ + Q_D(ItemFetchJob); + + return d->mFetchScope; +} + +void ItemFetchJob::setCollection(const Akonadi::Collection &collection) +{ + Q_D(ItemFetchJob); + + d->mCollection = collection; +} + +void ItemFetchJob::setDeliveryOption(DeliveryOptions options) +{ + Q_D(ItemFetchJob); + + d->mDeliveryOptions = options; +} + +ItemFetchJob::DeliveryOptions ItemFetchJob::deliveryOptions() const +{ + Q_D(const ItemFetchJob); + + return d->mDeliveryOptions; +} + +int ItemFetchJob::count() const +{ + Q_D(const ItemFetchJob); + + return d->mCount; +} +#include "moc_itemfetchjob.cpp" diff --git a/src/core/jobs/itemfetchjob.h b/src/core/jobs/itemfetchjob.h new file mode 100644 index 0000000..b5a24ec --- /dev/null +++ b/src/core/jobs/itemfetchjob.h @@ -0,0 +1,265 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_ITEMFETCHJOB_H +#define AKONADI_ITEMFETCHJOB_H + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ + +class Collection; +class ItemFetchJobPrivate; +class ItemFetchScope; + +/** + * @short Job that fetches items from the Akonadi storage. + * + * This class is used to fetch items from the Akonadi storage. + * Which parts of the items (e.g. headers only, attachments or all) + * can be specified by the ItemFetchScope. + * + * Note that ItemFetchJob does not refresh the Akonadi storage from the + * backend; this is unnecessary due to the fact that backend updates + * automatically trigger an update to the Akonadi database whenever they occur + * (unless the resource is offline). + * + * Note that items can not be created in the root collection (Collection::root()) + * and therefore can not be fetched from there either. That is - an item fetch in + * the root collection will yield an empty list. + * + * + * Example: + * + * @code + * + * // Fetch all items with full payload from a collection + * + * const Collection collection = getCollection(); + * + * Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(collection); + * connect(job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*))); + * job->fetchScope().fetchFullPayload(); + * + * ... + * + * MyClass::jobFinished(KJob *job) + * { + * if (job->error()) { + * qDebug() << "Error occurred"; + * return; + * } + * + * Akonadi::ItemFetchJob *fetchJob = qobject_cast(job); + * + * const Akonadi::Item::List items = fetchJob->items(); + * foreach (const Akonadi::Item &item, items) { + * qDebug() << "Item ID:" << item.id(); + * } + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemFetchJob : public Job +{ + Q_OBJECT + Q_FLAGS(DeliveryOptions) +public: + /** + * Creates a new item fetch job that retrieves all items inside the given collection. + * + * @param collection The parent collection to fetch all items from. + * @param parent The parent object. + */ + explicit ItemFetchJob(const Collection &collection, QObject *parent = Q_NULLPTR); + + /** + * Creates a new item fetch job that retrieves the specified item. + * If the item has a uid set, this is used to identify the item on the Akonadi + * server. If only a remote identifier is available, that is used. + * However, as remote identifiers are not necessarily globally unique, you + * need to specify the collection to search in in that case, using + * setCollection(). + * + * @internal + * For internal use only when using remote identifiers, the resource search + * context can be set globally by ResourceSelectJob. + * @endinternal + * + * @param item The item to fetch. + * @param parent The parent object. + */ + explicit ItemFetchJob(const Item &item, QObject *parent = Q_NULLPTR); + + /** + * Creates a new item fetch job that retrieves the specified items. + * If the items have a uid set, this is used to identify the item on the Akonadi + * server. If only a remote identifier is available, that is used. + * However, as remote identifiers are not necessarily globally unique, you + * need to specify the collection to search in in that case, using + * setCollection(). + * + * @internal + * For internal use only when using remote identifiers, the resource search + * context can be set globally by ResourceSelectJob. + * @endinternal + * + * @param items The items to fetch. + * @param parent The parent object. + * @since 4.4 + */ + explicit ItemFetchJob(const Item::List &items, QObject *parent = Q_NULLPTR); + + /** + * Convenience ctor equivalent to ItemFetchJob(const Item::List &items, QObject *parent = Q_NULLPTR) + * @since 4.8 + */ + explicit ItemFetchJob(const QList &items, QObject *parent = Q_NULLPTR); + + /** + * Creates a new item fetch job that retrieves all items tagged with specified @p tag. + * + * @param tag The tag to fetch all items from. + * @param parent The parent object. + * + * @since 4.14 + */ + explicit ItemFetchJob(const Tag &tag, QObject *parent = Q_NULLPTR); + + /** + * Destroys the item fetch job. + */ + virtual ~ItemFetchJob(); + + /** + * Returns the fetched items. + * + * This returns an empty list when not using the ItemGetter DeliveryOption. + * + * @note The items are invalid before the result(KJob*) + * signal has been emitted or if an error occurred. + */ + Item::List items() const; + + /** + * Save memory by clearing the fetched items. + * @since 4.12 + */ + void clearItems(); + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + * @since 4.4 + */ + void setFetchScope(const ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + + /** + * Specifies the collection the item is in. + * This is only required when retrieving an item based on its remote id + * which might not be unique globally. + * + * @internal + * @see ResourceSelectJob (for internal use only) + * @endinternal + */ + void setCollection(const Collection &collection); + + enum DeliveryOption { + ItemGetter = 0x1, ///< items available through items() + EmitItemsIndividually = 0x2, ///< emitted via signal upon reception + EmitItemsInBatches = 0x4, ///< emitted via signal in bulk (collected and emitted delayed via timer) + Default = ItemGetter | EmitItemsInBatches + }; + Q_DECLARE_FLAGS(DeliveryOptions, DeliveryOption) + + /** + * Sets the mechanisms by which the items should be fetched + * @since 4.13 + */ + void setDeliveryOption(DeliveryOptions options); + + /** + * Returns the delivery options + * @since 4.13 + */ + DeliveryOptions deliveryOptions() const; + + /** + * Returns the total number of retrieved items. + * This works also without the ItemGetter DeliveryOption. + * @since 4.14 + */ + int count() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever new items have been fetched completely. + * + * @note This is an optimization; instead of waiting for the end of the job + * and calling items(), you can connect to this signal and get the items + * incrementally. + * + * @param items The fetched items. + */ + void itemsReceived(const Akonadi::Item::List &items); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(ItemFetchJob) + + //@cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void timeout()) + //@endcond +}; + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(Akonadi::ItemFetchJob::DeliveryOptions) + +#endif diff --git a/src/core/jobs/itemmodifyjob.cpp b/src/core/jobs/itemmodifyjob.cpp new file mode 100644 index 0000000..0127239 --- /dev/null +++ b/src/core/jobs/itemmodifyjob.cpp @@ -0,0 +1,413 @@ +/* + 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 "itemmodifyjob.h" +#include "itemmodifyjob_p.h" +#include "akonadicore_debug.h" + +#include "changemediator_p.h" +#include "collection.h" +#include "conflicthandler_p.h" +#include "item_p.h" +#include "itemserializer_p.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "gidextractor_p.h" + +#include + +using namespace Akonadi; + +ItemModifyJobPrivate::ItemModifyJobPrivate(ItemModifyJob *parent) + : JobPrivate(parent) + , mRevCheck(true) + , mIgnorePayload(false) + , mAutomaticConflictHandlingEnabled(true) + , mSilent(false) +{ +} + +void ItemModifyJobPrivate::setClean() +{ + mOperations.insert(Dirty); +} + +Protocol::PartMetaData ItemModifyJobPrivate::preparePart(const QByteArray &partName) +{ + ProtocolHelper::PartNamespace ns; // dummy + const QByteArray partLabel = ProtocolHelper::decodePartIdentifier(partName, ns); + if (!mParts.remove(partLabel)) { + // Error? + return Protocol::PartMetaData(); + } + + mPendingData.clear(); + int version = 0; + ItemSerializer::serialize(mItems.first(), partLabel, mPendingData, version); + + return Protocol::PartMetaData(partName, mPendingData.size(), version); +} + +void ItemModifyJobPrivate::conflictResolved() +{ + Q_Q(ItemModifyJob); + + q->setError(KJob::NoError); + q->setErrorText(QString()); + q->emitResult(); +} + +void ItemModifyJobPrivate::conflictResolveError(const QString &message) +{ + Q_Q(ItemModifyJob); + + q->setErrorText(q->errorText() + message); + q->emitResult(); +} + +void ItemModifyJobPrivate::doUpdateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + auto it = std::find_if(mItems.begin(), mItems.end(), + [&itemId](const Item & item) -> bool { + return item.id() == itemId; + }); + if (it != mItems.end() && (*it).revision() == oldRevision) { + (*it).setRevision(newRevision); + } +} + +QString ItemModifyJobPrivate::jobDebuggingString() const +{ + try { + return fullCommand().debugString(); + } catch (const Exception &e) { + return QString::fromUtf8(e.what()); + } +} + +void ItemModifyJobPrivate::setSilent(bool silent) +{ + mSilent = silent; +} + +ItemModifyJob::ItemModifyJob(const Item &item, QObject *parent) + : Job(new ItemModifyJobPrivate(this), parent) +{ + Q_D(ItemModifyJob); + + d->mItems.append(item); + d->mParts = item.loadedPayloadParts(); + + d->mOperations.insert(ItemModifyJobPrivate::RemoteId); + d->mOperations.insert(ItemModifyJobPrivate::RemoteRevision); +} + +ItemModifyJob::ItemModifyJob(const Akonadi::Item::List &items, QObject *parent) + : Job(new ItemModifyJobPrivate(this), parent) +{ + Q_ASSERT(!items.isEmpty()); + Q_D(ItemModifyJob); + d->mItems = items; + + // same as single item ctor + if (d->mItems.size() == 1) { + d->mParts = items.first().loadedPayloadParts(); + d->mOperations.insert(ItemModifyJobPrivate::RemoteId); + d->mOperations.insert(ItemModifyJobPrivate::RemoteRevision); + } else { + d->mIgnorePayload = true; + d->mRevCheck = false; + } +} + +ItemModifyJob::~ItemModifyJob() +{ +} + +Protocol::Command ItemModifyJobPrivate::fullCommand() const +{ + Protocol::ModifyItemsCommand cmd; + + const Akonadi::Item item = mItems.first(); + QList changes; + foreach (int op, mOperations) { + switch (op) { + case ItemModifyJobPrivate::RemoteId: + if (!item.remoteId().isNull()) { + cmd.setRemoteId(item.remoteId()); + } + break; + case ItemModifyJobPrivate::Gid: { + const QString gid = GidExtractor::getGid(item); + if (!gid.isNull()) { + cmd.setGid(gid); + } + break; + } + case ItemModifyJobPrivate::RemoteRevision: + if (!item.remoteRevision().isNull()) { + cmd.setRemoteRevision(item.remoteRevision()); + } + break; + case ItemModifyJobPrivate::Dirty: + cmd.setDirty(false); + break; + } + } + + if (item.d_ptr->mClearPayload) { + cmd.setInvalidateCache(true); + } + if (mSilent) { + cmd.setNotify(true); + } + + if (item.d_ptr->mFlagsOverwritten) { + cmd.setFlags(item.flags()); + } else { + const auto addedFlags = ItemChangeLog::instance()->addedFlags(item.d_ptr); + if (!addedFlags.isEmpty()) { + cmd.setAddedFlags(addedFlags); + } + const auto deletedFlags = ItemChangeLog::instance()->deletedFlags(item.d_ptr); + if (!deletedFlags.isEmpty()) { + cmd.setRemovedFlags(deletedFlags); + } + } + + if (item.d_ptr->mTagsOverwritten) { + cmd.setTags(ProtocolHelper::entitySetToScope(item.tags())); + } else { + const auto addedTags = ItemChangeLog::instance()->addedTags(item.d_ptr); + if (!addedTags.isEmpty()) { + cmd.setAddedTags(ProtocolHelper::entitySetToScope(addedTags)); + } + const auto deletedTags = ItemChangeLog::instance()->deletedTags(item.d_ptr); + if (!deletedTags.isEmpty()) { + cmd.setRemovedTags(ProtocolHelper::entitySetToScope(deletedTags)); + } + } + + if (!mParts.isEmpty()) { + QSet parts; + parts.reserve(mParts.size()); + Q_FOREACH (const QByteArray &part, mParts) { + parts.insert(ProtocolHelper::encodePartIdentifier(ProtocolHelper::PartPayload, part)); + } + cmd.setParts(parts); + } + + const auto deletedAttributes = ItemChangeLog::instance()->deletedAttributes(item.d_ptr); + if (!deletedAttributes.isEmpty()) { + QSet removedParts; + removedParts.reserve(deletedAttributes.size()); + Q_FOREACH (const QByteArray &part, deletedAttributes) { + removedParts.insert("ATR:" + part); + } + cmd.setRemovedParts(removedParts); + } + + // nothing to do + if (cmd.modifiedParts() == Protocol::ModifyItemsCommand::None && mParts.isEmpty() && item.attributes().isEmpty()) { + return cmd; + } + + cmd.setItems(ProtocolHelper::entitySetToScope(mItems)); + if (mRevCheck && item.revision() >= 0) { + cmd.setOldRevision(item.revision()); + } + + if (item.d_ptr->mSizeChanged) { + cmd.setItemSize(item.size()); + } + + cmd.setAttributes(ProtocolHelper::attributesToProtocol(item)); + + return cmd; +} + +void ItemModifyJob::doStart() +{ + Q_D(ItemModifyJob); + + Protocol::ModifyItemsCommand command; + try { + command = d->fullCommand(); + } catch (const Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } + + if (command.modifiedParts() == Protocol::ModifyItemsCommand::None) { + emitResult(); + return; + } + + d->sendCommand(command); +} + +bool ItemModifyJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(ItemModifyJob); + + if (!response.isResponse() && response.type() == Protocol::Command::StreamPayload) { + const Protocol::StreamPayloadCommand streamCmd(response); + Protocol::StreamPayloadResponse streamResp; + if (streamCmd.request() == Protocol::StreamPayloadCommand::MetaData) { + streamResp.setMetaData(d->preparePart(streamCmd.payloadName())); + } else { + if (streamCmd.destination().isEmpty()) { + streamResp.setData(d->mPendingData); + } else { + QByteArray error; + if (!ProtocolHelper::streamPayloadToFile(streamCmd.destination(), d->mPendingData, error)) { + // TODO: Error? + } + } + } + d->sendCommand(tag, streamResp); + return false; + } + + if (response.isResponse() && response.type() == Protocol::Command::ModifyItems) { + Protocol::ModifyItemsResponse resp(response); + if (resp.errorCode()) { + setError(Unknown); + setErrorText(resp.errorMessage()); + return true; + } + + if (resp.errorMessage().contains(QStringLiteral("[LLCONFLICT]"))) { + if (d->mAutomaticConflictHandlingEnabled) { + ConflictHandler *handler = new ConflictHandler(ConflictHandler::LocalLocalConflict, this); + handler->setConflictingItems(d->mItems.first(), d->mItems.first()); + connect(handler, SIGNAL(conflictResolved()), SLOT(conflictResolved())); + connect(handler, SIGNAL(error(QString)), SLOT(conflictResolveError(QString))); + + QMetaObject::invokeMethod(handler, "start", Qt::QueuedConnection); + return true; + } + } + + if (resp.modificationDateTime().isValid()) { + Item &item = d->mItems.first(); + item.setModificationTime(resp.modificationDateTime()); + item.d_ptr->resetChangeLog(); + } else if (resp.id() > -1) { + auto it = std::find_if(d->mItems.begin(), d->mItems.end(), + [&resp](const Item & item) -> bool { + return item.id() == resp.id(); + }); + if (it == d->mItems.end()) { + qCDebug(AKONADICORE_LOG) << "Received STORE response for an item we did not modify: " << tag << response.debugString(); + return true; + } + + const int newRev = resp.newRevision(); + const int oldRev = (*it).revision(); + if (newRev >= oldRev && newRev >= 0) { + d->itemRevisionChanged((*it).id(), oldRev, newRev); + (*it).setRevision(newRev); + } + // There will be more responses, either for other modified items, + // or the final response with invalid ID, but with modification datetime + return false; + } + + Q_FOREACH (const Item &item, d->mItems) { + ChangeMediator::invalidateItem(item); + } + + return true; + } + + return Job::doHandleResponse(tag, response); +} + +void ItemModifyJob::setIgnorePayload(bool ignore) +{ + Q_D(ItemModifyJob); + + if (d->mIgnorePayload == ignore) { + return; + } + + d->mIgnorePayload = ignore; + if (d->mIgnorePayload) { + d->mParts = QSet(); + } else { + Q_ASSERT(!d->mItems.first().mimeType().isEmpty()); + d->mParts = d->mItems.first().loadedPayloadParts(); + } +} + +bool ItemModifyJob::ignorePayload() const +{ + Q_D(const ItemModifyJob); + + return d->mIgnorePayload; +} + +void ItemModifyJob::setUpdateGid(bool update) +{ + Q_D(ItemModifyJob); + if (update && !updateGid()) { + d->mOperations.insert(ItemModifyJobPrivate::Gid); + } else { + d->mOperations.remove(ItemModifyJobPrivate::Gid); + } +} + +bool ItemModifyJob::updateGid() const +{ + Q_D(const ItemModifyJob); + return d->mOperations.contains(ItemModifyJobPrivate::Gid); +} + +void ItemModifyJob::disableRevisionCheck() +{ + Q_D(ItemModifyJob); + + d->mRevCheck = false; +} + +void ItemModifyJob::disableAutomaticConflictHandling() +{ + Q_D(ItemModifyJob); + + d->mAutomaticConflictHandlingEnabled = false; +} + +Item ItemModifyJob::item() const +{ + Q_D(const ItemModifyJob); + Q_ASSERT(d->mItems.size() == 1); + + return d->mItems.first(); +} + +Item::List ItemModifyJob::items() const +{ + Q_D(const ItemModifyJob); + return d->mItems; +} + +#include "moc_itemmodifyjob.cpp" diff --git a/src/core/jobs/itemmodifyjob.h b/src/core/jobs/itemmodifyjob.h new file mode 100644 index 0000000..8a5afd5 --- /dev/null +++ b/src/core/jobs/itemmodifyjob.h @@ -0,0 +1,215 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_ITEMMODIFYJOB_H +#define AKONADI_ITEMMODIFYJOB_H + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ + +class ItemModifyJobPrivate; + +/** + * @short Job that modifies an existing item in the Akonadi storage. + * + * This job is used to writing back items to the Akonadi storage, after + * the user has changed them in any way. + * For performance reasons either the full item (including the full payload) + * can written back or only the meta data of the item. + * + * Example: + * + * @code + * + * // Fetch item with unique id 125 + * Akonadi::ItemFetchJob *fetchJob = new Akonadi::ItemFetchJob( Akonadi::Item( 125 ) ); + * connect( fetchJob, SIGNAL(result(KJob*)), SLOT(fetchFinished(KJob*)) ); + * + * ... + * + * MyClass::fetchFinished( KJob *job ) + * { + * if ( job->error() ) + * return; + * + * Akonadi::ItemFetchJob *fetchJob = qobject_cast( job ); + * + * Akonadi::Item item = fetchJob->items().at(0); + * + * // Set a custom flag + * item.setFlag( "\GotIt" ); + * + * // Store back modified item + * Akonadi::ItemModifyJob *modifyJob = new Akonadi::ItemModifyJob( item ); + * connect( modifyJob, SIGNAL(result(KJob*)), SLOT(modifyFinished(KJob*)) ); + * } + * + * MyClass::modifyFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Item modified successfully"; + * } + * + * @endcode + * + *

Conflict Resolution

+ + * When the job is executed, a check is made to ensure that the Item contained + * in the job is not older than the version of the Item already held in the + * Akonadi database. If it is older, a conflict resolution dialog is displayed + * for the user to choose which version of the Item to use, unless + * disableAutomaticConflictHandling() has been called to disable the dialog, or + * disableRevisionCheck() has been called to disable version checking + * altogether. + * + * The item version is checked by comparing the Item::revision() values in the + * job and in the database. To ensure that two successive ItemModifyJobs for + * the same Item work correctly, the revision number of the Item supplied to + * the second ItemModifyJob should be set equal to the Item's revision number + * on completion of the first ItemModifyJob. This can be obtained by, for + * example, calling item().revision() in the job's result slot. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemModifyJob : public Job +{ + friend class ResourceBase; + + Q_OBJECT + +public: + /** + * Creates a new item modify job. + * + * @param item The modified item object to store. + * @param parent The parent object. + */ + explicit ItemModifyJob(const Item &item, QObject *parent = Q_NULLPTR); + + /** + * Creates a new item modify job for bulk modifications. + * + * Using this is different from running a modification job per item. + * Use this when applying the same change to a set of items, such as a + * mass-change of item flags, not if you just want to store a bunch of + * randomly modified items. + * + * Currently the following modifications are supported: + * - flag changes + * + * @note Since this does not do payload modifications, it implies + * setIgnorePayload( true ) and disableRevisionCheck(). + * @param items The list of items to modify, must not be empty. + * @since 4.6 + */ + explicit ItemModifyJob(const Item::List &items, QObject *parent = Q_NULLPTR); + + /** + * Destroys the item modify job. + */ + virtual ~ItemModifyJob(); + + /** + * Sets whether the payload of the modified item shall be + * omitted from transmission to the Akonadi storage. + * The default is @c false, however it can be set for + * performance reasons. + * @param ignore ignores payload if set as @c true + */ + void setIgnorePayload(bool ignore); + + /** + * Returns whether the payload of the modified item shall be + * omitted from transmission to the Akonadi storage. + */ + bool ignorePayload() const; + + /** + * Sets whether the GID shall be updated either from the gid parameter or + * by extracting it from the payload. + * The default is @c false to avoid unnecessarily update the GID, + * as it should never change once set, and the ItemCreateJob already sets it. + * @param update update the GID if set as @c true + * + * @note If disabled the GID will not be updated, but still be used for identification of the item. + * @since 4.12 + */ + void setUpdateGid(bool update); + + /** + * Returns whether the GID should be updated. + * @since 4.12 + */ + bool updateGid() const; + + /** + * Disables the check of the revision number. + * + * @note If disabled, no conflict detection is available. + */ + void disableRevisionCheck(); + + /** + * Returns the modified and stored item including the changed revision number. + * + * @note Use this method only when using the single item constructor. + */ + Item item() const; + + /** + * Returns the modified and stored items including the changed revision number. + * + * @since 4.6 + */ + Item::List items() const; + + /** + * Disables the automatic handling of conflicts. + * + * By default the item modify job will bring up a dialog to resolve + * a conflict that might happen when modifying an item. + * Calling this method will avoid that and the job returns with an + * error in case of a conflict. + * + * @since 4.6 + */ + void disableAutomaticConflictHandling(); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(ItemModifyJob) + + Q_PRIVATE_SLOT(d_func(), void conflictResolved()) + Q_PRIVATE_SLOT(d_func(), void conflictResolveError(const QString &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/itemmodifyjob_p.h b/src/core/jobs/itemmodifyjob_p.h new file mode 100644 index 0000000..9237a2d --- /dev/null +++ b/src/core/jobs/itemmodifyjob_p.h @@ -0,0 +1,78 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_ITEMMODIFYJOB_P_H +#define AKONADI_ITEMMODIFYJOB_P_H + +#include "akonadicore_export.h" +#include "job_p.h" + +namespace Akonadi +{ + +namespace Protocol +{ +class PartMetaData; +class Command; +} + +/** + * @internal + */ +class AKONADICORE_EXPORT ItemModifyJobPrivate : public JobPrivate +{ +public: + enum Operation { + RemoteId, + RemoteRevision, + Gid, + Dirty + }; + + ItemModifyJobPrivate(ItemModifyJob *parent); + + void setClean(); + Protocol::PartMetaData preparePart(const QByteArray &partName); + + void conflictResolved(); + void conflictResolveError(const QString &message); + + void doUpdateItemRevision(Item::Id id, int oldRevision, int newRevision) Q_DECL_OVERRIDE; + + QString jobDebuggingString() const Q_DECL_OVERRIDE /*Q_DECL_OVERRIDE*/; + Protocol::Command fullCommand() const; + + void setSilent(bool silent); + + Q_DECLARE_PUBLIC(ItemModifyJob) + + QSet mOperations; + QByteArray mTag; + Item::List mItems; + bool mRevCheck; + QSet mParts; + QByteArray mPendingData; + bool mIgnorePayload; + bool mAutomaticConflictHandlingEnabled; + bool mSilent; +}; + +} + +#endif diff --git a/src/core/jobs/itemmovejob.cpp b/src/core/jobs/itemmovejob.cpp new file mode 100644 index 0000000..fceb14b --- /dev/null +++ b/src/core/jobs/itemmovejob.cpp @@ -0,0 +1,127 @@ +/* + 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 "itemmovejob.h" + +#include "collection.h" +#include "item.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +#include + +using namespace Akonadi; + +class Akonadi::ItemMoveJobPrivate : public Akonadi::JobPrivate +{ +public: + ItemMoveJobPrivate(ItemMoveJob *parent) + : JobPrivate(parent) + { + } + + Item::List items; + Collection destination; + Collection source; + + Q_DECLARE_PUBLIC(ItemMoveJob) +}; + +ItemMoveJob::ItemMoveJob(const Item &item, const Collection &destination, QObject *parent) + : Job(new ItemMoveJobPrivate(this), parent) +{ + Q_D(ItemMoveJob); + d->destination = destination; + d->items.append(item); +} + +ItemMoveJob::ItemMoveJob(const Item::List &items, const Collection &destination, QObject *parent) + : Job(new ItemMoveJobPrivate(this), parent) +{ + Q_D(ItemMoveJob); + d->destination = destination; + d->items = items; +} + +ItemMoveJob::ItemMoveJob(const Item::List &items, const Collection &source, const Collection &destination, QObject *parent) + : Job(new ItemMoveJobPrivate(this), parent) +{ + Q_D(ItemMoveJob); + d->source = source; + d->destination = destination; + d->items = items; +} + +ItemMoveJob::~ItemMoveJob() +{ +} + +void ItemMoveJob::doStart() +{ + Q_D(ItemMoveJob); + + if (d->items.isEmpty()) { + setError(Job::Unknown); + setErrorText(i18n("No objects specified for moving")); + emitResult(); + return; + } + + if (!d->destination.isValid() && d->destination.remoteId().isEmpty()) { + setError(Job::Unknown); + setErrorText(i18n("No valid destination specified")); + emitResult(); + return; + } + + try { + d->sendCommand(Protocol::MoveItemsCommand( + ProtocolHelper::entitySetToScope(d->items), + ProtocolHelper::commandContextToProtocol(d->source, Tag(), d->items), + ProtocolHelper::entityToScope(d->destination))); + } catch (const Akonadi::Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } +} + +bool ItemMoveJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::MoveItems) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +Collection ItemMoveJob::destinationCollection() const +{ + Q_D(const ItemMoveJob); + return d->destination; +} + +Item::List ItemMoveJob::items() const +{ + Q_D(const ItemMoveJob); + return d->items; +} + diff --git a/src/core/jobs/itemmovejob.h b/src/core/jobs/itemmovejob.h new file mode 100644 index 0000000..b9b53d5 --- /dev/null +++ b/src/core/jobs/itemmovejob.h @@ -0,0 +1,112 @@ +/* + 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_ITEMMOVEJOB_H +#define AKONADI_ITEMMOVEJOB_H + +#include "akonadicore_export.h" +#include "job.h" +#include "item.h" +namespace Akonadi +{ + +class Collection; +class ItemMoveJobPrivate; + +/** + * @short Job that moves an item into a different collection in the Akonadi storage. + * + * This job takes an item and moves it to a collection in the Akonadi storage. + * + * @code + * + * Akonadi::Item item = ... + * Akonadi::Collection collection = ... + * + * Akonadi::ItemMoveJob *job = new Akonadi::ItemMoveJob( item, collection ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(moveResult(KJob*)) ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemMoveJob : public Job +{ + Q_OBJECT + +public: + /** + * Move the given item into the given collection. + * + * @param item The item to move. + * @param destination The destination collection. + * @param parent The parent object. + */ + ItemMoveJob(const Item &item, const Collection &destination, QObject *parent = Q_NULLPTR); + + /** + * Move the given items into @p destination. + * + * @param items A list of items to move. + * @param destination The destination collection. + * @param parent The parent object. + */ + ItemMoveJob(const Item::List &items, const Collection &destination, QObject *parent = Q_NULLPTR); + + /** + * Move the given items from @p source to @p destination. + * + * @internal If the items are identified only by RID, then you MUST use this + * constructor to specify the source collection, otherwise the job will fail. + * RID-based moves are only allowed to resources. + * + * @since 4.14 + */ + ItemMoveJob(const Item::List &items, const Collection &source, const Collection &destination, QObject *parent = Q_NULLPTR); + + /** + * Destroys the item move job. + */ + ~ItemMoveJob(); + + /** + * Returns the destination collection. + * + * @since 4.7 + */ + Collection destinationCollection() const; + + /** + * Returns the list of items that where passed in the constructor. + * + * @since 4.7 + */ + Akonadi::Item::List items() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(ItemMoveJob) +}; + +} + +#endif diff --git a/src/core/jobs/itemsearchjob.cpp b/src/core/jobs/itemsearchjob.cpp new file mode 100644 index 0000000..df91f15 --- /dev/null +++ b/src/core/jobs/itemsearchjob.cpp @@ -0,0 +1,262 @@ +/* + Copyright (c) 2009 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. +*/ + +#include "itemsearchjob.h" + +#include "itemfetchscope.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "searchquery.h" +#include "private/protocol_p.h" + +#include +#include + +using namespace Akonadi; + +class Akonadi::ItemSearchJobPrivate : public JobPrivate +{ +public: + ItemSearchJobPrivate(ItemSearchJob *parent, const SearchQuery &query) + : JobPrivate(parent) + , mQuery(query) + , mRecursive(false) + , mRemote(false) + , mEmitTimer(0) + { + } + + void init() + { + Q_Q(ItemSearchJob); + mEmitTimer = new QTimer(q); + mEmitTimer->setSingleShot(true); + mEmitTimer->setInterval(100); + q->connect(mEmitTimer, SIGNAL(timeout()), q, SLOT(timeout())); + q->connect(q, SIGNAL(result(KJob*)), q, SLOT(timeout())); + } + + void timeout() + { + Q_Q(Akonadi::ItemSearchJob); + + mEmitTimer->stop(); // in case we are called by result() + if (!mPendingItems.isEmpty()) { + if (!q->error()) { + emit q->itemsReceived(mPendingItems); + } + mPendingItems.clear(); + } + } + QString jobDebuggingString() const Q_DECL_OVERRIDE + { + QStringList flags; + if (mRecursive) { + flags.append(QStringLiteral("recursive")); + } + if (mRemote) { + flags.append(QStringLiteral("remote")); + } + if (mCollections.isEmpty()) { + flags.append(QStringLiteral("all collections")); + } else { + flags.append(QStringLiteral("%1 collections").arg(mCollections.count())); + } + return QStringLiteral("%1,json=%2").arg(flags.join(QStringLiteral(",")), QString::fromUtf8(mQuery.toJSON())); + } + + Q_DECLARE_PUBLIC(ItemSearchJob) + + SearchQuery mQuery; + Collection::List mCollections; + QStringList mMimeTypes; + bool mRecursive; + bool mRemote; + ItemFetchScope mFetchScope; + + Item::List mItems; + Item::List mPendingItems; // items pending for emitting itemsReceived() + + QTimer *mEmitTimer; +}; + +QThreadStorage instances; + +static Session *defaultSearchSession() +{ + if (!instances.hasLocalData()) { + const QByteArray sessionName = Session::defaultSession()->sessionId() + "-SearchSession"; + instances.setLocalData(new Session(sessionName)); + } + return instances.localData(); +} + +static QObject *sessionForJob(QObject *parent) +{ + if (qobject_cast(parent) || qobject_cast(parent)) { + return parent; + } + return defaultSearchSession(); +} + +ItemSearchJob::ItemSearchJob(QObject* parent) + : Job(new ItemSearchJobPrivate(this, SearchQuery()), sessionForJob(parent)) +{ + Q_D(ItemSearchJob); + + d->init(); +} + + +ItemSearchJob::ItemSearchJob(const SearchQuery &query, QObject *parent) + : Job(new ItemSearchJobPrivate(this, query), sessionForJob(parent)) +{ + Q_D(ItemSearchJob); + + d->init(); +} + +ItemSearchJob::~ItemSearchJob() +{ +} + +void ItemSearchJob::setQuery(const SearchQuery &query) +{ + Q_D(ItemSearchJob); + + d->mQuery = query; +} + +void ItemSearchJob::setFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(ItemSearchJob); + + d->mFetchScope = fetchScope; +} + +ItemFetchScope &ItemSearchJob::fetchScope() +{ + Q_D(ItemSearchJob); + + return d->mFetchScope; +} + +void ItemSearchJob::setSearchCollections(const Collection::List &collections) +{ + Q_D(ItemSearchJob); + + d->mCollections = collections; +} + +Collection::List ItemSearchJob::searchCollections() const +{ + return d_func()->mCollections; +} + +void ItemSearchJob::setMimeTypes(const QStringList &mimeTypes) +{ + Q_D(ItemSearchJob); + + d->mMimeTypes = mimeTypes; +} + +QStringList ItemSearchJob::mimeTypes() const +{ + return d_func()->mMimeTypes; +} + +void ItemSearchJob::setRecursive(bool recursive) +{ + Q_D(ItemSearchJob); + + d->mRecursive = recursive; +} + +bool ItemSearchJob::isRecursive() const +{ + return d_func()->mRecursive; +} + +void ItemSearchJob::setRemoteSearchEnabled(bool enabled) +{ + Q_D(ItemSearchJob); + + d->mRemote = enabled; +} + +bool ItemSearchJob::isRemoteSearchEnabled() const +{ + return d_func()->mRemote; +} + +void ItemSearchJob::doStart() +{ + Q_D(ItemSearchJob); + + Protocol::SearchCommand cmd; + cmd.setMimeTypes(d->mMimeTypes); + if (!d->mCollections.isEmpty()) { + QVector ids; + ids.reserve(d->mCollections.size()); + Q_FOREACH (const Collection &col, d->mCollections) { + ids << col.id(); + } + cmd.setCollections(ids); + } + cmd.setRecursive(d->mRecursive); + cmd.setRemote(d->mRemote); + cmd.setQuery(QString::fromUtf8(d->mQuery.toJSON())); + cmd.setFetchScope(ProtocolHelper::itemFetchScopeToProtocol(d->mFetchScope)); + + d->sendCommand(cmd); +} + +bool ItemSearchJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(ItemSearchJob); + + if (response.isResponse() && response.type() == Protocol::Command::FetchItems) { + const Item item = ProtocolHelper::parseItemFetchResult(response); + if (!item.isValid()) { + return false; + } + d->mItems.append(item); + d->mPendingItems.append(item); + if (!d->mEmitTimer->isActive()) { + d->mEmitTimer->start(); + } + + return false; + } + + if (response.isResponse() && response.type() == Protocol::Command::Search) { + return true; + } + + return Job::doHandleResponse(tag, response); +} + +Item::List ItemSearchJob::items() const +{ + Q_D(const ItemSearchJob); + + return d->mItems; +} + +#include "moc_itemsearchjob.cpp" diff --git a/src/core/jobs/itemsearchjob.h b/src/core/jobs/itemsearchjob.h new file mode 100644 index 0000000..ebb7eab --- /dev/null +++ b/src/core/jobs/itemsearchjob.h @@ -0,0 +1,250 @@ +/* + Copyright (c) 2009 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_ITEMSEARCHJOB_H +#define AKONADI_ITEMSEARCHJOB_H + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" +#include "collection.h" + +#include + +namespace Akonadi +{ + +class ItemFetchScope; +class ItemSearchJobPrivate; +class SearchQuery; + +/** + * @short Job that searches for items in the Akonadi storage. + * + * This job searches for items that match a given search query and returns + * the list of matching item. + * + * @code + * + * SearchQuery query; + * query.addTerm( SearchTerm( "From", "user1@domain.example", SearchTerm::CondEqual ) ); + * query.addTerm( SearchTerm( "Date", QDateTime( QDate( 2014, 01, 27 ), QTime( 00, 00, 00 ) ), SearchTerm::CondGreaterThan ); + * + * Akonadi::ItemSearchJob *job = new Akonadi::ItemSearchJob( query ); + * job->fetchScope().fetchFullPayload(); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(searchResult(KJob*)) ); + * + * ... + * + * MyClass::searchResult( KJob *job ) + * { + * Akonadi::ItemSearchJob *searchJob = qobject_cast( job ); + * const Akonadi::Item::List items = searchJob->items(); + * foreach ( const Akonadi::Item &item, items ) { + * // extract the payload and do further stuff + * } + * } + * + * @endcode + * + * @author Tobias Koenig + * @since 4.4 + */ +class AKONADICORE_EXPORT ItemSearchJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates an invalid search job. + * + * @param query The search query. + * @param parent The parent object. + * @since 5.1 + */ + explicit ItemSearchJob(QObject *parent = Q_NULLPTR); + + /** + * Creates an item search job. + * + * @param query The search query. + * @param parent The parent object. + * @since 4.13 + */ + explicit ItemSearchJob(const SearchQuery &query, QObject *parent = Q_NULLPTR); + + /** + * Destroys the item search job. + */ + ~ItemSearchJob(); + + /** + * Sets the search @p query. + * + * @since 4.13 + */ + void setQuery(const SearchQuery &query); + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an matching item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(const ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + + /** + * Returns the items that matched the search query. + */ + Item::List items() const; + + /** + * Search only for items of given mime types. + * + * @since 4.13 + */ + void setMimeTypes(const QStringList &mimeTypes); + + /** + * Returns list of mime types to search in + * + * @since 4.13 + */ + QStringList mimeTypes() const; + + /** + * Search only in given collections. + * + * When recursive search is enabled, all child collections of each specified + * collection will be searched too + * + * By default all collections are be searched. + * + * @param collections Collections to search + * @since 4.13 + */ + void setSearchCollections(const Collection::List &collections); + + /** + * Returns list of collections to search. + * + * This list does not include child collections that will be searched when + * recursive search is enabled + * + * @since 4.13 + */ + Collection::List searchCollections() const; + + /** + * Sets whether the search should recurse into collections + * + * When set to true, all child collections of the specific collections will + * be search recursively. + * + * @param recursive Whether to search recursively + * @since 4.13 + */ + void setRecursive(bool recursive); + + /** + * Returns whether the search is recursive + * + * @since 4.13 + */ + bool isRecursive() const; + + /** + * Sets whether resources should be queried too. + * + * When set to true, Akonadi will search local indexed items and will also + * query resources that support server-side search, to forward the query + * to remote storage (for example using SEARCH feature on IMAP servers) and + * merge their results with results from local index. + * + * This is useful especially when searching resources, that don't fetch full + * payload by default, for example the IMAP resource, which only fetches headers + * by default and the body is fetched on demand, which means that emails that + * were not yet fully fetched cannot be indexed in local index, and thus cannot + * be searched. With remote search, even those emails can be included in search + * results. + * + * This feature is disabled by default. + * + * Results are streamed back to client as they are received from queried sources, + * so this job can take some time to finish, but will deliver initial results + * from local index fairly quickly. + * + * @param enabled Whether remote search is enabled + * @since 4.13 + */ + void setRemoteSearchEnabled(bool enabled); + + /** + * Returns whether remote search is enabled. + * + * @since 4.13 + */ + bool isRemoteSearchEnabled() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever new matching items have been fetched completely. + * + * @note This is an optimization, instead of waiting for the end of the job + * and calling items(), you can connect to this signal and get the items + * incrementally. + * + * @param items The matching items. + */ + void itemsReceived(const Akonadi::Item::List &items); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(ItemSearchJob) + + Q_PRIVATE_SLOT(d_func(), void timeout()) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/job.cpp b/src/core/jobs/job.cpp new file mode 100644 index 0000000..1761baf --- /dev/null +++ b/src/core/jobs/job.cpp @@ -0,0 +1,391 @@ +/* + Copyright (c) 2006 Tobias Koenig + 2006 Marc Mutz + 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 "job.h" +#include "job_p.h" +#include "akonadicore_debug.h" +#include "KDBusConnectionPool" +#include +#include "private/protocol_p.h" +#include "session.h" +#include "session_p.h" + +#include + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +static QDBusAbstractInterface *s_jobtracker = 0; + +//@cond PRIVATE +void JobPrivate::handleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_Q(Job); + + if (mCurrentSubJob) { + mCurrentSubJob->d_ptr->handleResponse(tag, response); + return; + } + + if (tag == mTag) { + if (response.isResponse()) { + Protocol::Response resp(response); + if (resp.isError()) { + q->setError(Job::Unknown); + q->setErrorText(resp.errorMessage()); + q->emitResult(); + return; + } + } + } + + if (mReadingFinished) { + qCWarning(AKONADICORE_LOG) << "Received response for a job that does not expect any more data, ignoring"; + Q_ASSERT(!mReadingFinished); + return; + } + + if (q->doHandleResponse(tag, response)) { + mReadingFinished = true; + QTimer::singleShot(0, q, SLOT(delayedEmitResult())); + } +} + +void JobPrivate::init(QObject *parent) +{ + Q_Q(Job); + + mParentJob = qobject_cast(parent); + mSession = qobject_cast(parent); + + if (!mSession) { + if (!mParentJob) { + mSession = Session::defaultSession(); + } else { + mSession = mParentJob->d_ptr->mSession; + } + } + + if (!mParentJob) { + mSession->d->addJob(q); + } else { + mParentJob->addSubjob(q); + } + + // if there's a job tracker running, tell it about the new job + if (!s_jobtracker) { + // Let's only check for the debugging console every 3 seconds, otherwise every single job + // makes a dbus call to the dbus daemon, doesn't help performance. + static QTime s_lastTime; + if (s_lastTime.isNull() || s_lastTime.elapsed() > 3000) { + if (s_lastTime.isNull()) { + s_lastTime.start(); + } + if (KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(QStringLiteral("org.kde.akonadiconsole"))) { + s_jobtracker = new QDBusInterface(QStringLiteral("org.kde.akonadiconsole"), + QStringLiteral("/jobtracker"), + QStringLiteral("org.freedesktop.Akonadi.JobTracker"), + KDBusConnectionPool::threadConnection(), 0); + } else { + s_lastTime.restart(); + } + } + // Note: we never reset s_jobtracker to 0 when a call fails; but if we did + // then we should restart s_lastTime. + } + QMetaObject::invokeMethod(q, "signalCreationToJobTracker", Qt::QueuedConnection); +} + +void JobPrivate::signalCreationToJobTracker() +{ + Q_Q(Job); + if (s_jobtracker) { + // We do these dbus calls manually, so as to avoid having to install (or copy) the console's + // xml interface document. Since this is purely a debugging aid, that seems preferable to + // publishing something not intended for public consumption. + // WARNING: for any signature change here, apply it to resourcescheduler.cpp too + QList argumentList; + argumentList << QLatin1String(mSession->sessionId()) + << QString::number(reinterpret_cast(q), 16) + << (mParentJob ? QString::number(reinterpret_cast(mParentJob), 16) : QString()) + << QString::fromLatin1(q->metaObject()->className()) + << jobDebuggingString(); + s_jobtracker->callWithArgumentList(QDBus::NoBlock, QStringLiteral("jobCreated"), argumentList); + } +} + +void JobPrivate::signalStartedToJobTracker() +{ + Q_Q(Job); + if (s_jobtracker) { + // if there's a job tracker running, tell it a job started + QList argumentList; + argumentList << QString::number(reinterpret_cast(q), 16); + s_jobtracker->callWithArgumentList(QDBus::NoBlock, QStringLiteral("jobStarted"), argumentList); + } +} + +void JobPrivate::aboutToFinish() +{ + // Dummy +} + +void JobPrivate::delayedEmitResult() +{ + Q_Q(Job); + if (q->hasSubjobs()) { + // We still have subjobs, wait for them to finish + mFinishPending = true; + } else { + aboutToFinish(); + q->emitResult(); + } +} + +void JobPrivate::startQueued() +{ + Q_Q(Job); + mStarted = true; + + emit q->aboutToStart(q); + q->doStart(); + QTimer::singleShot(0, q, SLOT(startNext())); + QMetaObject::invokeMethod(q, "signalStartedToJobTracker", Qt::QueuedConnection); +} + +void JobPrivate::lostConnection() +{ + Q_Q(Job); + + if (mCurrentSubJob) { + mCurrentSubJob->d_ptr->lostConnection(); + } else { + q->setError(Job::ConnectionFailed); + q->emitResult(); + } +} + +void JobPrivate::slotSubJobAboutToStart(Job *job) +{ + Q_ASSERT(mCurrentSubJob == 0); + mCurrentSubJob = job; +} + +void JobPrivate::startNext() +{ + Q_Q(Job); + + if (mStarted && !mCurrentSubJob && q->hasSubjobs()) { + Job *job = qobject_cast(q->subjobs().at(0)); + Q_ASSERT(job); + job->d_ptr->startQueued(); + } else if (mFinishPending && !q->hasSubjobs()) { + // The last subjob we've been waiting for has finished, emitResult() finally + QTimer::singleShot(0, q, SLOT(delayedEmitResult())); + } +} + +qint64 JobPrivate::newTag() +{ + if (mParentJob) { + mTag = mParentJob->d_ptr->newTag(); + } else { + mTag = mSession->d->nextTag(); + } + return mTag; +} + +qint64 JobPrivate::tag() const +{ + return mTag; +} + +void JobPrivate::sendCommand(qint64 tag, const Protocol::Command &cmd) +{ + if (mParentJob) { + mParentJob->d_ptr->sendCommand(tag, cmd); + } else { + mSession->d->sendCommand(tag, cmd); + }; +} + +void JobPrivate::sendCommand(const Protocol::Command &cmd) +{ + sendCommand(newTag(), cmd); +} + +void JobPrivate::itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + mSession->d->itemRevisionChanged(itemId, oldRevision, newRevision); +} + +void JobPrivate::updateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + Q_Q(Job); + foreach (KJob *j, q->subjobs()) { + Akonadi::Job *job = qobject_cast(j); + if (job) { + job->d_ptr->updateItemRevision(itemId, oldRevision, newRevision); + } + } + doUpdateItemRevision(itemId, oldRevision, newRevision); +} + +void JobPrivate::doUpdateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + Q_UNUSED(itemId); + Q_UNUSED(oldRevision); + Q_UNUSED(newRevision); +} + +int JobPrivate::protocolVersion() const +{ + return mSession->d->protocolVersion; +} +//@endcond + +Job::Job(QObject *parent) + : KCompositeJob(parent) + , d_ptr(new JobPrivate(this)) +{ + d_ptr->init(parent); +} + +Job::Job(JobPrivate *dd, QObject *parent) + : KCompositeJob(parent) + , d_ptr(dd) +{ + d_ptr->init(parent); +} + +Job::~Job() +{ + delete d_ptr; + + // if there is a job tracer listening, tell it the job is done now + if (s_jobtracker) { + QList argumentList; + argumentList << QString::number(reinterpret_cast(this), 16) + << errorString(); + s_jobtracker->callWithArgumentList(QDBus::NoBlock, QStringLiteral("jobEnded"), argumentList); + } +} + +void Job::start() +{ +} + +bool Job::doKill() +{ + Q_D(Job); + if (d->mStarted) { + // the only way to cancel an already started job is reconnecting to the server + d->mSession->d->forceReconnect(); + } + d->mStarted = false; + return true; +} + +QString Job::errorString() const +{ + QString str; + switch (error()) { + case NoError: + break; + case ConnectionFailed: + str = i18n("Cannot connect to the Akonadi service."); + break; + case ProtocolVersionMismatch: + str = i18n("The protocol version of the Akonadi server is incompatible. Make sure you have a compatible version installed."); + break; + case UserCanceled: + str = i18n("User canceled operation."); + break; + case Unknown: + return errorText(); + default: + str = i18n("Unknown error."); + break; + } + if (!errorText().isEmpty()) { + str += QStringLiteral(" (%1)").arg(errorText()); + } + return str; +} + +bool Job::addSubjob(KJob *job) +{ + bool rv = KCompositeJob::addSubjob(job); + if (rv) { + connect(job, SIGNAL(aboutToStart(Akonadi::Job*)), SLOT(slotSubJobAboutToStart(Akonadi::Job*))); + QTimer::singleShot(0, this, SLOT(startNext())); + } + return rv; +} + +bool Job::removeSubjob(KJob *job) +{ + bool rv = KCompositeJob::removeSubjob(job); + if (job == d_ptr->mCurrentSubJob) { + d_ptr->mCurrentSubJob = 0; + QTimer::singleShot(0, this, SLOT(startNext())); + } + return rv; +} + +bool Job::doHandleResponse(qint64 tag, const Protocol::Command &command) +{ + qCDebug(AKONADICORE_LOG) << this << "Unhandled response: " << tag << command.debugString(); + setError(Unknown); + setErrorText(i18n("Unexpected response")); + emitResult(); + return true; +} + +void Job::slotResult(KJob *job) +{ + if (d_ptr->mCurrentSubJob == job) { + // current job finished, start the next one + d_ptr->mCurrentSubJob = 0; + KCompositeJob::slotResult(job); + if (!job->error()) { + QTimer::singleShot(0, this, SLOT(startNext())); + } + } else { + // job that was still waiting for execution finished, probably canceled, + // so just remove it from the queue and move on without caring about + // its error code + KCompositeJob::removeSubjob(job); + } +} + +void Job::emitWriteFinished() +{ + d_ptr->mWriteFinished = true; + emit writeFinished(this); +} + +#include "moc_job.cpp" diff --git a/src/core/jobs/job.h b/src/core/jobs/job.h new file mode 100644 index 0000000..83e9ff4 --- /dev/null +++ b/src/core/jobs/job.h @@ -0,0 +1,236 @@ +/* + Copyright (c) 2006 Tobias Koenig + 2006 Marc Mutz + 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_JOB_H +#define AKONADI_JOB_H + +#include "akonadicore_export.h" + +#include + +class QString; + +namespace Akonadi +{ + +namespace Protocol +{ +class Command; +} + +class JobPrivate; +class Session; +class SessionPrivate; + +/** + * @short Base class for all actions in the Akonadi storage. + * + * This class encapsulates a request to the pim storage service, + * the code looks like + * + * @code + * + * Akonadi::Job *job = new Akonadi::SomeJob( some parameter ); + * connect( job, SIGNAL(result(KJob*)), + * this, SLOT(slotResult(KJob*)) ); + * + * @endcode + * + * The job is queued for execution as soon as the event loop is entered + * again. + * + * And the slotResult is usually at least: + * + * @code + * + * if ( job->error() ) { + * // handle error... + * } + * + * @endcode + * + * With the synchronous interface the code looks like + * + * @code + * Akonadi::SomeJob *job = new Akonadi::SomeJob( some parameter ); + * if ( !job->exec() ) { + * qDebug() << "Error:" << job->errorString(); + * } else { + * // do something + * } + * @endcode + * + * @warning Using the synchronous method is error prone, use this only + * if the asynchronous access is not possible. See the documentation of + * KJob::exec() for more details. + * + * Subclasses must reimplement doStart(). + * + * @note KJob-derived objects delete itself, it is thus not possible + * to create job objects on the stack! + * + * @author Volker Krause , Tobias Koenig , Marc Mutz + */ +class AKONADICORE_EXPORT Job : public KCompositeJob +{ + Q_OBJECT + + friend class Session; + friend class SessionPrivate; + +public: + /** + * Describes a list of jobs. + */ + typedef QList List; + + /** + * Describes the error codes that can be emitted by this class. + * Subclasses can provide additional codes, starting from UserError + * onwards + */ + enum Error { + ConnectionFailed = UserDefinedError, ///< The connection to the Akonadi server failed. + ProtocolVersionMismatch, ///< The server protocol version is too old or too new. + UserCanceled, ///< The user canceld this job. + Unknown, ///< Unknown error. + UserError = UserDefinedError + 42 ///< Starting point for error codes defined by sub-classes. + }; + + /** + * Creates a new job. + * + * If the parent object is a Job object, the new job will be a subjob of @p parent. + * If the parent object is a Session object, it will be used for server communication + * instead of the default session. + * + * @param parent The parent object, job or session. + */ + explicit Job(QObject *parent = Q_NULLPTR); + + /** + * Destroys the job. + */ + virtual ~Job(); + + /** + * Jobs are started automatically once entering the event loop again, no need + * to explicitly call this. + */ + void start() Q_DECL_OVERRIDE; + + /** + * Returns the error string, if there has been an error, an empty + * string otherwise. + */ + QString errorString() const Q_DECL_OVERRIDE; + +Q_SIGNALS: + /** + * This signal is emitted directly before the job will be started. + * + * @param job The started job. + */ + void aboutToStart(Akonadi::Job *job); + + /** + * This signal is emitted if the job has finished all write operations, ie. + * if this signal is emitted, the job guarantees to not call writeData() again. + * Do not emit this signal directly, call emitWriteFinished() instead. + * + * @param job This job. + * @see emitWriteFinished() + */ + void writeFinished(Akonadi::Job *job); + +protected: + /** + * This method must be reimplemented in the concrete jobs. It will be called + * after the job has been started and a connection to the Akonadi backend has + * been established. + */ + virtual void doStart() = 0; + + /** + * This method should be reimplemented in the concrete jobs in case you want + * to handle incoming data. It will be called on received data from the backend. + * The default implementation does nothing. + * + * @param tag The tag of the corresponding command, empty if this is an untagged response. + * @param response The received response + * + * @return Implementations should return true if the last response was processed and + * the job can emit result. Return false if more responses from server are expected. + */ + virtual bool doHandleResponse(qint64 tag, const Protocol::Command &response); + + /** + * Adds the given job as a subjob to this job. This method is automatically called + * if you construct a job using another job as parent object. + * The base implementation does the necessary setup to share the network connection + * with the backend. + * + * @param job The new subjob. + */ + bool addSubjob(KJob *job) Q_DECL_OVERRIDE; + + /** + * Removes the given subjob of this job. + * + * @param job The subjob to remove. + */ + bool removeSubjob(KJob *job) Q_DECL_OVERRIDE; + + /** + * Kills the execution of the job. + */ + bool doKill() Q_DECL_OVERRIDE; + + /** + * Call this method to indicate that this job will not call writeData() again. + * @see writeFinished() + */ + void emitWriteFinished(); + +protected Q_SLOTS: + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +protected: + //@cond PRIVATE + Job(JobPrivate *dd, QObject *parent); + JobPrivate *const d_ptr; + //@endcond + +private: + Q_DECLARE_PRIVATE(Job) + + //@cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void slotSubJobAboutToStart(Akonadi::Job *)) + Q_PRIVATE_SLOT(d_func(), void startNext()) + Q_PRIVATE_SLOT(d_func(), void signalCreationToJobTracker()) + Q_PRIVATE_SLOT(d_func(), void signalStartedToJobTracker()) + Q_PRIVATE_SLOT(d_func(), void delayedEmitResult()) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/job_p.h b/src/core/jobs/job_p.h new file mode 100644 index 0000000..b7d4576 --- /dev/null +++ b/src/core/jobs/job_p.h @@ -0,0 +1,138 @@ +/* + Copyright (c) 2007 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_JOB_P_H +#define AKONADI_JOB_P_H + +#include "session.h" +#include "item.h" + +namespace Akonadi +{ + +namespace Protocol +{ +class Command; +} + +/** + * @internal + */ +class JobPrivate +{ +public: + explicit JobPrivate(Job *parent) + : q_ptr(parent) + , mCurrentSubJob(0) + , mSession(0) + , mWriteFinished(false) + , mReadingFinished(false) + , mStarted(false) + , mFinishPending(false) + { + } + + virtual ~JobPrivate() + { + } + + void init(QObject *parent); + + void handleResponse(qint64 tag, const Protocol::Command &response); + void startQueued(); + void lostConnection(); + void slotSubJobAboutToStart(Akonadi::Job *job); + void startNext(); + void signalCreationToJobTracker(); + void signalStartedToJobTracker(); + void delayedEmitResult(); + /* + Returns a string to display in akonadi console's job tracker. E.g. item ID. + */ + virtual QString jobDebuggingString() const + { + return QString(); + } + /** + Returns a new unique command tag for communication with the backend. + */ + qint64 newTag(); + + /** + Return the tag used for the request. + */ + qint64 tag() const; + + /** + Sends the @p command to the backend + */ + void sendCommand(qint64 tag, const Protocol::Command &command); + + /** + * Same as calling JobPrivate::sendCommand(newTag(), command) + */ + void sendCommand(const Protocol::Command &command); + + /** + * Notify following jobs about item revision changes. + * This is used to avoid phantom conflicts between pipelined modify jobs on the same item. + * @param itemID the id of the item which has changed + * @param oldRevision the old item revision + * @param newRevision the new item revision + */ + void itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision); + + /** + * Propagate item revision changes to this job and its sub-jobs. + */ + void updateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision); + + /** + * Overwrite this if your job does operations with conflict detection and update + * the item revisions if your items are affected. The default implementation does nothing. + */ + virtual void doUpdateItemRevision(Akonadi::Item::Id, int oldRevision, int newRevision); + + /** + * This method is called right before result() and finished() signals are emitted. + * Overwrite this method in your job if you need to emit some signals or process + * some data before the job finishes. + * + * Default implementation does nothing. + */ + virtual void aboutToFinish(); + + int protocolVersion() const; + + Job *q_ptr; + Q_DECLARE_PUBLIC(Job) + + Job *mParentJob; + Job *mCurrentSubJob; + qint64 mTag; + Session *mSession; + bool mWriteFinished; + bool mReadingFinished; + bool mStarted; + bool mFinishPending; +}; + +} + +#endif diff --git a/src/core/jobs/kjobprivatebase.cpp b/src/core/jobs/kjobprivatebase.cpp new file mode 100644 index 0000000..7fa8102 --- /dev/null +++ b/src/core/jobs/kjobprivatebase.cpp @@ -0,0 +1,48 @@ +/* + Copyright (c) 2010 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 "kjobprivatebase_p.h" + +using namespace Akonadi; + +void KJobPrivateBase::start() +{ + const ServerManager::State serverState = ServerManager::state(); + + if (serverState == ServerManager::Running) { + doStart(); + return; + } + + connect(ServerManager::self(), &ServerManager::stateChanged, this, &KJobPrivateBase::serverStateChanged); + + if (serverState == ServerManager::NotRunning) { + ServerManager::start(); + } +} + +void KJobPrivateBase::serverStateChanged(Akonadi::ServerManager::State state) +{ + if (state == ServerManager::Running) { + disconnect(ServerManager::self(), &ServerManager::stateChanged, this, &KJobPrivateBase::serverStateChanged); + doStart(); + } +} + +#include "moc_kjobprivatebase_p.cpp" diff --git a/src/core/jobs/kjobprivatebase_p.h b/src/core/jobs/kjobprivatebase_p.h new file mode 100644 index 0000000..9f9023e --- /dev/null +++ b/src/core/jobs/kjobprivatebase_p.h @@ -0,0 +1,53 @@ +/* + Copyright (c) 2010 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_KJOBPRIVATEBASE_P_H +#define AKONADI_KJOBPRIVATEBASE_P_H + +#include + +#include "servermanager.h" + +namespace Akonadi +{ + +/** + * Base class for the private class of KJob but not Akonadi::Job based jobs that + * require the Akonadi server to be operational. + * Delays job execution until that is the case. + * @internal + */ +class KJobPrivateBase : public QObject +{ + Q_OBJECT + +public: + /** Call from KJob::start() reimplementation. */ + void start(); + + /** Reimplement and put here what was in KJob::start() before. */ + virtual void doStart() = 0; + +private Q_SLOTS: + void serverStateChanged(Akonadi::ServerManager::State state); +}; + +} + +#endif diff --git a/src/core/jobs/linkjob.cpp b/src/core/jobs/linkjob.cpp new file mode 100644 index 0000000..1715baf --- /dev/null +++ b/src/core/jobs/linkjob.cpp @@ -0,0 +1,59 @@ +/* + 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 "linkjob.h" + +#include "collection.h" +#include "job_p.h" +#include "linkjobimpl_p.h" + +using namespace Akonadi; + +class Akonadi::LinkJobPrivate : public LinkJobImpl +{ +public: + LinkJobPrivate(LinkJob *parent) + : LinkJobImpl(parent) + { + } +}; + +LinkJob::LinkJob(const Collection &collection, const Item::List &items, QObject *parent) + : Job(new LinkJobPrivate(this), parent) +{ + Q_D(LinkJob); + d->destination = collection; + d->objectsToLink = items; +} + +LinkJob::~LinkJob() +{ +} + +void LinkJob::doStart() +{ + Q_D(LinkJob); + d->sendCommand(Protocol::LinkItemsCommand::Link); +} + +bool LinkJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + return d_func()->handleResponse(tag, response); +} + diff --git a/src/core/jobs/linkjob.h b/src/core/jobs/linkjob.h new file mode 100644 index 0000000..b90932d --- /dev/null +++ b/src/core/jobs/linkjob.h @@ -0,0 +1,97 @@ +/* + 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_LINKJOB_H +#define AKONADI_LINKJOB_H + +#include "akonadicore_export.h" +#include "job.h" +#include "item.h" + +namespace Akonadi +{ + +class Collection; +class LinkJobPrivate; + +/** + * @short Job that links items inside the Akonadi storage. + * + * This job allows you to create references to a set of items in a virtual + * collection. + * + * Example: + * + * @code + * + * // Links the given items to the given virtual collection + * const Akonadi::Collection virtualCollection = ... + * const Akonadi::Item::List items = ... + * + * Akonadi::LinkJob *job = new Akonadi::LinkJob( virtualCollection, items ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Linked items successfully"; + * } + * + * @endcode + * + * @author Volker Krause + * @since 4.2 + * @see UnlinkJob + */ +class AKONADICORE_EXPORT LinkJob : public Job +{ + Q_OBJECT +public: + /** + * Creates the link job. + * + * The job will create references to the given items in the given collection. + * + * @param collection The collection in which the references should be created. + * @param items The items of which the references should be created. + * @param parent The parent object. + */ + LinkJob(const Collection &collection, const Item::List &items, QObject *parent = Q_NULLPTR); + + /** + * Destroys the link job. + */ + ~LinkJob(); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(LinkJob) + template friend class LinkJobImpl; +}; + +} + +#endif diff --git a/src/core/jobs/linkjobimpl_p.h b/src/core/jobs/linkjobimpl_p.h new file mode 100644 index 0000000..4b31071 --- /dev/null +++ b/src/core/jobs/linkjobimpl_p.h @@ -0,0 +1,87 @@ +/* + Copyright (c) 2008,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. +*/ + +#ifndef AKONADI_LINKJOBIMPL_P_H +#define AKONADI_LINKJOBIMPL_P_H + +#include "collection.h" +#include "item.h" +#include "job.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +#include +#include + +namespace Akonadi +{ + +/** Shared implementation details between item and collection move jobs. */ +template class LinkJobImpl : public JobPrivate +{ +public: + LinkJobImpl(Job *parent) + : JobPrivate(parent) + { + } + + inline void sendCommand(Protocol::LinkItemsCommand::Action action) + { + LinkJob *q = static_cast(q_func()); // Job would be enough already, but then we don't have access to the non-public stuff... + if (objectsToLink.isEmpty()) { + q->emitResult(); + return; + } + if (!destination.isValid() && destination.remoteId().isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("No valid destination specified")); + q->emitResult(); + return; + } + + try { + JobPrivate::sendCommand(Protocol::LinkItemsCommand(action, + ProtocolHelper::entitySetToScope(objectsToLink), + ProtocolHelper::entityToScope(destination))); + } catch (const std::exception &e) { + q->setError(Job::Unknown); + q->setErrorText(QString::fromUtf8(e.what())); + q->emitResult(); + return; + } + } + + inline bool handleResponse(qint64 tag, const Protocol::Command &response) + { + LinkJob *q = static_cast(q_func()); + if (!response.isResponse() || response.type() != Protocol::Command::LinkItems) { + return q->Job::doHandleResponse(tag, response); + } + + return true; + } + + Item::List objectsToLink; + Collection destination; +}; + +} + +#endif diff --git a/src/core/jobs/recursiveitemfetchjob.cpp b/src/core/jobs/recursiveitemfetchjob.cpp new file mode 100644 index 0000000..09e4efa --- /dev/null +++ b/src/core/jobs/recursiveitemfetchjob.cpp @@ -0,0 +1,133 @@ +/* + Copyright (c) 2009 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. +*/ + +#include "recursiveitemfetchjob.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" + +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN RecursiveItemFetchJob::Private +{ +public: + Private(const Collection &collection, const QStringList &mimeTypes, RecursiveItemFetchJob *parent) + : mParent(parent) + , mCollection(collection) + , mMimeTypes(mimeTypes) + , mFetchCount(0) + { + } + + void collectionFetchResult(KJob *job) + { + if (job->error()) { + mParent->emitResult(); + return; + } + + const CollectionFetchJob *fetchJob = qobject_cast(job); + + Collection::List collections = fetchJob->collections(); + collections.prepend(mCollection); + + foreach (const Collection &collection, collections) { + ItemFetchJob *itemFetchJob = new ItemFetchJob(collection, mParent); + itemFetchJob->setFetchScope(mFetchScope); + mParent->connect(itemFetchJob, SIGNAL(result(KJob*)), + mParent, SLOT(itemFetchResult(KJob*))); + + mFetchCount++; + } + } + + void itemFetchResult(KJob *job) + { + if (!job->error()) { + const ItemFetchJob *fetchJob = qobject_cast(job); + + if (!mMimeTypes.isEmpty()) { + foreach (const Item &item, fetchJob->items()) { + if (mMimeTypes.contains(item.mimeType())) { + mItems << item; + } + } + } else { + mItems << fetchJob->items(); + } + } + + mFetchCount--; + + if (mFetchCount == 0) { + mParent->emitResult(); + } + } + + RecursiveItemFetchJob *mParent; + Collection mCollection; + Item::List mItems; + ItemFetchScope mFetchScope; + QStringList mMimeTypes; + + int mFetchCount; +}; + +RecursiveItemFetchJob::RecursiveItemFetchJob(const Collection &collection, const QStringList &mimeTypes, QObject *parent) + : KJob(parent) + , d(new Private(collection, mimeTypes, this)) +{ +} + +RecursiveItemFetchJob::~RecursiveItemFetchJob() +{ + delete d; +} + +void RecursiveItemFetchJob::setFetchScope(const ItemFetchScope &fetchScope) +{ + d->mFetchScope = fetchScope; +} + +ItemFetchScope &RecursiveItemFetchJob::fetchScope() +{ + return d->mFetchScope; +} + +void RecursiveItemFetchJob::start() +{ + CollectionFetchJob *job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Recursive, this); + + if (!d->mMimeTypes.isEmpty()) { + job->fetchScope().setContentMimeTypes(d->mMimeTypes); + } + + connect(job, SIGNAL(result(KJob*)), this, SLOT(collectionFetchResult(KJob*))); +} + +Akonadi::Item::List RecursiveItemFetchJob::items() const +{ + return d->mItems; +} + +#include "moc_recursiveitemfetchjob.cpp" diff --git a/src/core/jobs/recursiveitemfetchjob.h b/src/core/jobs/recursiveitemfetchjob.h new file mode 100644 index 0000000..35606b4 --- /dev/null +++ b/src/core/jobs/recursiveitemfetchjob.h @@ -0,0 +1,155 @@ +/* + Copyright (c) 2009 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_RECURSIVEITEMFETCHJOB_H +#define AKONADI_RECURSIVEITEMFETCHJOB_H + +#include "akonadicore_export.h" +#include "item.h" + +#include + +namespace Akonadi +{ + +class Collection; +class ItemFetchScope; + +/** + * @short Job that fetches all items of a collection recursive. + * + * This job takes a collection as argument and returns a list of + * all items that are part of the passed collection and its child + * collections. The items to fetch can be filtered by mime types and + * the parts of the items that shall be fetched can + * be specified via an ItemFetchScope. + * + * Example: + * + * @code + * + * // Assume the following Akonadi storage tree structure: + * // + * // Root Collection + * // | + * // +- Contacts + * // | | + * // | +- Private + * // | | + * // | `- Business + * // | + * // `- Events + * // + * // Collection 'Contacts' has the ID 15, then the following code + * // returns all contact items from 'Contacts', 'Private' and 'Business'. + * + * const Akonadi::Collection contactsCollection( 15 ); + * const QStringList mimeTypes = QStringList() << KContacts::Addressee::mimeType(); + * + * Akonadi::RecursiveItemFetchJob *job = new Akonadi::RecursiveItemFetchJob( contactsCollection, mimeTypes ); + * job->fetchScope().fetchFullPayload(); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(fetchResult(KJob*)) ); + * + * job->start(); + * + * ... + * + * MyClass::fetchResult( KJob *job ) + * { + * Akonadi::RecursiveItemFetchJob *fetchJob = qobject_cast( job ); + * const Akonadi::Item::List items = fetchJob->items(); + * // do something with the items + * } + * + * @endcode + * + * @author Tobias Koenig + * @since 4.6 + */ +class AKONADICORE_EXPORT RecursiveItemFetchJob : public KJob +{ + Q_OBJECT + +public: + /** + * Creates a new recursive item fetch job. + * + * @param collection The collection that shall be fetched recursive. + * @param mimeTypes The list of mime types that will be used for filtering. + * @param parent The parent object. + */ + explicit RecursiveItemFetchJob(const Akonadi::Collection &collection, + const QStringList &mimeTypes, + QObject *parent = Q_NULLPTR); + + /** + * Destroys the recursive item fetch job. + */ + ~RecursiveItemFetchJob(); + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(const Akonadi::ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + Akonadi::ItemFetchScope &fetchScope(); + + /** + * Returns the list of fetched items. + */ + Akonadi::Item::List items() const; + + /** + * Starts the recursive item fetch job. + */ + void start() Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void collectionFetchResult(KJob *)) + Q_PRIVATE_SLOT(d, void itemFetchResult(KJob *)) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/relationcreatejob.cpp b/src/core/jobs/relationcreatejob.cpp new file mode 100644 index 0000000..995520f --- /dev/null +++ b/src/core/jobs/relationcreatejob.cpp @@ -0,0 +1,77 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "relationcreatejob.h" +#include "job_p.h" +#include "relation.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" +#include "akonadicore_debug.h" +#include + +using namespace Akonadi; + +struct Akonadi::RelationCreateJobPrivate : public JobPrivate { + RelationCreateJobPrivate(RelationCreateJob *parent) + : JobPrivate(parent) + { + } + + Relation mRelation; +}; + +RelationCreateJob::RelationCreateJob(const Akonadi::Relation &relation, QObject *parent) + : Job(new RelationCreateJobPrivate(this), parent) +{ + Q_D(RelationCreateJob); + d->mRelation = relation; +} + +void RelationCreateJob::doStart() +{ + Q_D(RelationCreateJob); + + if (!d->mRelation.isValid()) { + qCWarning(AKONADICORE_LOG) << "The relation is invalid"; + setError(Job::Unknown); + setErrorText(i18n("Failed to create relation.")); + emitResult(); + return; + } + + d->sendCommand(Protocol::ModifyRelationCommand(d->mRelation.left().id(), + d->mRelation.right().id(), + d->mRelation.type(), + d->mRelation.remoteId())); +} + +bool RelationCreateJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::ModifyRelation) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +Relation RelationCreateJob::relation() const +{ + Q_D(const RelationCreateJob); + return d->mRelation; +} diff --git a/src/core/jobs/relationcreatejob.h b/src/core/jobs/relationcreatejob.h new file mode 100644 index 0000000..38e3b1b --- /dev/null +++ b/src/core/jobs/relationcreatejob.h @@ -0,0 +1,63 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_RELATIONCREATEJOB_H +#define AKONADI_RELATIONCREATEJOB_H + +#include "job.h" + +namespace Akonadi +{ + +class Relation; +class RelationCreateJobPrivate; + +/** + * @short Job that creates a new relation in the Akonadi storage. + * @since 4.15 + */ +class AKONADICORE_EXPORT RelationCreateJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new relation create job. + * + * @param relation The relation to create. + * @param parent The parent object. + */ + explicit RelationCreateJob(const Relation &relation, QObject *parent = 0); + + /** + * Returns the relation. + */ + Relation relation() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(RelationCreateJob) +}; + +} + +#endif diff --git a/src/core/jobs/relationdeletejob.cpp b/src/core/jobs/relationdeletejob.cpp new file mode 100644 index 0000000..73b4f9e --- /dev/null +++ b/src/core/jobs/relationdeletejob.cpp @@ -0,0 +1,77 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "relationdeletejob.h" +#include "job_p.h" +#include "relation.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" +#include "akonadicore_debug.h" +#include + + +using namespace Akonadi; + +struct Akonadi::RelationDeleteJobPrivate : public JobPrivate { + RelationDeleteJobPrivate(RelationDeleteJob *parent) + : JobPrivate(parent) + { + } + + Relation mRelation; +}; + +RelationDeleteJob::RelationDeleteJob(const Akonadi::Relation &relation, QObject *parent) + : Job(new RelationDeleteJobPrivate(this), parent) +{ + Q_D(RelationDeleteJob); + d->mRelation = relation; +} + +void RelationDeleteJob::doStart() +{ + Q_D(RelationDeleteJob); + + if (!d->mRelation.isValid()) { + qCWarning(AKONADICORE_LOG) << "The relation is invalid"; + setError(Job::Unknown); + setErrorText(i18n("Failed to create relation.")); + emitResult(); + return; + } + + d->sendCommand(Protocol::RemoveRelationsCommand(d->mRelation.left().id(), + d->mRelation.right().id(), + d->mRelation.type())); +} + +bool RelationDeleteJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::RemoveRelations) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +Relation RelationDeleteJob::relation() const +{ + Q_D(const RelationDeleteJob); + return d->mRelation; +} diff --git a/src/core/jobs/relationdeletejob.h b/src/core/jobs/relationdeletejob.h new file mode 100644 index 0000000..575b3e1 --- /dev/null +++ b/src/core/jobs/relationdeletejob.h @@ -0,0 +1,63 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_RELATIONDELETEJOB_H +#define AKONADI_RELATIONDELETEJOB_H + +#include "job.h" + +namespace Akonadi +{ + +class Relation; +class RelationDeleteJobPrivate; + +/** + * @short Job that deletes a relation in the Akonadi storage. + * @since 4.15 + */ +class AKONADICORE_EXPORT RelationDeleteJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new relation delete job. + * + * @param relation The relation to delete. + * @param parent The parent object. + */ + explicit RelationDeleteJob(const Relation &relation, QObject *parent = 0); + + /** + * Returns the relation. + */ + Relation relation() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(RelationDeleteJob) +}; + +} + +#endif diff --git a/src/core/jobs/relationfetchjob.cpp b/src/core/jobs/relationfetchjob.cpp new file mode 100644 index 0000000..37f15c9 --- /dev/null +++ b/src/core/jobs/relationfetchjob.cpp @@ -0,0 +1,135 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "relationfetchjob.h" +#include "job_p.h" +#include "relation.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" +#include + + +using namespace Akonadi; + +class Akonadi::RelationFetchJobPrivate : public JobPrivate +{ +public: + RelationFetchJobPrivate(RelationFetchJob *parent) + : JobPrivate(parent) + , mEmitTimer(0) + { + } + + void init() + { + Q_Q(RelationFetchJob); + mEmitTimer = new QTimer(q); + mEmitTimer->setSingleShot(true); + mEmitTimer->setInterval(100); + q->connect(mEmitTimer, SIGNAL(timeout()), q, SLOT(timeout())); + } + + void aboutToFinish() Q_DECL_OVERRIDE { + timeout(); + } + + void timeout() + { + Q_Q(RelationFetchJob); + mEmitTimer->stop(); // in case we are called by result() + if (!mPendingRelations.isEmpty()) { + if (!q->error()) { + emit q->relationsReceived(mPendingRelations); + } + mPendingRelations.clear(); + } + } + + Q_DECLARE_PUBLIC(RelationFetchJob) + + Relation::List mResultRelations; + Relation::List mPendingRelations; // relation pending for emitting itemsReceived() + QTimer *mEmitTimer; + QVector mTypes; + QString mResource; + Relation mRequestedRelation; +}; + +RelationFetchJob::RelationFetchJob(const Relation &relation, QObject *parent) + : Job(new RelationFetchJobPrivate(this), parent) +{ + Q_D(RelationFetchJob); + d->init(); + d->mRequestedRelation = relation; +} + +RelationFetchJob::RelationFetchJob(const QVector &types, QObject *parent) + : Job(new RelationFetchJobPrivate(this), parent) +{ + Q_D(RelationFetchJob); + d->init(); + d->mTypes = types; +} + +void RelationFetchJob::doStart() +{ + Q_D(RelationFetchJob); + + d->sendCommand(Protocol::FetchRelationsCommand( + d->mRequestedRelation.left().id(), + d->mRequestedRelation.right().id(), + (d->mTypes.isEmpty() && !d->mRequestedRelation.type().isEmpty()) ? QVector() << d->mRequestedRelation.type() : d->mTypes, + d->mResource)); +} + +bool RelationFetchJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(RelationFetchJob); + + if (!response.isResponse() || response.type() != Protocol::Command::FetchRelations) { + return Job::doHandleResponse(tag, response); + } + + const Relation rel = ProtocolHelper::parseRelationFetchResult(response); + // Invalid response means there will be no more responses + if (!rel.isValid()) { + return true; + } + + d->mResultRelations.append(rel); + d->mPendingRelations.append(rel); + if (!d->mEmitTimer->isActive()) { + d->mEmitTimer->start(); + } + return false; +} + +Relation::List RelationFetchJob::relations() const +{ + Q_D(const RelationFetchJob); + return d->mResultRelations; +} + +void RelationFetchJob::setResource(const QString &identifier) +{ + Q_D(RelationFetchJob); + d->mResource = identifier; +} + +#include "moc_relationfetchjob.cpp" diff --git a/src/core/jobs/relationfetchjob.h b/src/core/jobs/relationfetchjob.h new file mode 100644 index 0000000..3786a85 --- /dev/null +++ b/src/core/jobs/relationfetchjob.h @@ -0,0 +1,80 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_RELATIONFETCHJOB_H +#define AKONADI_RELATIONFETCHJOB_H + +#include "job.h" +#include "relation.h" + +namespace Akonadi +{ + +class Relation; +class RelationFetchJobPrivate; + +/** + * @short Job that to fetch relations from Akonadi storage. + * @since 4.15 + */ +class AKONADICORE_EXPORT RelationFetchJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new relation fetch job. + * + * @param relation The relation to fetch. + * @param parent The parent object. + */ + explicit RelationFetchJob(const Relation &relation, QObject *parent = 0); + + explicit RelationFetchJob(const QVector &types, QObject *parent = 0); + + void setResource(const QString &identifier); + + /** + * Returns the relations. + */ + Relation::List relations() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever new relations have been fetched completely. + * + * @param relations The fetched relations. + */ + void relationsReceived(const Akonadi::Relation::List &relations); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(RelationFetchJob) + + //@cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void timeout()) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/resourceselectjob.cpp b/src/core/jobs/resourceselectjob.cpp new file mode 100644 index 0000000..c9dac9a --- /dev/null +++ b/src/core/jobs/resourceselectjob.cpp @@ -0,0 +1,62 @@ +/* + 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 "resourceselectjob_p.h" + +#include "job_p.h" +#include "private/imapparser_p.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::ResourceSelectJobPrivate : public JobPrivate +{ +public: + ResourceSelectJobPrivate(ResourceSelectJob *parent) + : JobPrivate(parent) + { + } + + QString resourceId; +}; + +ResourceSelectJob::ResourceSelectJob(const QString &identifier, QObject *parent) + : Job(new ResourceSelectJobPrivate(this), parent) +{ + Q_D(ResourceSelectJob); + d->resourceId = identifier; +} + +void ResourceSelectJob::doStart() +{ + Q_D(ResourceSelectJob); + + d->sendCommand(Protocol::SelectResourceCommand(d->resourceId)); +} + +bool ResourceSelectJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::SelectResource) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +#include "moc_resourceselectjob_p.cpp" diff --git a/src/core/jobs/resourceselectjob_p.h b/src/core/jobs/resourceselectjob_p.h new file mode 100644 index 0000000..1c7e910 --- /dev/null +++ b/src/core/jobs/resourceselectjob_p.h @@ -0,0 +1,108 @@ +/* + 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. +*/ + +#ifndef AKONADI_RESOURCESELECTJOB_P_H +#define AKONADI_RESOURCESELECTJOB_P_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class ResourceSelectJobPrivate; + +/** + * @internal + * + * @short Job that selects a resource context for remote identifier based operations. + * + * This job selects a resource context that is used whenever remote identifier + * based operations ( e.g. fetch items or collections by remote identifier ) are + * executed. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * // Find out the akonadi id of the item with the remote id 'd1627013c6d5a2e7bb58c12560c27047' + * // that is stored in the resource with identifier 'my_mail_resource' + * + * Session *m_resourceSession = new Session( "resourceSession" ); + * + * ResourceSelectJob *job = new ResourceSelectJob( "my_mail_resource", resourceSession ); + * + * connect( job, SIGNAL(result(KJob*)), SLOT(resourceSelected(KJob*)) ); + * ... + * + * void resourceSelected( KJob *job ) + * { + * if ( job->error() ) + * return; + * + * Item item; + * item.setRemoteIdentifier( "d1627013c6d5a2e7bb58c12560c27047" ); + * + * ItemFetchJob *fetchJob = new ItemFetchJob( item, m_resourceSession ); + * connect( fetchJob, SIGNAL(result(KJob*)), SLOT(itemFetched(KJob*)) ); + * } + * + * void itemFetched( KJob *job ) + * { + * if ( job->error() ) + * return; + * + * const Item item = job->items().at(0); + * + * qDebug() << "Remote id" << item.remoteId() << "has akonadi id" << item.id(); + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ResourceSelectJob : public Job +{ + Q_OBJECT +public: + /** + * Selects the specified resource for all following remote identifier + * based operations in the same session. + * + * @param identifier The resource identifier, or any empty string to reset + * the selection. + * @param parent The parent object. + */ + explicit ResourceSelectJob(const QString &identifier, QObject *parent = 0); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(ResourceSelectJob) + //@endcond PRIVATE +}; + +} + +#endif diff --git a/src/core/jobs/resourcesynchronizationjob.cpp b/src/core/jobs/resourcesynchronizationjob.cpp new file mode 100644 index 0000000..5a28c65 --- /dev/null +++ b/src/core/jobs/resourcesynchronizationjob.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2009 Volker Krause + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "resourcesynchronizationjob.h" +#include "KDBusConnectionPool" +#include "kjobprivatebase_p.h" +#include "servermanager.h" +#include "agentinstance.h" +#include "agentmanager.h" +#include "akonadicore_debug.h" + +#include + +#include +#include + +namespace Akonadi +{ + +class ResourceSynchronizationJobPrivate : public KJobPrivateBase +{ +public: + ResourceSynchronizationJobPrivate(ResourceSynchronizationJob *parent) + : q(parent) + , interface(0) + , safetyTimer(0) + , timeoutCount(60) + , collectionTreeOnly(false) + { + } + + void doStart() Q_DECL_OVERRIDE; + + ResourceSynchronizationJob *q; + AgentInstance instance; + QDBusInterface *interface; + QTimer *safetyTimer; + int timeoutCount; + bool collectionTreeOnly; + int timeoutCountLimit; + + void slotSynchronized(); + void slotTimeout(); +}; + +ResourceSynchronizationJob::ResourceSynchronizationJob(const AgentInstance &instance, QObject *parent) + : KJob(parent) + , d(new ResourceSynchronizationJobPrivate(this)) +{ + d->instance = instance; + d->safetyTimer = new QTimer(this); + connect(d->safetyTimer, SIGNAL(timeout()), SLOT(slotTimeout())); + d->safetyTimer->setInterval(10 * 1000); + d->safetyTimer->setSingleShot(false); +} + +ResourceSynchronizationJob::~ResourceSynchronizationJob() +{ + delete d; +} + +void ResourceSynchronizationJob::start() +{ + d->start(); +} + +void ResourceSynchronizationJob::setTimeoutCountLimit(int count) +{ + d->timeoutCountLimit = count; +} + +int ResourceSynchronizationJob::timeoutCountLimit() const +{ + return d->timeoutCountLimit; +} + + + +bool ResourceSynchronizationJob::collectionTreeOnly() const +{ + return d->collectionTreeOnly; +} + +void ResourceSynchronizationJob::setCollectionTreeOnly(bool b) +{ + d->collectionTreeOnly = b; +} + +void ResourceSynchronizationJobPrivate::doStart() +{ + if (!instance.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Invalid resource instance.")); + q->emitResult(); + return; + } + + interface = new QDBusInterface(ServerManager::agentServiceName(ServerManager::Resource, instance.identifier()), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.Akonadi.Resource"), + KDBusConnectionPool::threadConnection(), this); + if (collectionTreeOnly) { + connect(interface, SIGNAL(collectionTreeSynchronized()), q, SLOT(slotSynchronized())); + } else { + connect(interface, SIGNAL(synchronized()), q, SLOT(slotSynchronized())); + } + + if (interface->isValid()) { + if (collectionTreeOnly) { + instance.synchronizeCollectionTree(); + } else { + instance.synchronize(); + } + + safetyTimer->start(); + } else { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to obtain D-Bus interface for resource '%1'", instance.identifier())); + q->emitResult(); + return; + } +} + +void ResourceSynchronizationJobPrivate::slotSynchronized() +{ + if (collectionTreeOnly) { + q->disconnect(interface, SIGNAL(collectionTreeSynchronized()), q, SLOT(slotSynchronized())); + } else { + q->disconnect(interface, SIGNAL(synchronized()), q, SLOT(slotSynchronized())); + } + safetyTimer->stop(); + q->emitResult(); +} + +void ResourceSynchronizationJobPrivate::slotTimeout() +{ + instance = AgentManager::self()->instance(instance.identifier()); + timeoutCount++; + + if (timeoutCount > timeoutCountLimit) { + safetyTimer->stop(); + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Resource synchronization timed out.")); + q->emitResult(); + return; + } + + if (instance.status() == AgentInstance::Idle) { + // try again, we might have lost the synchronized()/synchronizedCollectionTree() signal + qCDebug(AKONADICORE_LOG) << "trying again to sync resource" << instance.identifier(); + if (collectionTreeOnly) { + instance.synchronizeCollectionTree(); + } else { + instance.synchronize(); + } + } +} + +AgentInstance ResourceSynchronizationJob::resource() const +{ + return d->instance; +} + +} + +#include "moc_resourcesynchronizationjob.cpp" diff --git a/src/core/jobs/resourcesynchronizationjob.h b/src/core/jobs/resourcesynchronizationjob.h new file mode 100644 index 0000000..a8c64c2 --- /dev/null +++ b/src/core/jobs/resourcesynchronizationjob.h @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2009 Volker Krause + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#ifndef AKONADI_RESOURCESYNCHRONIZATIONJOB_H +#define AKONADI_RESOURCESYNCHRONIZATIONJOB_H + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ + +class AgentInstance; +class ResourceSynchronizationJobPrivate; + +/** + * @short Job that synchronizes a resource. + * + * This job will trigger a resource to synchronize the backend it is + * responsible for (e.g. a local file or a groupware server) with the + * Akonadi storage. + * + * If you only want to trigger the synchronization without being + * interested in the result, using Akonadi::AgentInstance::synchronize() is enough. + * If you want to wait until it's finished, use this class. + * + * Example: + * + * @code + * using namespace Akonadi; + * + * const AgentInstance resource = AgentManager::self()->instance( "myresourceidentifier" ); + * + * ResourceSynchronizationJob *job = new ResourceSynchronizationJob( resource ); + * connect( job, SIGNAL(result(KJob*)), SLOT(synchronizationFinished(KJob*)) ); + * job->start(); + * + * @endcode + * + * @note This is a KJob, not an Akonadi::Job, so it won't auto-start! + * + * @author Volker Krause + * @since 4.4 + */ +class AKONADICORE_EXPORT ResourceSynchronizationJob : public KJob +{ + Q_OBJECT + +public: + /** + * Creates a new synchronization job for the given resource. + * + * @param instance The resource instance to synchronize. + */ + explicit ResourceSynchronizationJob(const AgentInstance &instance, QObject *parent = Q_NULLPTR); + + /** + * Destroys the synchronization job. + */ + ~ResourceSynchronizationJob(); + + /** + * Returns whether a full synchronization will be done, or just the collection tree (without items). + * The default is @c false, i.e. a full sync will be requested. + * + * @since 4.8 + */ + bool collectionTreeOnly() const; + + /** + * Sets the collectionTreeOnly property. + * + * @param collectionTreeOnly If set, only the collection tree will be synchronized. + * @since 4.8 + */ + void setCollectionTreeOnly(bool collectionTreeOnly); + + /** + * Returns the resource that has been synchronized. + */ + AgentInstance resource() const; + + /* reimpl */ + void start() Q_DECL_OVERRIDE; + + /* + * @since 5.1 + */ + void setTimeoutCountLimit(int count); + int timeoutCountLimit() const; + +private: + //@cond PRIVATE + ResourceSynchronizationJobPrivate *const d; + friend class ResourceSynchronizationJobPrivate; + + Q_PRIVATE_SLOT(d, void slotSynchronized()) + Q_PRIVATE_SLOT(d, void slotTimeout()) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/searchcreatejob.cpp b/src/core/jobs/searchcreatejob.cpp new file mode 100644 index 0000000..94ecd35 --- /dev/null +++ b/src/core/jobs/searchcreatejob.cpp @@ -0,0 +1,151 @@ + +/* + Copyright (c) 2007 Volker Krause + Copyright (c) 2014 Daniel Vrátil + + 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 "searchcreatejob.h" + +#include "collection.h" +#include "protocolhelper_p.h" +#include "job_p.h" +#include "searchquery.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::SearchCreateJobPrivate : public JobPrivate +{ +public: + SearchCreateJobPrivate(const QString &name, const SearchQuery &query, SearchCreateJob *parent) + : JobPrivate(parent) + , mName(name) + , mQuery(query) + , mRecursive(false) + , mRemote(false) + { + } + + QString mName; + SearchQuery mQuery; + QStringList mMimeTypes; + QVector mCollections; + bool mRecursive; + bool mRemote; + Collection mCreatedCollection; +}; + +SearchCreateJob::SearchCreateJob(const QString &name, const SearchQuery &searchQuery, QObject *parent) + : Job(new SearchCreateJobPrivate(name, searchQuery, this), parent) +{ +} + +SearchCreateJob::~SearchCreateJob() +{ +} + +void SearchCreateJob::setSearchCollections(const QVector &collections) +{ + Q_D(SearchCreateJob); + + d->mCollections = collections; +} + +QVector SearchCreateJob::searchCollections() const +{ + return d_func()->mCollections; +} + +void SearchCreateJob::setSearchMimeTypes(const QStringList &mimeTypes) +{ + Q_D(SearchCreateJob); + + d->mMimeTypes = mimeTypes; +} + +QStringList SearchCreateJob::searchMimeTypes() const +{ + return d_func()->mMimeTypes; +} + +void SearchCreateJob::setRecursive(bool recursive) +{ + Q_D(SearchCreateJob); + + d->mRecursive = recursive; +} + +bool SearchCreateJob::isRecursive() const +{ + return d_func()->mRecursive; +} + +void SearchCreateJob::setRemoteSearchEnabled(bool enabled) +{ + Q_D(SearchCreateJob); + + d->mRemote = enabled; +} + +bool SearchCreateJob::isRemoteSearchEnabled() const +{ + return d_func()->mRemote; +} + +void SearchCreateJob::doStart() +{ + Q_D(SearchCreateJob); + + Protocol::StoreSearchCommand cmd; + cmd.setName(d->mName); + cmd.setQuery(QString::fromUtf8(d->mQuery.toJSON())); + cmd.setMimeTypes(d->mMimeTypes); + cmd.setRecursive(d->mRecursive); + cmd.setRemote(d->mRemote); + if (!d->mCollections.isEmpty()) { + QVector ids; + ids.reserve(d->mCollections.size()); + Q_FOREACH (const Collection &col, d->mCollections) { + ids << col.id(); + } + cmd.setQueryCollections(ids); + } + + d->sendCommand(cmd); +} + +Akonadi::Collection SearchCreateJob::createdCollection() const +{ + Q_D(const SearchCreateJob); + return d->mCreatedCollection; +} + +bool SearchCreateJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(SearchCreateJob); + if (response.isResponse() && response.type() == Protocol::Command::FetchCollections) { + d->mCreatedCollection = ProtocolHelper::parseCollection(response); + return false; + } + + if (response.isResponse() && response.type() == Protocol::Command::StoreSearch) { + return true; + } + + return Job::doHandleResponse(tag, response); +} diff --git a/src/core/jobs/searchcreatejob.h b/src/core/jobs/searchcreatejob.h new file mode 100644 index 0000000..e09d453 --- /dev/null +++ b/src/core/jobs/searchcreatejob.h @@ -0,0 +1,189 @@ +/* + 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_SEARCHCREATEJOB_H +#define AKONADI_SEARCHCREATEJOB_H + +#include "akonadicore_export.h" +#include "job.h" +#include "collection.h" + +namespace Akonadi +{ + +class Collection; +class SearchQuery; +class SearchCreateJobPrivate; + +/** + * @short Job that creates a virtual/search collection in the Akonadi storage. + * + * This job creates so called virtual or search collections, which don't contain + * real data, but references to items that match a given search query. + * + * @code + * + * const QString name = "My search folder"; + * const QString query = "..."; + * + * Akonadi::SearchCreateJob *job = new Akonadi::SearchCreateJob( name, query ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * qDebug() << "Created search folder successfully"; + * const Collection searchCollection = job->createdCollection(); + * ... + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT SearchCreateJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a search create job + * + * @param name The name of the search collection. + * @param query The search query. + * @param parent The parent object. + * @since 4.13 + */ + SearchCreateJob(const QString &name, const SearchQuery &searchQuery, QObject *parent = Q_NULLPTR); + + /** + * Sets list of mime types of items that search results can contain + * + * @param mimeTypes Mime types of items to include in search + * @since 4.13 + */ + void setSearchMimeTypes(const QStringList &mimeTypes); + + /** + * Returns list of mime types that search results can contain + * + * @since 4.13 + */ + QStringList searchMimeTypes() const; + + /** + * Sets list of collections to search in. + * + * When an empty list is set (default value), the search will contain + * results from all collections that contain given mime types. + * + * @param collections Collections to search in, or an empty list to search all + * @since 4.13 + */ + void setSearchCollections(const QVector &collections); + + /** + * Returns list of collections to search in + * + * @since 4.13 + */ + QVector searchCollections() const; + + /** + * Sets whether resources should be queried too. + * + * When set to true, Akonadi will search local indexed items and will also + * query resources that support server-side search, to forward the query + * to remote storage (for example using SEARCH feature on IMAP servers) and + * merge their results with results from local index. + * + * This is useful especially when searching resources, that don't fetch full + * payload by default, for example the IMAP resource, which only fetches headers + * by default and the body is fetched on demand, which means that emails that + * were not yet fully fetched cannot be indexed in local index, and thus cannot + * be searched. With remote search, even those emails can be included in search + * results. + * + * This feature is disabled by default. + * + * @param enabled Whether remote search is enabled + * @since 4.13 + */ + void setRemoteSearchEnabled(bool enabled); + + /** + * Returns whether remote search is enabled. + * + * @since 4.13 + */ + bool isRemoteSearchEnabled() const; + + /** + * Sets whether the search should recurse into collections + * + * When set to true, all child collections of the specific collections will + * be search recursively. + * + * @param recursive Whether to search recursively + * @since 4.13 + */ + void setRecursive(bool recursive); + + /** + * Returns whether the search is recursive + * + * @since 4.13 + */ + bool isRecursive() const; + + /** + * Destroys the search create job. + */ + ~SearchCreateJob(); + + /** + * Returns the newly created search collection once the job finished successfully. Returns an invalid + * collection if the job has not yet finished or failed. + * + * @since 4.4 + */ + Collection createdCollection() const; + +protected: + /** + * Reimplemented from Akonadi::Job + */ + void doStart() Q_DECL_OVERRIDE; + + /** + * Reimplemented from Akonadi::Job + */ + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(SearchCreateJob) +}; + +} + +#endif diff --git a/src/core/jobs/searchresultjob.cpp b/src/core/jobs/searchresultjob.cpp new file mode 100644 index 0000000..8bd3984 --- /dev/null +++ b/src/core/jobs/searchresultjob.cpp @@ -0,0 +1,120 @@ +/* + Copyright (c) 2013 Daniel Vrátil + + 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 "searchresultjob_p.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "private/protocol_p.h" + +namespace Akonadi +{ + +class SearchResultJobPrivate : public Akonadi::JobPrivate +{ +public: + SearchResultJobPrivate(SearchResultJob *parent); + + QByteArray searchId; + Collection collection; + ImapSet uid; + QVector rid; +}; + +SearchResultJobPrivate::SearchResultJobPrivate(SearchResultJob *parent) + : JobPrivate(parent) +{ +} + +} + +using namespace Akonadi; + +SearchResultJob::SearchResultJob(const QByteArray &searchId, const Collection &collection, QObject *parent) + : Job(new SearchResultJobPrivate(this), parent) +{ + Q_D(SearchResultJob); + Q_ASSERT(collection.isValid()); + + d->searchId = searchId; + d->collection = collection; +} + +SearchResultJob::~SearchResultJob() +{ +} + +void SearchResultJob::setSearchId(const QByteArray &searchId) +{ + Q_D(SearchResultJob); + d->searchId = searchId; +} + +QByteArray SearchResultJob::searchId() const +{ + return d_func()->searchId; +} + +void SearchResultJob::setResult(const ImapSet &set) +{ + Q_D(SearchResultJob); + d->rid.clear(); + d->uid = set; +} + +void SearchResultJob::setResult(const QVector &ids) +{ + Q_D(SearchResultJob); + d->rid.clear(); + d->uid = ImapSet(); + d->uid.add(ids); +} + +void SearchResultJob::setResult(const QVector &remoteIds) +{ + Q_D(SearchResultJob); + d->uid = ImapSet(); + d->rid = remoteIds; +} + +void SearchResultJob::doStart() +{ + Q_D(SearchResultJob); + + Scope scope; + if (!d->rid.isEmpty()) { + QStringList ridSet; + ridSet.reserve(d->rid.size()); + Q_FOREACH (const QByteArray &rid, d->rid) { + ridSet << QString::fromUtf8(rid); + } + scope.setRidSet(ridSet); + } else { + scope.setUidSet(d->uid); + } + d->sendCommand(Protocol::SearchResultCommand(d->searchId, d->collection.id(), scope)); +} + +bool SearchResultJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::SearchResult) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/searchresultjob_p.h b/src/core/jobs/searchresultjob_p.h new file mode 100644 index 0000000..4253d86 --- /dev/null +++ b/src/core/jobs/searchresultjob_p.h @@ -0,0 +1,56 @@ +/* + Copyright (c) 2013 Daniel Vrátil + + 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_SEARCHRESULTJOB_H +#define AKONADI_SEARCHRESULTJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class SearchResultJobPrivate; +class ImapSet; +class Collection; + +class AKONADICORE_EXPORT SearchResultJob : public Akonadi::Job +{ + Q_OBJECT +public: + explicit SearchResultJob(const QByteArray &searchId, const Collection &collection, QObject *parent = 0); + virtual ~SearchResultJob(); + + void setSearchId(const QByteArray &searchId); + QByteArray searchId() const; + + void setResult(const ImapSet &set); + void setResult(const QVector &remoteIds); + void setResult(const QVector &ids); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(SearchResultJob) +}; +} + +#endif // AKONADI_SEARCHRESULTJOB_H diff --git a/src/core/jobs/specialcollectionsdiscoveryjob.cpp b/src/core/jobs/specialcollectionsdiscoveryjob.cpp new file mode 100644 index 0000000..857a59b --- /dev/null +++ b/src/core/jobs/specialcollectionsdiscoveryjob.cpp @@ -0,0 +1,77 @@ +/* + Copyright (c) 2013 David Faure + + 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 "specialcollectionsdiscoveryjob.h" +#include "specialcollectionattribute.h" +#include "collectionfetchscope.h" +#include "collectionfetchjob.h" +#include + +#include "akonadicore_debug.h" + +using namespace Akonadi; + +/** + @internal +*/ +class Akonadi::SpecialCollectionsDiscoveryJobPrivate +{ +public: + SpecialCollectionsDiscoveryJobPrivate(SpecialCollections *collections, const QStringList &mimeTypes) + : mSpecialCollections(collections) + , mMimeTypes(mimeTypes) + { + } + + SpecialCollections *mSpecialCollections; + QStringList mMimeTypes; +}; + +Akonadi::SpecialCollectionsDiscoveryJob::SpecialCollectionsDiscoveryJob(SpecialCollections *collections, const QStringList &mimeTypes, QObject *parent) + : KCompositeJob(parent) + , d(new SpecialCollectionsDiscoveryJobPrivate(collections, mimeTypes)) +{ +} + +Akonadi::SpecialCollectionsDiscoveryJob::~SpecialCollectionsDiscoveryJob() +{ + delete d; +} + +void Akonadi::SpecialCollectionsDiscoveryJob::start() +{ + CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + job->fetchScope().setContentMimeTypes(d->mMimeTypes); + addSubjob(job); +} + +void Akonadi::SpecialCollectionsDiscoveryJob::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; + } + Akonadi::CollectionFetchJob *fetchJob = qobject_cast(job); + foreach (const Akonadi::Collection &collection, fetchJob->collections()) { + if (collection.hasAttribute()) { + d->mSpecialCollections->registerCollection(collection.attribute()->collectionType(), collection); + } + } + emitResult(); +} diff --git a/src/core/jobs/specialcollectionsdiscoveryjob.h b/src/core/jobs/specialcollectionsdiscoveryjob.h new file mode 100644 index 0000000..345cb8a --- /dev/null +++ b/src/core/jobs/specialcollectionsdiscoveryjob.h @@ -0,0 +1,77 @@ +/* + Copyright (c) 2013 David Faure + + 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_SPECIALCOLLECTIONSDISCOVERYJOB_H +#define AKONADI_SPECIALCOLLECTIONSDISCOVERYJOB_H + +#include "akonadicore_export.h" +#include "collection.h" +#include "specialcollections.h" +#include + +namespace Akonadi +{ + +class SpecialCollectionsDiscoveryJobPrivate; + +/** + * @short A job to discover all SpecialCollections. + * + * The collections get registered into SpecialCollections. + * + * This class is not meant to be used directly but as a base class for type + * specific special collection request jobs. + * + * @author David Faure + * @since 4.11 +*/ +class AKONADICORE_EXPORT SpecialCollectionsDiscoveryJob : public KCompositeJob +{ + Q_OBJECT + +public: + + /** + * Destroys the special collections request job. + */ + ~SpecialCollectionsDiscoveryJob(); + + void start() Q_DECL_OVERRIDE; + +protected: + /** + * Creates a new special collections request job. + * + * @param collections The SpecialCollections object that shall be used. + * @param parent The parent object. + */ + explicit SpecialCollectionsDiscoveryJob(SpecialCollections *collections, const QStringList &mimeTypes, QObject *parent = Q_NULLPTR); + + /* reimpl */ + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + SpecialCollectionsDiscoveryJobPrivate *const d; + //@endcond +}; + +} // namespace Akonadi + +#endif // AKONADI_SPECIALCOLLECTIONSDISCOVERYJOB_H diff --git a/src/core/jobs/specialcollectionshelperjobs.cpp b/src/core/jobs/specialcollectionshelperjobs.cpp new file mode 100644 index 0000000..1528c58 --- /dev/null +++ b/src/core/jobs/specialcollectionshelperjobs.cpp @@ -0,0 +1,663 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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 "specialcollectionshelperjobs_p.h" + +#include "KDBusConnectionPool" +#include "specialcollectionattribute.h" +#include "specialcollections.h" +#include "servermanager.h" + +#include "agentinstance.h" +#include "agentinstancecreatejob.h" +#include "agentmanager.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "entitydisplayattribute.h" +#include "resourcesynchronizationjob.h" + +#include "akonadicore_debug.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#define LOCK_WAIT_TIMEOUT_SECONDS 30 + +using namespace Akonadi; + +// convenient methods to get/set the default resource id +static void setDefaultResourceId(KCoreConfigSkeleton *settings, const QString &value) +{ + KConfigSkeletonItem *item = settings->findItem(QStringLiteral("DefaultResourceId")); + Q_ASSERT(item); + item->setProperty(value); +} + +static QString defaultResourceId(KCoreConfigSkeleton *settings) +{ + const KConfigSkeletonItem *item = settings->findItem(QStringLiteral("DefaultResourceId")); + Q_ASSERT(item); + return item->property().toString(); +} + +static QString dbusServiceName() +{ + QString service = QStringLiteral("org.kde.pim.SpecialCollections"); + if (ServerManager::hasInstanceIdentifier()) { + return service + ServerManager::instanceIdentifier(); + } + return service; +} + +static QVariant::Type argumentType(const QMetaObject *mo, const QString &method) +{ + QMetaMethod m; + for (int i = 0; i < mo->methodCount(); ++i) { + const QString signature = QString::fromLatin1(mo->method(i).methodSignature()); + if (signature.startsWith(method)) { + m = mo->method(i); + } + } + + if (m.methodSignature().isEmpty()) { + return QVariant::Invalid; + } + + const QList argTypes = m.parameterTypes(); + if (argTypes.count() != 1) { + return QVariant::Invalid; + } + + return QVariant::nameToType(argTypes.first().constData()); +} + +// ===================== ResourceScanJob ============================ + +/** + @internal +*/ +class Q_DECL_HIDDEN Akonadi::ResourceScanJob::Private +{ +public: + Private(KCoreConfigSkeleton *settings, ResourceScanJob *qq); + + void fetchResult(KJob *job); // slot + + ResourceScanJob *const q; + + // Input: + QString mResourceId; + KCoreConfigSkeleton *mSettings; + + // Output: + Collection mRootCollection; + Collection::List mSpecialCollections; +}; + +ResourceScanJob::Private::Private(KCoreConfigSkeleton *settings, ResourceScanJob *qq) + : q(qq) + , mSettings(settings) +{ +} + +void ResourceScanJob::Private::fetchResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + return; + } + + CollectionFetchJob *fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + + Q_ASSERT(!mRootCollection.isValid()); + Q_ASSERT(mSpecialCollections.isEmpty()); + foreach (const Collection &collection, fetchJob->collections()) { + if (collection.parentCollection() == Collection::root()) { + if (mRootCollection.isValid()) { + qCWarning(AKONADICORE_LOG) << "Resource has more than one root collection. I don't know what to do."; + } else { + mRootCollection = collection; + } + } + + if (collection.hasAttribute()) { + mSpecialCollections.append(collection); + } + } + + qCDebug(AKONADICORE_LOG) << "Fetched root collection" << mRootCollection.id() + << "and" << mSpecialCollections.count() << "local folders" + << "(total" << fetchJob->collections().count() << "collections)."; + + if (!mRootCollection.isValid()) { + q->setError(Unknown); + q->setErrorText(i18n("Could not fetch root collection of resource %1.", mResourceId)); + q->emitResult(); + return; + } + + // We are done! + q->emitResult(); +} + +ResourceScanJob::ResourceScanJob(const QString &resourceId, KCoreConfigSkeleton *settings, QObject *parent) + : Job(parent) + , d(new Private(settings, this)) +{ + setResourceId(resourceId); +} + +ResourceScanJob::~ResourceScanJob() +{ + delete d; +} + +QString ResourceScanJob::resourceId() const +{ + return d->mResourceId; +} + +void ResourceScanJob::setResourceId(const QString &resourceId) +{ + d->mResourceId = resourceId; +} + +Akonadi::Collection ResourceScanJob::rootResourceCollection() const +{ + return d->mRootCollection; +} + +Akonadi::Collection::List ResourceScanJob::specialCollections() const +{ + return d->mSpecialCollections; +} + +void ResourceScanJob::doStart() +{ + if (d->mResourceId.isEmpty()) { + if (!qobject_cast(this)) { + qCCritical(AKONADICORE_LOG) << "No resource ID given."; + setError(Job::Unknown); + setErrorText(i18n("No resource ID given.")); + } + emitResult(); + return; + } + + CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection::root(), + CollectionFetchJob::Recursive, this); + fetchJob->fetchScope().setResource(d->mResourceId); + fetchJob->fetchScope().setIncludeStatistics(true); + fetchJob->fetchScope().setListFilter(CollectionFetchScope::Display); + connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(fetchResult(KJob*))); +} + +// ===================== DefaultResourceJob ============================ + +/** + @internal +*/ +class Akonadi::DefaultResourceJobPrivate +{ +public: + DefaultResourceJobPrivate(KCoreConfigSkeleton *settings, DefaultResourceJob *qq); + + void tryFetchResource(); + void resourceCreateResult(KJob *job); // slot + void resourceSyncResult(KJob *job); // slot + void collectionFetchResult(KJob *job); // slot + void collectionModifyResult(KJob *job); // slot + + DefaultResourceJob *const q; + KCoreConfigSkeleton *mSettings; + bool mResourceWasPreexisting; + int mPendingModifyJobs; + QString mDefaultResourceType; + QVariantMap mDefaultResourceOptions; + QList mKnownTypes; + QMap mNameForTypeMap; + QMap mIconForTypeMap; +}; + +DefaultResourceJobPrivate::DefaultResourceJobPrivate(KCoreConfigSkeleton *settings, DefaultResourceJob *qq) + : q(qq) + , mSettings(settings) + , mResourceWasPreexisting(true /* for safety, so as not to accidentally delete data */) + , mPendingModifyJobs(0) +{ +} + +void DefaultResourceJobPrivate::tryFetchResource() +{ + // Get the resourceId from config. Another instance might have changed it in the meantime. + mSettings->load(); + + const QString resourceId = defaultResourceId(mSettings); + + qCDebug(AKONADICORE_LOG) << "Read defaultResourceId" << resourceId << "from config."; + + const AgentInstance resource = AgentManager::self()->instance(resourceId); + if (resource.isValid()) { + // The resource exists; scan it. + mResourceWasPreexisting = true; + qCDebug(AKONADICORE_LOG) << "Found resource" << resourceId; + q->setResourceId(resourceId); + + CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, q); + fetchJob->fetchScope().setResource(resourceId); + fetchJob->fetchScope().setIncludeStatistics(true); + q->connect(fetchJob, SIGNAL(result(KJob*)), q, SLOT(collectionFetchResult(KJob*))); + } else { + // Try harder: maybe the default resource has been removed and another one added + // without updating the config file, in this case search for a resource + // of the same type and the default name + const AgentInstance::List resources = AgentManager::self()->instances(); + foreach (const AgentInstance &resource, resources) { + if (resource.type().identifier() == mDefaultResourceType) { + if (resource.name() == mDefaultResourceOptions.value(QStringLiteral("Name")).toString()) { + // found a matching one... + setDefaultResourceId(mSettings, resource.identifier()); + mSettings->save(); + mResourceWasPreexisting = true; + qCDebug(AKONADICORE_LOG) << "Found resource" << resource.identifier(); + q->setResourceId(resource.identifier()); + q->ResourceScanJob::doStart(); + return; + } + } + } + + // Create the resource. + mResourceWasPreexisting = false; + qCDebug(AKONADICORE_LOG) << "Creating maildir resource."; + const AgentType type = AgentManager::self()->type(mDefaultResourceType); + AgentInstanceCreateJob *job = new AgentInstanceCreateJob(type, q); + QObject::connect(job, SIGNAL(result(KJob*)), q, SLOT(resourceCreateResult(KJob*))); + job->start(); // non-Akonadi::Job + } +} + +void DefaultResourceJobPrivate::resourceCreateResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + //fail( i18n( "Failed to create the default resource (%1).", job->errorString() ) ); + q->setError(job->error()); + q->setErrorText(job->errorText()); + q->emitResult(); + return; + } + + AgentInstance agent; + + // Get the resource instance. + { + AgentInstanceCreateJob *createJob = qobject_cast(job); + Q_ASSERT(createJob); + agent = createJob->instance(); + setDefaultResourceId(mSettings, agent.identifier()); + qCDebug(AKONADICORE_LOG) << "Created maildir resource with id" << defaultResourceId(mSettings); + } + + const QString defaultId = defaultResourceId(mSettings); + + // Configure the resource. + { + agent.setName(mDefaultResourceOptions.value(QStringLiteral("Name")).toString()); + + QDBusInterface conf(QLatin1String("org.freedesktop.Akonadi.Resource.") + defaultId, + QStringLiteral("/Settings"), QString()); + + if (!conf.isValid()) { + q->setError(-1); + q->setErrorText(i18n("Invalid resource identifier '%1'", defaultId)); + q->emitResult(); + return; + } + + QMapIterator it(mDefaultResourceOptions); + while (it.hasNext()) { + it.next(); + + if (it.key() == QLatin1String("Name")) { + continue; + } + + const QString methodName = QStringLiteral("set%1").arg(it.key()); + const QVariant::Type argType = argumentType(conf.metaObject(), methodName); + if (argType == QVariant::Invalid) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Failed to configure default resource via D-Bus.")); + q->emitResult(); + return; + } + + QDBusReply reply = conf.call(methodName, it.value()); + if (!reply.isValid()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Failed to configure default resource via D-Bus.")); + q->emitResult(); + return; + } + } + + conf.call(QStringLiteral("writeConfig")); + + agent.reconfigure(); + } + + // Sync the resource. + { + ResourceSynchronizationJob *syncJob = new ResourceSynchronizationJob(agent, q); + QObject::connect(syncJob, SIGNAL(result(KJob*)), q, SLOT(resourceSyncResult(KJob*))); + syncJob->start(); // non-Akonadi + } +} + +void DefaultResourceJobPrivate::resourceSyncResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + //fail( i18n( "ResourceSynchronizationJob failed (%1).", job->errorString() ) ); + return; + } + + // Fetch the collections of the resource. + qCDebug(AKONADICORE_LOG) << "Fetching maildir collections."; + CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, q); + fetchJob->fetchScope().setResource(defaultResourceId(mSettings)); + QObject::connect(fetchJob, SIGNAL(result(KJob*)), q, SLOT(collectionFetchResult(KJob*))); +} + +void DefaultResourceJobPrivate::collectionFetchResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + //fail( i18n( "Failed to fetch the root maildir collection (%1).", job->errorString() ) ); + return; + } + + CollectionFetchJob *fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + + const Collection::List collections = fetchJob->collections(); + qCDebug(AKONADICORE_LOG) << "Fetched" << collections.count() << "collections."; + + // Find the root maildir collection. + Collection::List toRecover; + Collection resourceCollection; + foreach (const Collection &collection, collections) { + if (collection.parentCollection() == Collection::root()) { + resourceCollection = collection; + toRecover.append(collection); + break; + } + } + + if (!resourceCollection.isValid()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Failed to fetch the resource collection.")); + q->emitResult(); + return; + } + + // Find all children of the resource collection. + foreach (const Collection &collection, collections) { + if (collection.parentCollection() == resourceCollection) { + toRecover.append(collection); + } + } + + QHash typeForName; + foreach (const QByteArray &type, mKnownTypes) { + const QString displayName = mNameForTypeMap.value(type); + typeForName[displayName] = type; + } + + // These collections have been created by the maildir resource, when it + // found the folders on disk. So give them the necessary attributes now. + Q_ASSERT(mPendingModifyJobs == 0); + foreach (Collection collection, toRecover) { // krazy:exclude=foreach + + if (collection.hasAttribute()) { + continue; + } + + // Find the type for the collection. + const QString name = collection.displayName(); + const QByteArray type = typeForName.value(name); + + if (!type.isEmpty()) { + qCDebug(AKONADICORE_LOG) << "Recovering collection" << name; + setCollectionAttributes(collection, type, mNameForTypeMap, mIconForTypeMap); + + CollectionModifyJob *modifyJob = new CollectionModifyJob(collection, q); + QObject::connect(modifyJob, SIGNAL(result(KJob*)), q, SLOT(collectionModifyResult(KJob*))); + mPendingModifyJobs++; + } else { + qCDebug(AKONADICORE_LOG) << "Searching for names: " << typeForName.keys(); + qCDebug(AKONADICORE_LOG) << "Unknown collection name" << name << "-- not recovering."; + } + } + + if (mPendingModifyJobs == 0) { + // Scan the resource. + q->setResourceId(defaultResourceId(mSettings)); + q->ResourceScanJob::doStart(); + } +} + +void DefaultResourceJobPrivate::collectionModifyResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + //fail( i18n( "Failed to modify the root maildir collection (%1).", job->errorString() ) ); + return; + } + + Q_ASSERT(mPendingModifyJobs > 0); + mPendingModifyJobs--; + qCDebug(AKONADICORE_LOG) << "pendingModifyJobs now" << mPendingModifyJobs; + if (mPendingModifyJobs == 0) { + // Write the updated config. + qCDebug(AKONADICORE_LOG) << "Writing defaultResourceId" << defaultResourceId(mSettings) << "to config."; + mSettings->save(); + + // Scan the resource. + q->setResourceId(defaultResourceId(mSettings)); + q->ResourceScanJob::doStart(); + } +} + +DefaultResourceJob::DefaultResourceJob(KCoreConfigSkeleton *settings, QObject *parent) + : ResourceScanJob(QString(), settings, parent) + , d(new DefaultResourceJobPrivate(settings, this)) +{ +} + +DefaultResourceJob::~DefaultResourceJob() +{ + delete d; +} + +void DefaultResourceJob::setDefaultResourceType(const QString &type) +{ + d->mDefaultResourceType = type; +} + +void DefaultResourceJob::setDefaultResourceOptions(const QVariantMap &options) +{ + d->mDefaultResourceOptions = options; +} + +void DefaultResourceJob::setTypes(const QList &types) +{ + d->mKnownTypes = types; +} + +void DefaultResourceJob::setNameForTypeMap(const QMap &map) +{ + d->mNameForTypeMap = map; +} + +void DefaultResourceJob::setIconForTypeMap(const QMap &map) +{ + d->mIconForTypeMap = map; +} + +void DefaultResourceJob::doStart() +{ + d->tryFetchResource(); +} + +void DefaultResourceJob::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + // Do some cleanup. + if (!d->mResourceWasPreexisting) { + // We only removed the resource instance if we have created it. + // Otherwise we might lose the user's data. + const AgentInstance resource = AgentManager::self()->instance(defaultResourceId(d->mSettings)); + qCDebug(AKONADICORE_LOG) << "Removing resource" << resource.identifier(); + AgentManager::self()->removeInstance(resource); + } + } + + Job::slotResult(job); +} + +// ===================== GetLockJob ============================ + +class Q_DECL_HIDDEN Akonadi::GetLockJob::Private +{ +public: + Private(GetLockJob *qq); + + void doStart(); // slot + void serviceOwnerChanged(const QString &name, const QString &oldOwner, + const QString &newOwner); // slot + void timeout(); // slot + + GetLockJob *const q; + QTimer *mSafetyTimer; +}; + +GetLockJob::Private::Private(GetLockJob *qq) + : q(qq) + , mSafetyTimer(0) +{ +} + +void GetLockJob::Private::doStart() +{ + // Just doing registerService() and checking its return value is not sufficient, + // since we may *already* own the name, and then registerService() returns true. + + QDBusConnection bus = KDBusConnectionPool::threadConnection(); + const bool alreadyLocked = bus.interface()->isServiceRegistered(dbusServiceName()); + const bool gotIt = bus.registerService(dbusServiceName()); + + if (gotIt && !alreadyLocked) { + //qCDebug(AKONADICORE_LOG) << "Got lock immediately."; + q->emitResult(); + } else { + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(dbusServiceName(), KDBusConnectionPool::threadConnection(), + QDBusServiceWatcher::WatchForOwnerChange, q); + //qCDebug(AKONADICORE_LOG) << "Waiting for lock."; + connect(watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)), q, SLOT(serviceOwnerChanged(QString,QString,QString))); + + mSafetyTimer = new QTimer(q); + mSafetyTimer->setSingleShot(true); + mSafetyTimer->setInterval(LOCK_WAIT_TIMEOUT_SECONDS * 1000); + mSafetyTimer->start(); + connect(mSafetyTimer, SIGNAL(timeout()), q, SLOT(timeout())); + } +} + +void GetLockJob::Private::serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(oldOwner) + + if (newOwner.isEmpty()) { + const bool gotIt = KDBusConnectionPool::threadConnection().registerService(dbusServiceName()); + if (gotIt) { + mSafetyTimer->stop(); + q->emitResult(); + } + } +} + +void GetLockJob::Private::timeout() +{ + qCWarning(AKONADICORE_LOG) << "Timeout trying to get lock. Check who has acquired the name" << dbusServiceName() << "on DBus, using qdbus or qdbusviewer."; + q->setError(Job::Unknown); + q->setErrorText(i18n("Timeout trying to get lock.")); + q->emitResult(); +} + +GetLockJob::GetLockJob(QObject *parent) + : KJob(parent) + , d(new Private(this)) +{ +} + +GetLockJob::~GetLockJob() +{ + delete d; +} + +void GetLockJob::start() +{ + QTimer::singleShot(0, this, SLOT(doStart())); +} + +void Akonadi::setCollectionAttributes(Akonadi::Collection &collection, const QByteArray &type, + const QMap &nameForType, + const QMap &iconForType) +{ + { + EntityDisplayAttribute *attr = new EntityDisplayAttribute; + attr->setIconName(iconForType.value(type)); + attr->setDisplayName(nameForType.value(type)); + collection.addAttribute(attr); + } + + { + SpecialCollectionAttribute *attr = new SpecialCollectionAttribute; + attr->setCollectionType(type); + collection.addAttribute(attr); + } +} + +bool Akonadi::releaseLock() +{ + return KDBusConnectionPool::threadConnection().unregisterService(dbusServiceName()); +} + +#include "moc_specialcollectionshelperjobs_p.cpp" diff --git a/src/core/jobs/specialcollectionshelperjobs_p.h b/src/core/jobs/specialcollectionshelperjobs_p.h new file mode 100644 index 0000000..bcebd5a --- /dev/null +++ b/src/core/jobs/specialcollectionshelperjobs_p.h @@ -0,0 +1,245 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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_SPECIALCOLLECTIONSHELPERJOBS_P_H +#define AKONADI_SPECIALCOLLECTIONSHELPERJOBS_P_H + +#include "akonaditests_export.h" +#include "collection.h" +#include "specialcollections.h" +#include "transactionsequence.h" + +#include + +namespace Akonadi +{ + +// ===================== ResourceScanJob ============================ + +/** + @internal + Helper job for SpecialCollectionsRequestJob. + + A Job that fetches all the collections of a resource, and returns only + those that have a SpecialCollectionAttribute. + + @author Constantin Berzan + @since 4.4 +*/ +class AKONADI_TESTS_EXPORT ResourceScanJob : public Job +{ + Q_OBJECT + +public: + /** + Creates a new ResourceScanJob. + */ + explicit ResourceScanJob(const QString &resourceId, KCoreConfigSkeleton *settings, QObject *parent = 0); + + /** + Destroys this ResourceScanJob. + */ + ~ResourceScanJob(); + + /** + Returns the resource ID of the resource being scanned. + */ + QString resourceId() const; + + /** + Sets the resource ID of the resource to scan. + */ + void setResourceId(const QString &resourceId); + + /** + Returns the root collection of the resource being scanned. + This function relies on there being a single top-level collection owned + by this resource. + */ + Akonadi::Collection rootResourceCollection() const; + + /** + Returns all the collections of this resource which have a + SpecialCollectionAttribute. These might include the root resource collection. + */ + Akonadi::Collection::List specialCollections() const; + +protected: + /* reimpl */ + void doStart() Q_DECL_OVERRIDE; + +private: + class Private; + friend class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void fetchResult(KJob *)) +}; + +// ===================== DefaultResourceJob ============================ + +class DefaultResourceJobPrivate; + +/** + @internal + Helper job for SpecialCollectionsRequestJob. + + A custom ResourceScanJob for the default local folders resource. This is a + maildir resource stored in ~/.local/share/local-mail. + + This job does two things that a regular ResourceScanJob does not do: + 1) It creates and syncs the resource if it is missing. The resource ID is + stored in a config file named specialcollectionsrc. + 2) If the resource had to be recreated, but the folders existed on disk + before that, it recovers the folders based on name. For instance, it will + give a folder named outbox a SpecialCollectionAttribute of type Outbox. + + @author Constantin Berzan + @since 4.4 +*/ +class AKONADI_TESTS_EXPORT DefaultResourceJob : public ResourceScanJob +{ + Q_OBJECT + +public: + /** + * Creates a new DefaultResourceJob. + */ + explicit DefaultResourceJob(KCoreConfigSkeleton *settings, QObject *parent = 0); + + /** + * Destroys the DefaultResourceJob. + */ + ~DefaultResourceJob(); + + /** + * Sets the @p type of the resource that shall be created if the requested + * special collection does not exist yet. + */ + void setDefaultResourceType(const QString &type); + + /** + * Sets the configuration @p options that shall be applied to the new resource + * that is created if the requested special collection does not exist yet. + */ + void setDefaultResourceOptions(const QVariantMap &options); + + /** + * Sets the list of well known special collection @p types. + */ + void setTypes(const QList &types); + + /** + * Sets the @p map of special collection types to display names. + */ + void setNameForTypeMap(const QMap &map); + + /** + * Sets the @p map of special collection types to icon names. + */ + void setIconForTypeMap(const QMap &map); + +protected: + /* reimpl */ + void doStart() Q_DECL_OVERRIDE; + /* reimpl */ + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +private: + friend class DefaultResourceJobPrivate; + DefaultResourceJobPrivate *const d; + + Q_PRIVATE_SLOT(d, void resourceCreateResult(KJob *)) + Q_PRIVATE_SLOT(d, void resourceSyncResult(KJob *)) + Q_PRIVATE_SLOT(d, void collectionFetchResult(KJob *)) + Q_PRIVATE_SLOT(d, void collectionModifyResult(KJob *)) +}; + +// ===================== GetLockJob ============================ + +/** + @internal + Helper job for SpecialCollectionsRequestJob. + + If SpecialCollectionsRequestJob needs to create a collection, it sets a lock so + that other instances do not interfere. This lock is an + org.kde.pim.SpecialCollections name registered on D-Bus. This job is used to get + that lock. + This job will give the lock immediately if possible, or wait up to three + seconds for the lock to be released. If the lock is not released during + that time, this job fails. (This is based on the assumption that + SpecialCollectionsRequestJob operations should not take long.) + + Use the releaseLock() function to release the lock. + + @author Constantin Berzan + @since 4.4 +*/ +class AKONADI_TESTS_EXPORT GetLockJob : public KJob +{ + Q_OBJECT + +public: + /** + Creates a new GetLockJob. + */ + explicit GetLockJob(QObject *parent = 0); + + /** + Destroys the GetLockJob. + */ + ~GetLockJob(); + + /* reimpl */ + void start() Q_DECL_OVERRIDE; + +private: + class Private; + friend class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void doStart()) + Q_PRIVATE_SLOT(d, void serviceOwnerChanged(QString, QString, QString)) + Q_PRIVATE_SLOT(d, void timeout()) +}; + +// ===================== helper functions ============================ + +/** + * Sets on @p col the required attributes of SpecialCollection type @p type + * These are a SpecialCollectionAttribute and an EntityDisplayAttribute. + * @param col collection + * @param type collection type + * @param nameForType collection name for type + * @param iconForType collection icon for type +*/ +void setCollectionAttributes(Akonadi::Collection &col, const QByteArray &type, + const QMap &nameForType, + const QMap &iconForType); + +/** + Releases the SpecialCollectionsRequestJob lock that was obtained through + GetLockJob. + @return Whether the lock was released successfully. +*/ +bool AKONADI_TESTS_EXPORT releaseLock(); + +} // namespace Akonadi + +#endif // AKONADI_SPECIALCOLLECTIONSHELPERJOBS_P_H diff --git a/src/core/jobs/specialcollectionsrequestjob.cpp b/src/core/jobs/specialcollectionsrequestjob.cpp new file mode 100644 index 0000000..44ec3d1 --- /dev/null +++ b/src/core/jobs/specialcollectionsrequestjob.cpp @@ -0,0 +1,364 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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 "specialcollectionsrequestjob.h" + +#include "specialcollectionattribute.h" +#include "specialcollections.h" +#include "specialcollections_p.h" +#include "specialcollectionshelperjobs_p.h" + +#include "agentmanager.h" +#include "collectioncreatejob.h" +#include "entitydisplayattribute.h" + +#include "akonadicore_debug.h" + +#include + +using namespace Akonadi; + +/** + @internal +*/ +class Akonadi::SpecialCollectionsRequestJobPrivate +{ +public: + SpecialCollectionsRequestJobPrivate(SpecialCollections *collections, SpecialCollectionsRequestJob *qq); + + bool isEverythingReady(); + void lockResult(KJob *job); // slot + void releaseLock(); // slot + void nextResource(); + void resourceScanResult(KJob *job); // slot + void createRequestedFolders(ResourceScanJob *job, QHash &requestedFolders); + void collectionCreateResult(KJob *job); // slot + + SpecialCollectionsRequestJob *q; + SpecialCollections *mSpecialCollections; + int mPendingCreateJobs; + + QByteArray mRequestedType; + AgentInstance mRequestedResource; + + // Input: + QHash mDefaultFolders; + bool mRequestingDefaultFolders; + QHash< QString, QHash > mFoldersForResource; + QString mDefaultResourceType; + QVariantMap mDefaultResourceOptions; + QList mKnownTypes; + QMap mNameForTypeMap; + QMap mIconForTypeMap; + + // Output: + QStringList mToForget; + QVector< QPair > mToRegister; +}; + +SpecialCollectionsRequestJobPrivate::SpecialCollectionsRequestJobPrivate(SpecialCollections *collections, + SpecialCollectionsRequestJob *qq) + : q(qq) + , mSpecialCollections(collections) + , mPendingCreateJobs(0) + , mRequestingDefaultFolders(false) +{ +} + +bool SpecialCollectionsRequestJobPrivate::isEverythingReady() +{ + // check if all requested folders are known already + if (mRequestingDefaultFolders) { + QHashIterator it(mDefaultFolders); + while (it.hasNext()) { + it.next(); + if (it.value() && !mSpecialCollections->hasDefaultCollection(it.key())) { + return false; + } + } + } + + const QStringList resourceIds = mFoldersForResource.keys(); + QHashIterator > resourceIt(mFoldersForResource); + while (resourceIt.hasNext()) { + resourceIt.next(); + + const QHash &requested = resourceIt.value(); + QHashIterator it(requested); + while (it.hasNext()) { + it.next(); + if (it.value() && !mSpecialCollections->hasCollection(it.key(), AgentManager::self()->instance(resourceIt.key()))) { + return false; + } + } + } + + return true; +} + +void SpecialCollectionsRequestJobPrivate::lockResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Failed to get lock:" << job->errorString(); + q->setError(job->error()); + q->setErrorText(job->errorString()); + q->emitResult(); + return; + } + + if (mRequestingDefaultFolders) { + // If default folders are requested, deal with that first. + DefaultResourceJob *resjob = new DefaultResourceJob(mSpecialCollections->d->mSettings, q); + resjob->setDefaultResourceType(mDefaultResourceType); + resjob->setDefaultResourceOptions(mDefaultResourceOptions); + resjob->setTypes(mKnownTypes); + resjob->setNameForTypeMap(mNameForTypeMap); + resjob->setIconForTypeMap(mIconForTypeMap); + QObject::connect(resjob, SIGNAL(result(KJob*)), q, SLOT(resourceScanResult(KJob*))); + } else { + // If no default folders are requested, go straight to the next step. + nextResource(); + } +} + +void SpecialCollectionsRequestJobPrivate::releaseLock() +{ + const bool ok = Akonadi::releaseLock(); + if (!ok) { + qCWarning(AKONADICORE_LOG) << "WTF, can't release lock."; + } +} + +void SpecialCollectionsRequestJobPrivate::nextResource() +{ + if (mFoldersForResource.isEmpty()) { + qCDebug(AKONADICORE_LOG) << "All done! Comitting."; + + mSpecialCollections->d->beginBatchRegister(); + + // Forget everything we knew before about these resources. + foreach (const QString &resourceId, mToForget) { + mSpecialCollections->d->forgetFoldersForResource(resourceId); + } + + // Register all the collections that we fetched / created. + typedef QPair RegisterPair; + foreach (const RegisterPair &pair, mToRegister) { + const bool ok = mSpecialCollections->registerCollection(pair.second, pair.first); + Q_ASSERT(ok); + Q_UNUSED(ok); + } + + mSpecialCollections->d->endBatchRegister(); + + // Release the lock once the transaction has been committed. + QObject::connect(q, SIGNAL(result(KJob*)), q, SLOT(releaseLock())); + + // We are done! + q->commit(); + + } else { + const QString resourceId = mFoldersForResource.cbegin().key(); + qCDebug(AKONADICORE_LOG) << "A resource is done," << mFoldersForResource.count() + << "more to do. Now doing resource" << resourceId; + ResourceScanJob *resjob = new ResourceScanJob(resourceId, mSpecialCollections->d->mSettings, q); + QObject::connect(resjob, SIGNAL(result(KJob*)), q, SLOT(resourceScanResult(KJob*))); + } +} + +void SpecialCollectionsRequestJobPrivate::resourceScanResult(KJob *job) +{ + ResourceScanJob *resjob = qobject_cast(job); + Q_ASSERT(resjob); + + const QString resourceId = resjob->resourceId(); + qCDebug(AKONADICORE_LOG) << "resourceId" << resourceId; + + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Failed to request resource" << resourceId << ":" << job->errorString(); + return; + } + + if (qobject_cast(job)) { + // This is the default resource. + if (resourceId != mSpecialCollections->d->defaultResourceId()) { + qCWarning(AKONADICORE_LOG) << "Resource id's don't match: " << resourceId + << mSpecialCollections->d->defaultResourceId(); + Q_ASSERT(false); + } + //mToForget.append( mSpecialCollections->defaultResourceId() ); + createRequestedFolders(resjob, mDefaultFolders); + } else { + // This is not the default resource. + QHash requestedFolders = mFoldersForResource[resourceId]; + mFoldersForResource.remove(resourceId); + createRequestedFolders(resjob, requestedFolders); + } +} + +void SpecialCollectionsRequestJobPrivate::createRequestedFolders(ResourceScanJob *scanJob, + QHash &requestedFolders) +{ + // Remove from the request list the folders which already exist. + foreach (const Collection &collection, scanJob->specialCollections()) { + Q_ASSERT(collection.hasAttribute()); + const SpecialCollectionAttribute *attr = collection.attribute(); + const QByteArray type = attr->collectionType(); + + if (!type.isEmpty()) { + mToRegister.append(qMakePair(collection, type)); + requestedFolders.insert(type, false); + } + } + mToForget.append(scanJob->resourceId()); + + // Folders left in the request list must be created. + Q_ASSERT(mPendingCreateJobs == 0); + Q_ASSERT(scanJob->rootResourceCollection().isValid()); + + QHashIterator it(requestedFolders); + while (it.hasNext()) { + it.next(); + + if (it.value()) { + Collection collection; + collection.setParentCollection(scanJob->rootResourceCollection()); + collection.setName(mNameForTypeMap.value(it.key())); + + setCollectionAttributes(collection, it.key(), mNameForTypeMap, mIconForTypeMap); + + CollectionCreateJob *createJob = new CollectionCreateJob(collection, q); + createJob->setProperty("type", it.key()); + QObject::connect(createJob, SIGNAL(result(KJob*)), q, SLOT(collectionCreateResult(KJob*))); + + mPendingCreateJobs++; + } + } + + if (mPendingCreateJobs == 0) { + nextResource(); + } +} + +void SpecialCollectionsRequestJobPrivate::collectionCreateResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Failed CollectionCreateJob." << job->errorString(); + return; + } + + CollectionCreateJob *createJob = qobject_cast(job); + Q_ASSERT(createJob); + + const Collection collection = createJob->collection(); + mToRegister.append(qMakePair(collection, createJob->property("type").toByteArray())); + + Q_ASSERT(mPendingCreateJobs > 0); + mPendingCreateJobs--; + qCDebug(AKONADICORE_LOG) << "mPendingCreateJobs now" << mPendingCreateJobs; + + if (mPendingCreateJobs == 0) { + nextResource(); + } +} + +// TODO KDE5: do not inherit from TransactionSequence +SpecialCollectionsRequestJob::SpecialCollectionsRequestJob(SpecialCollections *collections, QObject *parent) + : TransactionSequence(parent) + , d(new SpecialCollectionsRequestJobPrivate(collections, this)) +{ + setProperty("transactionsDisabled", true); +} + +SpecialCollectionsRequestJob::~SpecialCollectionsRequestJob() +{ + delete d; +} + +void SpecialCollectionsRequestJob::requestDefaultCollection(const QByteArray &type) +{ + d->mDefaultFolders[type] = true; + d->mRequestingDefaultFolders = true; + d->mRequestedType = type; +} + +void SpecialCollectionsRequestJob::requestCollection(const QByteArray &type, const AgentInstance &instance) +{ + d->mFoldersForResource[instance.identifier()][type] = true; + d->mRequestedType = type; + d->mRequestedResource = instance; +} + +Akonadi::Collection SpecialCollectionsRequestJob::collection() const +{ + if (d->mRequestedResource.isValid()) { + return d->mSpecialCollections->collection(d->mRequestedType, d->mRequestedResource); + } else { + return d->mSpecialCollections->defaultCollection(d->mRequestedType); + } +} + +void SpecialCollectionsRequestJob::setDefaultResourceType(const QString &type) +{ + d->mDefaultResourceType = type; +} + +void SpecialCollectionsRequestJob::setDefaultResourceOptions(const QVariantMap &options) +{ + d->mDefaultResourceOptions = options; +} + +void SpecialCollectionsRequestJob::setTypes(const QList &types) +{ + d->mKnownTypes = types; +} + +void SpecialCollectionsRequestJob::setNameForTypeMap(const QMap &map) +{ + d->mNameForTypeMap = map; +} + +void SpecialCollectionsRequestJob::setIconForTypeMap(const QMap &map) +{ + d->mIconForTypeMap = map; +} + +void SpecialCollectionsRequestJob::doStart() +{ + if (d->isEverythingReady()) { + emitResult(); + } else { + GetLockJob *lockJob = new GetLockJob(this); + connect(lockJob, SIGNAL(result(KJob*)), this, SLOT(lockResult(KJob*))); + lockJob->start(); + } +} + +void SpecialCollectionsRequestJob::slotResult(KJob *job) +{ + if (job->error()) { + // If we failed, let others try. + qCWarning(AKONADICORE_LOG) << "Failed SpecialCollectionsRequestJob::slotResult" << job->errorString(); + + d->releaseLock(); + } + TransactionSequence::slotResult(job); +} + +#include "moc_specialcollectionsrequestjob.cpp" diff --git a/src/core/jobs/specialcollectionsrequestjob.h b/src/core/jobs/specialcollectionsrequestjob.h new file mode 100644 index 0000000..bb33ac3 --- /dev/null +++ b/src/core/jobs/specialcollectionsrequestjob.h @@ -0,0 +1,137 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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_SPECIALCOLLECTIONSREQUESTJOB_H +#define AKONADI_SPECIALCOLLECTIONSREQUESTJOB_H + +#include "akonadicore_export.h" +#include "collection.h" +#include "specialcollections.h" +#include "transactionsequence.h" + +#include + +namespace Akonadi +{ + +class SpecialCollectionsRequestJobPrivate; + +/** + * @short A job to request SpecialCollections. + * + * Use this job to request the SpecialCollections you need. You can request both + * default SpecialCollections and SpecialCollections in a given resource. The default + * SpecialCollections resource is created when the first default SpecialCollection is + * requested, but if a SpecialCollection in a custom resource is requested, this + * job expects that resource to exist already. + * + * If the folders you requested already exist, this job simply succeeds. + * Otherwise, it creates the required collections and registers them with + * SpecialCollections. + * + * This class is not meant to be used directly but as a base class for type + * specific special collection request jobs. + * + * @author Constantin Berzan + * @since 4.4 +*/ +class AKONADICORE_EXPORT SpecialCollectionsRequestJob : public TransactionSequence +{ + Q_OBJECT + +public: + + /** + * Destroys the special collections request job. + */ + ~SpecialCollectionsRequestJob(); + + /** + * Requests a special collection of the given @p type in the default resource. + */ + void requestDefaultCollection(const QByteArray &type); + + /** + * Requests a special collection of the given @p type in the given resource @p instance. + */ + void requestCollection(const QByteArray &type, const AgentInstance &instance); + + /** + * Returns the requested collection. + */ + Collection collection() const; + +protected: + /** + * Creates a new special collections request job. + * + * @param collections The SpecialCollections object that shall be used. + * @param parent The parent object. + */ + explicit SpecialCollectionsRequestJob(SpecialCollections *collections, QObject *parent = Q_NULLPTR); + + /** + * Sets the @p type of the resource that shall be created if the requested + * special collection does not exist yet. + */ + void setDefaultResourceType(const QString &type); + + /** + * Sets the configuration @p options that shall be applied to the new resource + * that is created if the requested special collection does not exist yet. + */ + void setDefaultResourceOptions(const QVariantMap &options); + + /** + * Sets the list of well known special collection @p types. + */ + void setTypes(const QList &types); + + /** + * Sets the @p map of special collection types to display names. + */ + void setNameForTypeMap(const QMap &map); + + /** + * Sets the @p map of special collection types to icon names. + */ + void setIconForTypeMap(const QMap &map); + + /* reimpl */ + void doStart() Q_DECL_OVERRIDE; + /* reimpl */ + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + friend class SpecialCollectionsRequestJobPrivate; + friend class DefaultResourceJobPrivate; + + SpecialCollectionsRequestJobPrivate *const d; + + Q_PRIVATE_SLOT(d, void lockResult(KJob *)) + Q_PRIVATE_SLOT(d, void releaseLock()) + Q_PRIVATE_SLOT(d, void resourceScanResult(KJob *)) + Q_PRIVATE_SLOT(d, void collectionCreateResult(KJob *)) + //@endcond +}; + +} // namespace Akonadi + +#endif // AKONADI_SPECIALCOLLECTIONSREQUESTJOB_H diff --git a/src/core/jobs/subscriptionjob.cpp b/src/core/jobs/subscriptionjob.cpp new file mode 100644 index 0000000..c091167 --- /dev/null +++ b/src/core/jobs/subscriptionjob.cpp @@ -0,0 +1,99 @@ +/* + 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 "subscriptionjob_p.h" + +#include "job_p.h" +#include "collectionmodifyjob.h" + +using namespace Akonadi; + +class Akonadi::SubscriptionJobPrivate : public JobPrivate +{ +public: + SubscriptionJobPrivate(SubscriptionJob *parent) + : JobPrivate(parent) + { + } + + Q_DECLARE_PUBLIC(SubscriptionJob) + + Collection::List mSub, mUnsub; +}; + +SubscriptionJob::SubscriptionJob(QObject *parent) + : Job(new SubscriptionJobPrivate(this), parent) +{ +} + +SubscriptionJob::~SubscriptionJob() +{ +} + +void SubscriptionJob::subscribe(const Collection::List &list) +{ + Q_D(SubscriptionJob); + + d->mSub = list; +} + +void SubscriptionJob::unsubscribe(const Collection::List &list) +{ + Q_D(SubscriptionJob); + + d->mUnsub = list; +} + +void SubscriptionJob::doStart() +{ + Q_D(SubscriptionJob); + + if (d->mSub.isEmpty() && d->mUnsub.isEmpty()) { + emitResult(); + } + + Q_FOREACH (Collection col, d->mSub) { + col.setEnabled(true); + new CollectionModifyJob(col, this); + } + Q_FOREACH (Collection col, d->mUnsub) { + col.setEnabled(false); + new CollectionModifyJob(col, this); + } +} + +void SubscriptionJob::slotResult(KJob *job) +{ + if (job->error()) { + setError(job->error()); + setErrorText(job->errorText()); + Q_FOREACH (KJob *subjob, subjobs()) { + removeSubjob(subjob); + } + emitResult(); + } else { + Job::slotResult(job); + + if (!hasSubjobs()) { + emitResult(); + } + } +} + +#include "moc_subscriptionjob_p.cpp" diff --git a/src/core/jobs/subscriptionjob_p.h b/src/core/jobs/subscriptionjob_p.h new file mode 100644 index 0000000..c52fc37 --- /dev/null +++ b/src/core/jobs/subscriptionjob_p.h @@ -0,0 +1,77 @@ +/* + 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_SUBSCRIPTIONJOB_P_H +#define AKONADI_SUBSCRIPTIONJOB_P_H + +#include "akonadicore_export.h" +#include "collection.h" +#include "job.h" + +namespace Akonadi +{ + +class SubscriptionJobPrivate; + +/** + * @internal + * + * @short Job to manipulate the local subscription state of a set of collections. + */ +class AKONADICORE_EXPORT SubscriptionJob : public Job +{ + Q_OBJECT +public: + /** + * Creates a new subscription job. + * + * @param parent The parent object. + */ + explicit SubscriptionJob(QObject *parent = 0); + + /** + * Destroys the subscription job. + */ + ~SubscriptionJob(); + + /** + * Subscribes to the given list of collections. + * + * @param collections List of collections to subscribe to. + */ + void subscribe(const Collection::List &collections); + + /** + * Unsubscribes from the given list of collections. + * + * @param collections List of collections to unsubscribe from. + */ + void unsubscribe(const Collection::List &collections); + +protected: + void doStart() Q_DECL_OVERRIDE; + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(SubscriptionJob) +}; + +} + +#endif diff --git a/src/core/jobs/tagcreatejob.cpp b/src/core/jobs/tagcreatejob.cpp new file mode 100644 index 0000000..2239659 --- /dev/null +++ b/src/core/jobs/tagcreatejob.cpp @@ -0,0 +1,96 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "tagcreatejob.h" +#include "job_p.h" +#include "tag.h" +#include "protocolhelper_p.h" +#include "akonadicore_debug.h" +#include + +using namespace Akonadi; + +struct Akonadi::TagCreateJobPrivate : public JobPrivate { + TagCreateJobPrivate(TagCreateJob *parent) + : JobPrivate(parent) + , mMerge(false) + { + } + + Tag mTag; + Tag mResultTag; + bool mMerge; +}; + +TagCreateJob::TagCreateJob(const Akonadi::Tag &tag, QObject *parent) + : Job(new TagCreateJobPrivate(this), parent) +{ + Q_D(TagCreateJob); + d->mTag = tag; +} + +void TagCreateJob::setMergeIfExisting(bool merge) +{ + Q_D(TagCreateJob); + d->mMerge = merge; +} + +void TagCreateJob::doStart() +{ + Q_D(TagCreateJob); + + if (d->mTag.gid().isEmpty()) { + qCWarning(AKONADICORE_LOG) << "The gid of a new tag must not be empty"; + setError(Job::Unknown); + setErrorText(i18n("Failed to create tag.")); + emitResult(); + return; + } + + Protocol::CreateTagCommand cmd; + cmd.setGid(d->mTag.gid()); + cmd.setMerge(d->mMerge); + cmd.setType(d->mTag.type()); + cmd.setRemoteId(d->mTag.remoteId()); + cmd.setParentId(d->mTag.parent().id()); + cmd.setAttributes(ProtocolHelper::attributesToProtocol(d->mTag)); + d->sendCommand(cmd); +} + +bool TagCreateJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(TagCreateJob); + + if (response.isResponse() && response.type() == Protocol::Command::FetchTags) { + d->mResultTag = ProtocolHelper::parseTagFetchResult(response); + return false; + } + + if (response.isResponse() && response.type() == Protocol::Command::CreateTag) { + return true; + } + + return Job::doHandleResponse(tag, response); +} + +Tag TagCreateJob::tag() const +{ + Q_D(const TagCreateJob); + return d->mResultTag; +} diff --git a/src/core/jobs/tagcreatejob.h b/src/core/jobs/tagcreatejob.h new file mode 100644 index 0000000..1a6aee6 --- /dev/null +++ b/src/core/jobs/tagcreatejob.h @@ -0,0 +1,72 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_TAGCREATEJOB_H +#define AKONADI_TAGCREATEJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class Tag; +class TagCreateJobPrivate; + +/** + * @short Job that creates a new tag in the Akonadi storage. + * @since 4.13 + */ +class AKONADICORE_EXPORT TagCreateJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new tag create job. + * + * @param tag The tag to create. + * @param parent The parent object. + */ + explicit TagCreateJob(const Tag &tag, QObject *parent = Q_NULLPTR); + + /** + * Returns the created tag with the new unique id, or an invalid tag if the job failed. + */ + Tag tag() const; + + /** + * Merges the tag by GID if it is already existing, and returns the merged version. + * This is false by default. + * + * Note that the returned tag does not contain attributes. + */ + void setMergeIfExisting(bool merge); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(TagCreateJob) +}; + +} + +#endif diff --git a/src/core/jobs/tagdeletejob.cpp b/src/core/jobs/tagdeletejob.cpp new file mode 100644 index 0000000..5931af0 --- /dev/null +++ b/src/core/jobs/tagdeletejob.cpp @@ -0,0 +1,69 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "tagdeletejob.h" +#include "job_p.h" +#include "protocolhelper_p.h" + +using namespace Akonadi; + +struct Akonadi::TagDeleteJobPrivate : public JobPrivate { + TagDeleteJobPrivate(TagDeleteJob *parent) + : JobPrivate(parent) + { + } + + Tag::List mTagsToRemove; +}; + +TagDeleteJob::TagDeleteJob(const Akonadi::Tag &tag, QObject *parent) + : Job(new TagDeleteJobPrivate(this), parent) +{ + Q_D(TagDeleteJob); + d->mTagsToRemove << tag; +} + +TagDeleteJob::TagDeleteJob(const Tag::List &tags, QObject *parent) + : Job(new TagDeleteJobPrivate(this), parent) +{ + Q_D(TagDeleteJob); + d->mTagsToRemove = tags; +} + +void TagDeleteJob::doStart() +{ + Q_D(TagDeleteJob); + + d->sendCommand(Protocol::DeleteTagCommand(ProtocolHelper::entitySetToScope(d->mTagsToRemove))); +} + +bool TagDeleteJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::DeleteTag) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +Tag::List TagDeleteJob::tags() const +{ + Q_D(const TagDeleteJob); + return d->mTagsToRemove; +} diff --git a/src/core/jobs/tagdeletejob.h b/src/core/jobs/tagdeletejob.h new file mode 100644 index 0000000..06f915b --- /dev/null +++ b/src/core/jobs/tagdeletejob.h @@ -0,0 +1,60 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_TAGDELETEJOB_H +#define AKONADI_TAGDELETEJOB_H + +#include "akonadicore_export.h" +#include "job.h" +#include "tag.h" + +namespace Akonadi +{ + +class Tag; +class TagDeleteJobPrivate; + +/** + * @short Job that deletes tags. + * @since 4.13 + */ +class AKONADICORE_EXPORT TagDeleteJob : public Job +{ + Q_OBJECT + +public: + explicit TagDeleteJob(const Tag &tag, QObject *parent = Q_NULLPTR); + explicit TagDeleteJob(const Tag::List &tag, QObject *parent = Q_NULLPTR); + + /** + * Returns the tags passed to the constructor. + */ + Tag::List tags() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(TagDeleteJob) +}; + +} + +#endif diff --git a/src/core/jobs/tagfetchjob.cpp b/src/core/jobs/tagfetchjob.cpp new file mode 100644 index 0000000..a8c8bf7 --- /dev/null +++ b/src/core/jobs/tagfetchjob.cpp @@ -0,0 +1,171 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "tagfetchjob.h" +#include "job_p.h" +#include "tag.h" +#include "protocolhelper_p.h" +#include "tagfetchscope.h" +#include "attributefactory.h" +#include + +using namespace Akonadi; + +class Akonadi::TagFetchJobPrivate : public JobPrivate +{ +public: + TagFetchJobPrivate(TagFetchJob *parent) + : JobPrivate(parent) + , mEmitTimer(0) + { + } + + void init() + { + Q_Q(TagFetchJob); + mEmitTimer = new QTimer(q); + mEmitTimer->setSingleShot(true); + mEmitTimer->setInterval(100); + q->connect(mEmitTimer, SIGNAL(timeout()), q, SLOT(timeout())); + } + + void aboutToFinish() Q_DECL_OVERRIDE { + timeout(); + } + + void timeout() + { + Q_Q(TagFetchJob); + mEmitTimer->stop(); // in case we are called by result() + if (!mPendingTags.isEmpty()) { + if (!q->error()) { + emit q->tagsReceived(mPendingTags); + } + mPendingTags.clear(); + } + } + + Q_DECLARE_PUBLIC(TagFetchJob) + + Tag::List mRequestedTags; + Tag::List mResultTags; + Tag::List mPendingTags; // items pending for emitting itemsReceived() + QTimer *mEmitTimer; + TagFetchScope mFetchScope; +}; + +TagFetchJob::TagFetchJob(QObject *parent) + : Job(new TagFetchJobPrivate(this), parent) +{ + Q_D(TagFetchJob); + d->init(); +} + +TagFetchJob::TagFetchJob(const Tag &tag, QObject *parent) + : Job(new TagFetchJobPrivate(this), parent) +{ + Q_D(TagFetchJob); + d->init(); + d->mRequestedTags << tag; +} + +TagFetchJob::TagFetchJob(const Tag::List &tags, QObject *parent) + : Job(new TagFetchJobPrivate(this), parent) +{ + Q_D(TagFetchJob); + d->init(); + d->mRequestedTags = tags; +} + +TagFetchJob::TagFetchJob(const QList &ids, QObject *parent) + : Job(new TagFetchJobPrivate(this), parent) +{ + Q_D(TagFetchJob); + d->init(); + Q_FOREACH (Tag::Id id, ids) { + d->mRequestedTags << Tag(id); + } +} + +void TagFetchJob::setFetchScope(const TagFetchScope &fetchScope) +{ + Q_D(TagFetchJob); + d->mFetchScope = fetchScope; +} + +TagFetchScope &TagFetchJob::fetchScope() +{ + Q_D(TagFetchJob); + return d->mFetchScope; +} + +void TagFetchJob::doStart() +{ + Q_D(TagFetchJob); + + Protocol::FetchTagsCommand cmd; + if (d->mRequestedTags.isEmpty()) { + cmd = Protocol::FetchTagsCommand(Scope(ImapInterval(1, 0))); + } else { + try { + cmd = Protocol::FetchTagsCommand(ProtocolHelper::entitySetToScope(d->mRequestedTags)); + } catch (const Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } + } + cmd.setAttributes(d->mFetchScope.attributes()); + cmd.setIdOnly(d->mFetchScope.fetchIdOnly()); + + d->sendCommand(cmd); +} + +bool TagFetchJob::doHandleResponse(qint64 _tag, const Protocol::Command &response) +{ + Q_D(TagFetchJob); + + if (!response.isResponse() || response.type() != Protocol::Command::FetchTags) { + return Job::doHandleResponse(_tag, response); + } + + Protocol::FetchTagsResponse resp(response); + // Invalid tag in response marks the last response + if (resp.id() < 0) { + return true; + } + + const Tag tag = ProtocolHelper::parseTagFetchResult(response); + d->mResultTags.append(tag); + d->mPendingTags.append(tag); + if (!d->mEmitTimer->isActive()) { + d->mEmitTimer->start(); + } + + return false; +} + +Tag::List TagFetchJob::tags() const +{ + Q_D(const TagFetchJob); + return d->mResultTags; +} + +#include "moc_tagfetchjob.cpp" diff --git a/src/core/jobs/tagfetchjob.h b/src/core/jobs/tagfetchjob.h new file mode 100644 index 0000000..d538acd --- /dev/null +++ b/src/core/jobs/tagfetchjob.h @@ -0,0 +1,139 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_TAGFETCHJOB_H +#define AKONADI_TAGFETCHJOB_H + +#include "akonadicore_export.h" +#include "job.h" +#include "tag.h" + +namespace Akonadi +{ + +class TagFetchScope; +class TagFetchJobPrivate; + +/** + * @short Job that fetches tags from the Akonadi storage. + * + * This class is used to fetch tags from the Akonadi storage. + * + * If you want to fetch all items with given tag, use ItemFetchJob and the + * ItemFetchJob(const Tag &tag, QObject *parent = Q_NULLPTR) constructor (since 4.14) + * + * @since 4.13 + */ +class AKONADICORE_EXPORT TagFetchJob : public Job +{ + Q_OBJECT + +public: + /** + * Constructs a new tag fetch job that retrieves all tags stored in Akonadi. + * + * @param parent The parent object. + */ + explicit TagFetchJob(QObject *parent = Q_NULLPTR); + + /** + * Constructs a new tag fetch job that retrieves the specified tag. + * If the tag has a uid set, this is used to identify the tag on the Akonadi + * server. If only a remote identifier is available, that is used. However + * as remote identifiers are internal to resources, it's necessary to set + * the resource context (see ResourceSelectJob). + * + * @param tag The tag to fetch. + * @param parent The parent object. + */ + explicit TagFetchJob(const Tag &tag, QObject *parent = Q_NULLPTR); + + /** + * Constructs a new tag fetch job that retrieves specified tags. + * If the tags have a uid set, this is used to identify the tags on the Akonadi + * server. If only a remote identifier is available, that is used. However + * as remote identifiers are internal to resources, it's necessary to set + * the resource context (see ResourceSelectJob). + * + * @param tags Tags to fetch. + * @param parent The parent object. + */ + explicit TagFetchJob(const Tag::List &tags, QObject *parent = Q_NULLPTR); + + /** + * Convenience ctor equivalent to ItemFetchJob(const Item::List &items, QObject *parent = Q_NULLPTR) + * + * @param ids UIDs of tags to fetch. + * @param parent The parent object. + */ + explicit TagFetchJob(const QList &ids, QObject *parent = Q_NULLPTR); + + /** + * Sets the tag fetch scope. + * + * The TagFetchScope controls how much of an tags's data is fetched + * from the server. + * + * @param fetchScope The new fetch scope for tag fetch operations. + * @see fetchScope() + */ + void setFetchScope(const TagFetchScope &fetchScope); + + /** + * Returns the tag fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the TagFetchScope documentation + * for an example. + * + * @return a reference to the current tag fetch scope + * + * @see setFetchScope() for replacing the current tag fetch scope + */ + TagFetchScope &fetchScope(); + + /** + * Returns the fetched tags after the job has been completed. + */ + Tag::List tags() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever new tags have been fetched completely. + * + * @param items The fetched tags. + */ + void tagsReceived(const Akonadi::Tag::List &tags); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(TagFetchJob) + + //@cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void timeout()) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/tagmodifyjob.cpp b/src/core/jobs/tagmodifyjob.cpp new file mode 100644 index 0000000..984539b --- /dev/null +++ b/src/core/jobs/tagmodifyjob.cpp @@ -0,0 +1,86 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "tagmodifyjob.h" +#include "job_p.h" +#include "tag.h" +#include "tag_p.h" +#include "protocolhelper_p.h" +#include "changemediator_p.h" + +using namespace Akonadi; + +struct Akonadi::TagModifyJobPrivate : public JobPrivate { + TagModifyJobPrivate(TagModifyJob *parent) + : JobPrivate(parent) + { + } + + Tag mTag; +}; + +TagModifyJob::TagModifyJob(const Akonadi::Tag &tag, QObject *parent) + : Job(new TagModifyJobPrivate(this), parent) +{ + Q_D(TagModifyJob); + d->mTag = tag; +} + +void TagModifyJob::doStart() +{ + Q_D(TagModifyJob); + + Protocol::ModifyTagCommand cmd(d->mTag.id()); + if (!d->mTag.remoteId().isNull()) { + cmd.setRemoteId(d->mTag.remoteId()); + } + if (!d->mTag.type().isEmpty()) { + cmd.setType(d->mTag.type()); + } + if (d->mTag.parent().isValid() && !d->mTag.isImmutable()) { + cmd.setParentId(d->mTag.parent().id()); + } + if (!d->mTag.d_ptr->mDeletedAttributes.isEmpty()) { + cmd.setRemovedAttributes(d->mTag.d_ptr->mDeletedAttributes); + } + + cmd.setAttributes(ProtocolHelper::attributesToProtocol(d->mTag)); + + d->sendCommand(cmd); +} + +bool TagModifyJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(TagModifyJob); + + if (response.isResponse()) { + if (response.type() == Protocol::Command::FetchTags) { + // Tag was modified, we ignore the response for now + return false; + } else if (response.type() == Protocol::Command::DeleteTag) { + // The tag was deleted/merged + return false; + } else if (response.type() == Protocol::Command::ModifyTag) { + // Done. + return true; + } + } + + return Job::doHandleResponse(tag, response); +} diff --git a/src/core/jobs/tagmodifyjob.h b/src/core/jobs/tagmodifyjob.h new file mode 100644 index 0000000..6b89b2e --- /dev/null +++ b/src/core/jobs/tagmodifyjob.h @@ -0,0 +1,64 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_TAGMODIFYJOB_H +#define AKONADI_TAGMODIFYJOB_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class Tag; +class TagModifyJobPrivate; + +/** + * @short Job that modifies a tag in the Akonadi storage. + * @since 4.13 + */ +class AKONADICORE_EXPORT TagModifyJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new tag modify job. + * + * @param tag The tag to modify. + * @param parent The parent object. + */ + explicit TagModifyJob(const Tag &tag, QObject *parent = Q_NULLPTR); + + /** + * Returns the modified tag. + */ + Tag tag() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(TagModifyJob) +}; + +} + +#endif diff --git a/src/core/jobs/transactionjobs.cpp b/src/core/jobs/transactionjobs.cpp new file mode 100644 index 0000000..2634d5a --- /dev/null +++ b/src/core/jobs/transactionjobs.cpp @@ -0,0 +1,98 @@ +/* + 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 "transactionjobs.h" + +#include "job_p.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::TransactionJobPrivate : public JobPrivate +{ +public: + TransactionJobPrivate(Job *parent) + : JobPrivate(parent) + {} +}; + +TransactionJob::TransactionJob(QObject *parent) + : Job(new TransactionJobPrivate(this), parent) +{ + Q_ASSERT(parent); +} + +TransactionJob::~TransactionJob() +{ +} + +void TransactionJob::doStart() +{ + Q_D(TransactionJob); + + Protocol::TransactionCommand::Mode mode; + if (qobject_cast(this)) { + mode = Protocol::TransactionCommand::Begin; + } else if (qobject_cast(this)) { + mode = Protocol::TransactionCommand::Rollback; + } else if (qobject_cast(this)) { + mode = Protocol::TransactionCommand::Commit; + } else { + Q_ASSERT(false); + mode = Protocol::TransactionCommand::Invalid; + } + + d->sendCommand(Protocol::TransactionCommand(mode)); +} + +bool TransactionJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + if (!response.isResponse() || response.type() != Protocol::Command::Transaction) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +TransactionBeginJob::TransactionBeginJob(QObject *parent) + : TransactionJob(parent) +{ +} + +TransactionBeginJob::~TransactionBeginJob() +{ +} + +TransactionRollbackJob::TransactionRollbackJob(QObject *parent) + : TransactionJob(parent) +{ +} + +TransactionRollbackJob::~TransactionRollbackJob() +{ +} + +TransactionCommitJob::TransactionCommitJob(QObject *parent) + : TransactionJob(parent) +{ +} + +TransactionCommitJob::~TransactionCommitJob() +{ +} diff --git a/src/core/jobs/transactionjobs.h b/src/core/jobs/transactionjobs.h new file mode 100644 index 0000000..0f264f2 --- /dev/null +++ b/src/core/jobs/transactionjobs.h @@ -0,0 +1,138 @@ +/* + 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. +*/ + +#ifndef AKONADI_TRANSACTIONJOBS_H +#define AKONADI_TRANSACTIONJOBS_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class TransactionJobPrivate; +class TransactionJob : public Job +{ + Q_OBJECT + +public: + ~TransactionJob(); + +protected: + explicit TransactionJob(QObject *parent); + + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(TransactionJob) +}; + +class TransactionJobPrivate; + +/** + * @short Job that begins a session-global transaction. + * + * Sometimes you want to execute a sequence of commands in + * an atomic way, so that either all commands or none shall + * be executed. The TransactionBeginJob, TransactionCommitJob and + * TransactionRollbackJob provide these functionality for the + * Akonadi Job classes. + * + * @note This will only have an effect when used as a subjob or with a Session. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT TransactionBeginJob : public TransactionJob +{ + Q_OBJECT + +public: + /** + * Creates a new transaction begin job. + * + * @param parent The parent job or Session, must not be 0. + */ + explicit TransactionBeginJob(QObject *parent); + + /** + * Destroys the transaction begin job. + */ + ~TransactionBeginJob(); +}; + +/** + * @short Job that aborts a session-global transaction. + * + * If a job inside a TransactionBeginJob has been failed, + * the TransactionRollbackJob can be used to rollback all changes done by these + * jobs. + * + * @note This will only have an effect when used as a subjob or with a Session. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT TransactionRollbackJob : public TransactionJob +{ + Q_OBJECT + +public: + /** + * Creates a new transaction rollback job. + * The parent must be the same parent as for the TransactionBeginJob. + * + * @param parent The parent job or Session, must not be 0. + */ + explicit TransactionRollbackJob(QObject *parent); + + /** + * Destroys the transaction rollback job. + */ + ~TransactionRollbackJob(); +}; + +/** + * @short Job that commits a session-global transaction. + * + * This job commits all changes of this transaction. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT TransactionCommitJob : public TransactionJob +{ + Q_OBJECT + +public: + /** + * Creates a new transaction commit job. + * The parent must be the same parent as for the TransactionBeginJob. + * + * @param parent The parent job or Session, must not be 0. + */ + explicit TransactionCommitJob(QObject *parent); + + /** + * Destroys the transaction commit job. + */ + ~TransactionCommitJob(); +}; + +} + +#endif diff --git a/src/core/jobs/transactionsequence.cpp b/src/core/jobs/transactionsequence.cpp new file mode 100644 index 0000000..e5939e1 --- /dev/null +++ b/src/core/jobs/transactionsequence.cpp @@ -0,0 +1,233 @@ +/* + Copyright (c) 2006-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 "transactionsequence.h" +#include "transactionjobs.h" + +#include "job_p.h" + +#include +#include + +using namespace Akonadi; + +class Akonadi::TransactionSequencePrivate : public JobPrivate +{ +public: + TransactionSequencePrivate(TransactionSequence *parent) + : JobPrivate(parent) + , mState(Idle) + , mAutoCommit(true) + { + } + + enum TransactionState { + Idle, + Running, + WaitingForSubjobs, + RollingBack, + Committing + }; + + Q_DECLARE_PUBLIC(TransactionSequence) + + TransactionState mState; + QSet mIgnoredErrorJobs; + bool mAutoCommit; + + void commitResult(KJob *job) + { + Q_Q(TransactionSequence); + + if (job->error()) { + q->setError(job->error()); + q->setErrorText(job->errorText()); + } + q->emitResult(); + } + + void rollbackResult(KJob *job) + { + Q_Q(TransactionSequence); + + Q_UNUSED(job); + q->emitResult(); + } +}; + +TransactionSequence::TransactionSequence(QObject *parent) + : Job(new TransactionSequencePrivate(this), parent) +{ +} + +TransactionSequence::~TransactionSequence() +{ +} + +bool TransactionSequence::addSubjob(KJob *job) +{ + Q_D(TransactionSequence); + + //Don't abort the rollback job, while keeping the state set. + if (d->mState == TransactionSequencePrivate::RollingBack) { + return Job::addSubjob(job); + } + + if (error()) { + //This can happen if a rollback is in progress, so make sure we don't set the state back to running. + job->kill(EmitResult); + return false; + } + // TODO KDE5: remove property hack once SpecialCollectionsRequestJob has been fixed + if (d->mState == TransactionSequencePrivate::Idle && !property("transactionsDisabled").toBool()) { + d->mState = TransactionSequencePrivate::Running; // needs to be set before creating the transaction job to avoid infinite recursion + new TransactionBeginJob(this); + } else { + d->mState = TransactionSequencePrivate::Running; + } + return Job::addSubjob(job); +} + +void TransactionSequence::slotResult(KJob *job) +{ + Q_D(TransactionSequence); + + if (!job->error() || d->mIgnoredErrorJobs.contains(job)) { + // If we have an error but want to ignore it, we can't call Job::slotResult + // because it would confuse the subjob queue processing logic. Just removing + // the subjob instead is fine. + if (!job->error()) { + Job::slotResult(job); + } else { + Job::removeSubjob(job); + } + + if (!hasSubjobs() && d->mState == TransactionSequencePrivate::WaitingForSubjobs) { + if (property("transactionsDisabled").toBool()) { + emitResult(); + return; + } + d->mState = TransactionSequencePrivate::Committing; + TransactionCommitJob *job = new TransactionCommitJob(this); + connect(job, SIGNAL(result(KJob*)), SLOT(commitResult(KJob*))); + } + } else { + setError(job->error()); + setErrorText(job->errorText()); + removeSubjob(job); + + // cancel all subjobs in case someone else is listening (such as ItemSync), but without notifying ourselves again + foreach (KJob *job, subjobs()) { + disconnect(job, SIGNAL(result(KJob*)), this, SLOT(slotResult(KJob*))); + job->kill(EmitResult); + } + clearSubjobs(); + + if (d->mState == TransactionSequencePrivate::Running || d->mState == TransactionSequencePrivate::WaitingForSubjobs) { + if (property("transactionsDisabled").toBool()) { + emitResult(); + return; + } + d->mState = TransactionSequencePrivate::RollingBack; + TransactionRollbackJob *job = new TransactionRollbackJob(this); + connect(job, SIGNAL(result(KJob*)), SLOT(rollbackResult(KJob*))); + } + } +} + +void TransactionSequence::commit() +{ + Q_D(TransactionSequence); + + if (d->mState == TransactionSequencePrivate::Running) { + d->mState = TransactionSequencePrivate::WaitingForSubjobs; + } else { + // we never got any subjobs, that means we never started a transaction + // so we can just quit here + if (d->mState == TransactionSequencePrivate::Idle) { + emitResult(); + } + return; + } + + if (subjobs().isEmpty()) { + if (property("transactionsDisabled").toBool()) { + emitResult(); + return; + } + if (!error()) { + d->mState = TransactionSequencePrivate::Committing; + TransactionCommitJob *job = new TransactionCommitJob(this); + connect(job, SIGNAL(result(KJob*)), SLOT(commitResult(KJob*))); + } else { + d->mState = TransactionSequencePrivate::RollingBack; + TransactionRollbackJob *job = new TransactionRollbackJob(this); + connect(job, SIGNAL(result(KJob*)), SLOT(rollbackResult(KJob*))); + } + } +} + +void TransactionSequence::setIgnoreJobFailure(KJob *job) +{ + Q_D(TransactionSequence); + + // make sure this is one of our sub jobs + Q_ASSERT(subjobs().contains(job)); + + d->mIgnoredErrorJobs.insert(job); +} + +void TransactionSequence::doStart() +{ + Q_D(TransactionSequence); + + if (d->mAutoCommit) { + if (d->mState == TransactionSequencePrivate::Idle) { + emitResult(); + } else { + commit(); + } + } +} + +void TransactionSequence::setAutomaticCommittingEnabled(bool enable) +{ + Q_D(TransactionSequence); + d->mAutoCommit = enable; +} + +void TransactionSequence::rollback() +{ + Q_D(TransactionSequence); + + setError(UserCanceled); + // we never really started + if (d->mState == TransactionSequencePrivate::Idle) { + emitResult(); + return; + } + + // TODO cancel not yet executed sub-jobs here + + d->mState = TransactionSequencePrivate::RollingBack; + TransactionRollbackJob *job = new TransactionRollbackJob(this); + connect(job, SIGNAL(result(KJob*)), SLOT(rollbackResult(KJob*))); +} + +#include "moc_transactionsequence.cpp" diff --git a/src/core/jobs/transactionsequence.h b/src/core/jobs/transactionsequence.h new file mode 100644 index 0000000..dec3d10 --- /dev/null +++ b/src/core/jobs/transactionsequence.h @@ -0,0 +1,137 @@ +/* + Copyright (c) 2006-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_TRANSACTIONSEQUENCE_H +#define AKONADI_TRANSACTIONSEQUENCE_H + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ + +class TransactionSequencePrivate; + +/** + * @short Base class for jobs that need to run a sequence of sub-jobs in a transaction. + * + * As soon as the first subjob is added, the transaction is started. + * As soon as the last subjob has successfully finished, the transaction is committed. + * If any subjob fails, the transaction is rolled back. + * + * Alternatively, a TransactionSequence object can be used as a parent object + * for a set of jobs to achieve the same behaviour without subclassing. + * + * Example: + * + * @code + * + * // Delete a couple of items inside a transaction + * Akonadi::TransactionSequence *transaction = new Akonadi::TransactionSequence; + * connect( transaction, SIGNAL(result(KJob*)), SLOT(transactionFinished(KJob*)) ); + * + * const Akonadi::Item::List items = ... + * + * foreach ( const Akonadi::Item &item, items ) { + * new Akonadi::ItemDeleteJob( item, transaction ); + * } + * + * ... + * + * MyClass::transactionFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Items deleted successfully"; + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT TransactionSequence : public Job +{ + Q_OBJECT +public: + /** + * Creates a new transaction sequence. + * + * @param parent The parent object. + */ + explicit TransactionSequence(QObject *parent = Q_NULLPTR); + + /** + * Destroys the transaction sequence. + */ + ~TransactionSequence(); + + /** + * Commits the transaction as soon as all pending sub-jobs finished successfully. + */ + void commit(); + + /** + * Rolls back the current transaction as soon as possible. + * You only need to call this method when you want to roll back due to external + * reasons (e.g. user cancellation), the transaction is automatically rolled back + * if one of its subjobs fails. + * @since 4.5 + */ + void rollback(); + + /** + * Sets which job of the sequence might fail without rolling back the + * complete transaction. + * @param job a job to ignore errors from + * @since 4.5 + */ + void setIgnoreJobFailure(KJob *job); + + /** + * Disable automatic committing. + * Use this when you want to add jobs to this sequence after execution + * has been started, usually that is outside of the constructor or the + * method that creates this transaction sequence. + * @note Calling this method after execution of this job has been started + * has no effect. + * @param enable @c true to enable autocommitting (default), @c false to disable it + * @since 4.5 + */ + void setAutomaticCommittingEnabled(bool enable); + +protected: + bool addSubjob(KJob *job) Q_DECL_OVERRIDE; + void doStart() Q_DECL_OVERRIDE; + +protected Q_SLOTS: + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(TransactionSequence) + + //@cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void commitResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void rollbackResult(KJob *)) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/trashjob.cpp b/src/core/jobs/trashjob.cpp new file mode 100644 index 0000000..ef22751 --- /dev/null +++ b/src/core/jobs/trashjob.cpp @@ -0,0 +1,373 @@ +/* + Copyright (c) 2011 Christian Mollekopf + + 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 "trashjob.h" + +#include "collection.h" +#include "entitydeletedattribute.h" +#include "item.h" +#include "job_p.h" +#include "trashsettings.h" + +#include + +#include "itemdeletejob.h" +#include "collectiondeletejob.h" +#include "itemmovejob.h" +#include "collectionmovejob.h" +#include "itemmodifyjob.h" +#include "collectionmodifyjob.h" +#include "itemfetchscope.h" +#include "collectionfetchscope.h" +#include "itemfetchjob.h" +#include "collectionfetchjob.h" + +#include "akonadicore_debug.h" + +#include + +using namespace Akonadi; + +class TrashJob::TrashJobPrivate : public JobPrivate +{ +public: + TrashJobPrivate(TrashJob *parent) + : JobPrivate(parent) + , mKeepTrashInCollection(false) + , mSetRestoreCollection(false) + , mDeleteIfInTrash(false) + { + } +//4. + void selectResult(KJob *job); +//3. + //Helper functions to recursivly set the attribute on deleted collections + void setAttribute(const Akonadi::Collection::List &); + void setAttribute(const Akonadi::Item::List &); + //Set attributes after ensuring that move job was successful + void setAttribute(KJob *job); + +//2. + //called after parent of the trashed item was fetched (needed to see in which resource the item is in) + void parentCollectionReceived(const Akonadi::Collection::List &); + +//1. + //called after initial fetch of trashed items + void itemsReceived(const Akonadi::Item::List &); + //called after initial fetch of trashed collection + void collectionsReceived(const Akonadi::Collection::List &); + + Q_DECLARE_PUBLIC(TrashJob) + + Item::List mItems; + Collection mCollection; + Collection mRestoreCollection; + Collection mTrashCollection; + bool mKeepTrashInCollection; + bool mSetRestoreCollection; //only set restore collection when moved to trash collection (not in place) + bool mDeleteIfInTrash; + QHash mCollectionItems; //list of trashed items sorted according to parent collection + QHash mParentCollections; //fetched parent collection of items (containing the resource name) + +}; + +void TrashJob::TrashJobPrivate::selectResult(KJob *job) +{ + Q_Q(TrashJob); + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->objectName(); + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; // KCompositeJob takes care of errors + } + + if (!q->hasSubjobs() || (q->subjobs().contains(static_cast(q->sender())) && q->subjobs().size() == 1)) { + q->emitResult(); + } +} + +void TrashJob::TrashJobPrivate::setAttribute(const Akonadi::Collection::List &list) +{ + Q_Q(TrashJob); + QVectorIterator i(list); + while (i.hasNext()) { + const Collection &col = i.next(); + EntityDeletedAttribute *eda = new EntityDeletedAttribute(); + if (mSetRestoreCollection) { + Q_ASSERT(mRestoreCollection.isValid()); + eda->setRestoreCollection(mRestoreCollection); + } + + Collection modCol(col.id()); //really only modify attribute (forget old remote ids, etc.), otherwise we have an error because of the move + modCol.addAttribute(eda); + + CollectionModifyJob *job = new CollectionModifyJob(modCol, q); + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + + ItemFetchJob *itemFetchJob = new ItemFetchJob(col, q); + //TODO not sure if it is guaranteed that itemsReceived is always before result (otherwise the result is emitted before the attributes are set) + q->connect(itemFetchJob, SIGNAL(itemsReceived(Akonadi::Item::List)), SLOT(setAttribute(Akonadi::Item::List))); + q->connect(itemFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + } +} + +void TrashJob::TrashJobPrivate::setAttribute(const Akonadi::Item::List &list) +{ + Q_Q(TrashJob); + Item::List items = list; + QMutableVectorIterator i(items); + while (i.hasNext()) { + const Item &item = i.next(); + EntityDeletedAttribute *eda = new EntityDeletedAttribute(); + if (mSetRestoreCollection) { + //When deleting a collection, we want to restore the deleted collection's items restored to the deleted collection's parent, not the items parent + if (mRestoreCollection.isValid()) { + eda->setRestoreCollection(mRestoreCollection); + } else { + Q_ASSERT(mParentCollections.contains(item.parentCollection().id())); + eda->setRestoreCollection(mParentCollections.value(item.parentCollection().id())); + } + } + + Item modItem(item.id()); //really only modify attribute (forget old remote ids, etc.) + modItem.addAttribute(eda); + ItemModifyJob *job = new ItemModifyJob(modItem, q); + job->setIgnorePayload(true); + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + } + + //For some reason it is not possible to apply this change to multiple items at once + /*ItemModifyJob *job = new ItemModifyJob(items, q); + q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );*/ +} + +void TrashJob::TrashJobPrivate::setAttribute(KJob *job) +{ + Q_Q(TrashJob); + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->objectName(); + qCWarning(AKONADICORE_LOG) << job->errorString(); + q->setError(Job::Unknown); + q->setErrorText(i18n("Move to trash collection failed, aborting trash operation")); + return; + } + + //For Items + const QVariant var = job->property("MovedItems"); + if (var.isValid()) { + int id = var.toInt(); + Q_ASSERT(id >= 0); + setAttribute(mCollectionItems.value(Collection(id))); + return; + } + + //For a collection + Q_ASSERT(mCollection.isValid()); + setAttribute(Collection::List() << mCollection); + //Set the attribute on all subcollections and items + CollectionFetchJob *colFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); + q->connect(colFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(setAttribute(Akonadi::Collection::List))); + q->connect(colFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); +} + +void TrashJob::TrashJobPrivate::parentCollectionReceived(const Akonadi::Collection::List &collections) +{ + Q_Q(TrashJob); + Q_ASSERT(collections.size() == 1); + const Collection &parentCollection = collections.first(); + + //store attribute + Q_ASSERT(!parentCollection.resource().isEmpty()); + Collection trashCollection = mTrashCollection; + if (!mTrashCollection.isValid()) { + trashCollection = TrashSettings::getTrashCollection(parentCollection.resource()); + } + if (!mKeepTrashInCollection && trashCollection.isValid()) { //Only set the restore collection if the item is moved to trash + mSetRestoreCollection = true; + } + + mParentCollections.insert(parentCollection.id(), parentCollection); + + if (trashCollection.isValid()) { //Move the items to the correct collection if available + ItemMoveJob *job = new ItemMoveJob(mCollectionItems.value(parentCollection), trashCollection, q); + job->setProperty("MovedItems", parentCollection.id()); + q->connect(job, SIGNAL(result(KJob*)), SLOT(setAttribute(KJob*))); //Wait until the move finished to set the attirbute + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + } else { + setAttribute(mCollectionItems.value(parentCollection)); + } +} + +void TrashJob::TrashJobPrivate::itemsReceived(const Akonadi::Item::List &items) +{ + Q_Q(TrashJob); + if (items.isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid items passed")); + q->emitResult(); + return; + } + + Item::List toDelete; + + QVectorIterator i(items); + while (i.hasNext()) { + const Item &item = i.next(); + if (item.hasAttribute()) { + toDelete.append(item); + continue; + } + Q_ASSERT(item.parentCollection().isValid()); + mCollectionItems[item.parentCollection()].append(item); //Sort by parent col ( = restore collection) + } + + for (auto it = mCollectionItems.cbegin(), e = mCollectionItems.cend(); it != e; ++it) { + CollectionFetchJob *job = new CollectionFetchJob(it.key(), Akonadi::CollectionFetchJob::Base, q); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + SLOT(parentCollectionReceived(Akonadi::Collection::List))); + } + + if (mDeleteIfInTrash && !toDelete.isEmpty()) { + ItemDeleteJob *job = new ItemDeleteJob(toDelete, q); + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + } else if (mCollectionItems.isEmpty()) { //No job started, so we abort the job + qCWarning(AKONADICORE_LOG) << "Nothing to do"; + q->emitResult(); + } + +} + +void TrashJob::TrashJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections) +{ + Q_Q(TrashJob); + if (collections.isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid collection passed")); + q->emitResult(); + return; + } + Q_ASSERT(collections.size() == 1); + mCollection = collections.first(); + + if (mCollection.hasAttribute()) { //marked as deleted + if (mDeleteIfInTrash) { + CollectionDeleteJob *job = new CollectionDeleteJob(mCollection, q); + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + } else { + qCWarning(AKONADICORE_LOG) << "Nothing to do"; + q->emitResult(); + } + return; + } + + Collection trashCollection = mTrashCollection; + if (!mTrashCollection.isValid()) { + trashCollection = TrashSettings::getTrashCollection(mCollection.resource()); + } + if (!mKeepTrashInCollection && trashCollection.isValid()) { //only set the restore collection if the item is moved to trash + mSetRestoreCollection = true; + Q_ASSERT(mCollection.parentCollection().isValid()); + mRestoreCollection = mCollection.parentCollection(); + mRestoreCollection.setResource(mCollection.resource()); //The parent collection doesn't contain the resource, so we have to set it manually + } + + if (trashCollection.isValid()) { + CollectionMoveJob *job = new CollectionMoveJob(mCollection, trashCollection, q); + q->connect(job, SIGNAL(result(KJob*)), SLOT(setAttribute(KJob*))); + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + } else { + setAttribute(Collection::List() << mCollection); + } + +} + +TrashJob::TrashJob(const Item &item, QObject *parent) + : Job(new TrashJobPrivate(this), parent) +{ + Q_D(TrashJob); + d->mItems << item; +} + +TrashJob::TrashJob(const Item::List &items, QObject *parent) + : Job(new TrashJobPrivate(this), parent) +{ + Q_D(TrashJob); + d->mItems = items; +} + +TrashJob::TrashJob(const Collection &collection, QObject *parent) + : Job(new TrashJobPrivate(this), parent) +{ + Q_D(TrashJob); + d->mCollection = collection; +} + +TrashJob::~TrashJob() +{ +} + +Item::List TrashJob::items() const +{ + Q_D(const TrashJob); + return d->mItems; +} + +void TrashJob::setTrashCollection(const Akonadi::Collection &collection) +{ + Q_D(TrashJob); + d->mTrashCollection = collection; +} + +void TrashJob::keepTrashInCollection(bool enable) +{ + Q_D(TrashJob); + d->mKeepTrashInCollection = enable; +} + +void TrashJob::deleteIfInTrash(bool enable) +{ + Q_D(TrashJob); + d->mDeleteIfInTrash = enable; +} + +void TrashJob::doStart() +{ + Q_D(TrashJob); + + //Fetch items first to ensure that the EntityDeletedAttribute is available + if (!d->mItems.isEmpty()) { + ItemFetchJob *job = new ItemFetchJob(d->mItems, this); + job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); //so we have access to the resource + //job->fetchScope().setCacheOnly(true); + job->fetchScope().fetchAttribute(true); + connect(job, SIGNAL(itemsReceived(Akonadi::Item::List)), this, SLOT(itemsReceived(Akonadi::Item::List))); + + } else if (d->mCollection.isValid()) { + CollectionFetchJob *job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this); + job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent); + connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), this, SLOT(collectionsReceived(Akonadi::Collection::List))); + + } else { + qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist"; + setError(Job::Unknown); + setErrorText(i18n("No valid collection or empty itemlist")); + emitResult(); + } +} + +#include "moc_trashjob.cpp" diff --git a/src/core/jobs/trashjob.h b/src/core/jobs/trashjob.h new file mode 100644 index 0000000..1f123c6 --- /dev/null +++ b/src/core/jobs/trashjob.h @@ -0,0 +1,142 @@ +/* + Copyright (c) 2011 Christian Mollekopf + + 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_TRASHJOB_H +#define AKONADI_TRASHJOB_H + +#include "akonadicore_export.h" +#include "item.h" +#include "collection.h" +#include "job.h" + +namespace Akonadi +{ + +/** + * @short Job that moves items/collection to trash. + * + * This job marks the given entites as trash and moves them to a given trash collection, if available. + * + * Priorities of trash collections are the following: + * 1. keepTrashInCollection() + * 2. setTrashCollection() + * 3. configured collection in TrashSettings + * 4. keep in collection if nothing is configured + * + * If the item is already marked as trash, it will be deleted instead + * only if deleteIfInTrash() is set. + * The entity is marked as trash with the EntityDeletedAttribute. + * + * The restore collection in the EntityDeletedAttribute is set the following way: + * -If entites are not moved to trash -> no restore collection + * -If collection is deleted -> also subentites get collection.parentCollection as restore collection + * -If multiple items are deleted -> all items get their parentCollection as restore collection + * + * Example: + * + * @code + * + * const Akonadi::Item::List items = ... + * + * TrashJob *job = new TrashJob( items ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(deletionResult(KJob*)) ); + * + * @endcode + * + * @author Christian Mollekopf + * @since 4.8 + */ +class AKONADICORE_EXPORT TrashJob : public Job +{ + Q_OBJECT + +public: + + /** + * Creates a new trash job that marks @p item as trash, and moves it to the configured trash collection. + * + * If @p keepTrashInCollection is set, the item will not be moved to the configured trash collection. + * + * @param item The item to mark as trash. + * @param parent The parent object. + */ + explicit TrashJob(const Item &item, QObject *parent = Q_NULLPTR); + + /** + * Creates a new trash job that marks all items in the list + * @p items as trash, and moves it to the configured trash collection. + * The items can be in different collections/resources and will still be moved to the correct trash colleciton. + * + * If @p keepTrashInCollection is set, the item will not be moved to the configured trash collection. + * + * @param items The items to mark as trash. + * @param parent The parent object. + */ + explicit TrashJob(const Item::List &items, QObject *parent = Q_NULLPTR); + + /** + * Creates a new trash job that marks @p collection as trash, and moves it to the configured trash collection. + * The subentities of the collection are also marked as trash. + * + * If @p keepTrashInCollection is set, the item will not be moved to the configured trash collection. + * + * @param collection The collection to mark as trash. + * @param parent The parent object. + */ + explicit TrashJob(const Collection &collection, QObject *parent = Q_NULLPTR); + + ~TrashJob(); + + /** + * Ignore configured Trash collections and keep all items local + */ + void keepTrashInCollection(bool enable); + + /** + * Moves all entities to the give collection + */ + void setTrashCollection(const Collection &trashcollection); + + /** + * Delete Items which are already in trash, instead of ignoring them + */ + void deleteIfInTrash(bool enable); + + Item::List items() const; + +protected: + void doStart() Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class TrashJobPrivate; + Q_DECLARE_PRIVATE(TrashJob) + Q_PRIVATE_SLOT(d_func(), void selectResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void setAttribute(const Akonadi::Collection::List &)) + Q_PRIVATE_SLOT(d_func(), void setAttribute(const Akonadi::Item::List &)) + Q_PRIVATE_SLOT(d_func(), void setAttribute(KJob *)) + Q_PRIVATE_SLOT(d_func(), void collectionsReceived(const Akonadi::Collection::List &)) + Q_PRIVATE_SLOT(d_func(), void itemsReceived(const Akonadi::Item::List &)) + Q_PRIVATE_SLOT(d_func(), void parentCollectionReceived(const Akonadi::Collection::List &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/trashrestorejob.cpp b/src/core/jobs/trashrestorejob.cpp new file mode 100644 index 0000000..05daa79 --- /dev/null +++ b/src/core/jobs/trashrestorejob.cpp @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2011 Christian Mollekopf + * + * 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 "trashrestorejob.h" + +#include "collection.h" +#include "entitydeletedattribute.h" +#include "item.h" +#include "job_p.h" +#include "trashsettings.h" + +#include + +#include "itemdeletejob.h" +#include "collectiondeletejob.h" +#include "itemmovejob.h" +#include "collectionmovejob.h" +#include "itemmodifyjob.h" +#include "collectionmodifyjob.h" +#include "collectionfetchjob.h" +#include "itemfetchjob.h" +#include "collectionfetchscope.h" +#include "itemfetchscope.h" + +#include "akonadicore_debug.h" + +#include + +using namespace Akonadi; + +class TrashRestoreJob::TrashRestoreJobPrivate : public JobPrivate +{ +public: + TrashRestoreJobPrivate(TrashRestoreJob *parent) + : JobPrivate(parent) + { + } + + void selectResult(KJob *job); + + //Called when the target collection was fetched, + //will issue the move and the removal of the attributes if collection is valid + void targetCollectionFetched(KJob *job); + + void removeAttribute(const Akonadi::Item::List &list); + void removeAttribute(const Akonadi::Collection::List &list); + + //Called after initial fetch of items, issues fetch of target collection or removes attributes for in place restore + void itemsReceived(const Akonadi::Item::List &items); + void collectionsReceived(const Akonadi::Collection::List &collections); + + Q_DECLARE_PUBLIC(TrashRestoreJob) + + Item::List mItems; + Collection mCollection; + Collection mTargetCollection; + QHash restoreCollections; //groups items to target restore collections + +}; + +void TrashRestoreJob::TrashRestoreJobPrivate::selectResult(KJob *job) +{ + Q_Q(TrashRestoreJob); + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; // KCompositeJob takes care of errors + } + + if (!q->hasSubjobs() || (q->subjobs().contains(static_cast(q->sender())) && q->subjobs().size() == 1)) { + //qCWarning(AKONADICORE_LOG) << "trash restore finished"; + q->emitResult(); + } +} + +void TrashRestoreJob::TrashRestoreJobPrivate::targetCollectionFetched(KJob *job) +{ + Q_Q(TrashRestoreJob); + + CollectionFetchJob *fetchJob = qobject_cast (job); + Q_ASSERT(fetchJob); + const Collection::List &list = fetchJob->collections(); + + if (list.isEmpty() || !list.first().isValid() || list.first().hasAttribute()) { //target collection is invalid/not existing + + const QString res = fetchJob->property("Resource").toString(); + if (res.isEmpty()) { //There is no fallback + q->setError(Job::Unknown); + q->setErrorText(i18n("Could not find restore collection and restore resource is not available")); + q->emitResult(); + //FAIL + qCWarning(AKONADICORE_LOG) << "restore collection not available"; + return; + } + + //Try again with the root collection of the resource as fallback + CollectionFetchJob *resRootFetch = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, q); + resRootFetch->fetchScope().setResource(res); + const QVariant &var = fetchJob->property("Items"); + if (var.isValid()) { + resRootFetch->setProperty("Items", var.toInt()); + } + q->connect(resRootFetch, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*))); + q->connect(resRootFetch, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + return; + } + Q_ASSERT(list.size() == 1); + //SUCCESS + //We know where to move the entity, so remove the attributes and move them to the right location + if (!mItems.isEmpty()) { + const QVariant &var = fetchJob->property("Items"); + Q_ASSERT(var.isValid()); + const Item::List &items = restoreCollections[Collection(var.toInt())]; + + //store removed attribute if destination collection is valid or the item doesn't have a restore collection + //TODO only remove the attribute if the move job was successful (although it is unlikely that it fails since we already fetched the collection) + removeAttribute(items); + if (items.first().parentCollection() != list.first()) { + ItemMoveJob *job = new ItemMoveJob(items, list.first(), q); + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + } + } else { + Q_ASSERT(mCollection.isValid()); + //TODO only remove the attribute if the move job was successful + removeAttribute(Collection::List() << mCollection); + CollectionFetchJob *collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); + q->connect(collectionFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(collectionFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(removeAttribute(Akonadi::Collection::List))); + + if (mCollection.parentCollection() != list.first()) { + CollectionMoveJob *job = new CollectionMoveJob(mCollection, list.first(), q); + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + } + } + +} + +void TrashRestoreJob::TrashRestoreJobPrivate::itemsReceived(const Akonadi::Item::List &items) +{ + Q_Q(TrashRestoreJob); + if (items.isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid items passed")); + q->emitResult(); + return; + } + mItems = items; + + //Sort by restore collection + foreach (const Item &item, mItems) { + if (!item.hasAttribute()) { + continue; + } + //If the restore collection is invalid we restore the item in place, so we don't need to know its restore resource => we can put those cases in the same list + restoreCollections[item.attribute()->restoreCollection()].append(item); + } + + for (auto it = restoreCollections.cbegin(), e = restoreCollections.cend(); it != e; ++it) { + const Item &first = it.value().first(); + //Move the items to the correct collection if available + Collection targetCollection = it.key(); + const QString restoreResource = first.attribute()->restoreResource(); + + //Restore in place if no restore collection is set + if (!targetCollection.isValid()) { + removeAttribute(it.value()); + return; + } + + //Explicit target Q_DECL_OVERRIDEs the resource + if (mTargetCollection.isValid()) { + targetCollection = mTargetCollection; + } + + //Try to fetch the target resource to see if it is available + CollectionFetchJob *fetchJob = new CollectionFetchJob(targetCollection, Akonadi::CollectionFetchJob::Base, q); + if (!mTargetCollection.isValid()) { //explicit targets don't have a fallback + fetchJob->setProperty("Resource", restoreResource); + } + fetchJob->setProperty("Items", it.key().id()); //to find the items in restore collections again + q->connect(fetchJob, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*))); + } +} + +void TrashRestoreJob::TrashRestoreJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections) +{ + Q_Q(TrashRestoreJob); + if (collections.isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid collection passed")); + q->emitResult(); + return; + } + Q_ASSERT(collections.size() == 1); + mCollection = collections.first(); + + if (!mCollection.hasAttribute()) { + return; + } + + const QString restoreResource = mCollection.attribute()->restoreResource(); + Collection targetCollection = mCollection.attribute()->restoreCollection(); + + //Restore in place if no restore collection/resource is set + if (!targetCollection.isValid()) { + removeAttribute(Collection::List() << mCollection); + CollectionFetchJob *collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); + q->connect(collectionFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(collectionFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(removeAttribute(Akonadi::Collection::List))); + return; + } + + //Explicit target Q_DECL_OVERRIDEs the resource/configured restore collection + if (mTargetCollection.isValid()) { + targetCollection = mTargetCollection; + } + + //Fetch the target collection to check if it's valid + CollectionFetchJob *fetchJob = new CollectionFetchJob(targetCollection, CollectionFetchJob::Base, q); + if (!mTargetCollection.isValid()) { //explicit targets don't have a fallback + fetchJob->setProperty("Resource", restoreResource); + } + q->connect(fetchJob, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*))); +} + +void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute(const Akonadi::Collection::List &list) +{ + Q_Q(TrashRestoreJob); + QVectorIterator i(list); + while (i.hasNext()) { + Collection col = i.next(); + col.removeAttribute(); + + CollectionModifyJob *job = new CollectionModifyJob(col, q); + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + + ItemFetchJob *itemFetchJob = new ItemFetchJob(col, q); + itemFetchJob->fetchScope().fetchAttribute (true); + q->connect(itemFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(itemFetchJob, SIGNAL(itemsReceived(Akonadi::Item::List)), SLOT(removeAttribute(Akonadi::Item::List))); + } +} + +void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute(const Akonadi::Item::List &list) +{ + Q_Q(TrashRestoreJob); + Item::List items = list; + QMutableVectorIterator i(items); + while (i.hasNext()) { + Item &item = i.next(); + item.removeAttribute(); + ItemModifyJob *job = new ItemModifyJob(item, q); + job->setIgnorePayload(true); + q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + } + //For some reason it is not possible to apply this change to multiple items at once + //ItemModifyJob *job = new ItemModifyJob(items, q); + //q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) ); +} + +TrashRestoreJob::TrashRestoreJob(const Item &item, QObject *parent) + : Job(new TrashRestoreJobPrivate(this), parent) +{ + Q_D(TrashRestoreJob); + d->mItems << item; +} + +TrashRestoreJob::TrashRestoreJob(const Item::List &items, QObject *parent) + : Job(new TrashRestoreJobPrivate(this), parent) +{ + Q_D(TrashRestoreJob); + d->mItems = items; +} + +TrashRestoreJob::TrashRestoreJob(const Collection &collection, QObject *parent) + : Job(new TrashRestoreJobPrivate(this), parent) +{ + Q_D(TrashRestoreJob); + d->mCollection = collection; +} + +TrashRestoreJob::~TrashRestoreJob() +{ +} + +void TrashRestoreJob::setTargetCollection(const Akonadi::Collection &collection) +{ + Q_D(TrashRestoreJob); + d->mTargetCollection = collection; +} + +Item::List TrashRestoreJob::items() const +{ + Q_D(const TrashRestoreJob); + return d->mItems; +} + +void TrashRestoreJob::doStart() +{ + Q_D(TrashRestoreJob); + + //We always have to fetch the entities to ensure that the EntityDeletedAttribute is available + if (!d->mItems.isEmpty()) { + ItemFetchJob *job = new ItemFetchJob(d->mItems, this); + job->fetchScope().setCacheOnly(true); + job->fetchScope().fetchAttribute (true); + connect(job, SIGNAL(itemsReceived(Akonadi::Item::List)), this, SLOT(itemsReceived(Akonadi::Item::List))); + } else if (d->mCollection.isValid()) { + CollectionFetchJob *job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this); + connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), this, SLOT(collectionsReceived(Akonadi::Collection::List))); + } else { + qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist"; + setError(Job::Unknown); + setErrorText(i18n("No valid collection or empty itemlist")); + emitResult(); + } + +} + +#include "moc_trashrestorejob.cpp" diff --git a/src/core/jobs/trashrestorejob.h b/src/core/jobs/trashrestorejob.h new file mode 100644 index 0000000..42d1381 --- /dev/null +++ b/src/core/jobs/trashrestorejob.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2011 Christian Mollekopf + * + * 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_TRASHRESTOREJOB_H +#define AKONADI_TRASHRESTOREJOB_H + +#include "akonadicore_export.h" +#include "collection.h" +#include "job.h" +#include "item.h" + +namespace Akonadi +{ + +/** + * @short Job that restores entites from trash + * + * This job restores the given entites from trash. + * The EntityDeletedAttribute is removed and the item is restored to the stored restore collection. + * + * If the stored restore collection is not available, the root collection of the original resource is used. + * If also this is not available, setTargetCollection has to be used to restore the item to a specific collection. + * + * Example: + * + * @code + * + * const Akonadi::Item::List items = ... + * + * TrashRestoreJob *job = new TrashRestoreJob( items ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(restoreResult(KJob*)) ); + * + * @endcode + * + * @author Christian Mollekopf + * @since 4.8 + */ +class AKONADICORE_EXPORT TrashRestoreJob : public Job +{ + Q_OBJECT +public: + + /** + * All items need to be from the same resource + */ + explicit TrashRestoreJob(const Item &item, QObject *parent = Q_NULLPTR); + + explicit TrashRestoreJob(const Item::List &items, QObject *parent = Q_NULLPTR); + + explicit TrashRestoreJob(const Collection &collection, QObject *parent = Q_NULLPTR); + + ~TrashRestoreJob(); + + /** + * Sets the target collection, where the item is moved to. + * If not set the item will be restored in the collection saved in the EntityDeletedAttribute. + * @param collection the collection to set as target + */ + void setTargetCollection(const Collection &collection); + + Item::List items() const; +protected: + void doStart() Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class TrashRestoreJobPrivate; + Q_DECLARE_PRIVATE(TrashRestoreJob) + Q_PRIVATE_SLOT(d_func(), void selectResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void targetCollectionFetched(KJob *)) + Q_PRIVATE_SLOT(d_func(), void removeAttribute(const Akonadi::Collection::List &)) + Q_PRIVATE_SLOT(d_func(), void removeAttribute(const Akonadi::Item::List &)) + Q_PRIVATE_SLOT(d_func(), void collectionsReceived(const Akonadi::Collection::List &)) + Q_PRIVATE_SLOT(d_func(), void itemsReceived(const Akonadi::Item::List &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/jobs/unlinkjob.cpp b/src/core/jobs/unlinkjob.cpp new file mode 100644 index 0000000..876601d --- /dev/null +++ b/src/core/jobs/unlinkjob.cpp @@ -0,0 +1,59 @@ +/* + 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 "unlinkjob.h" + +#include "collection.h" +#include "job_p.h" +#include "linkjobimpl_p.h" + +using namespace Akonadi; + +class Akonadi::UnlinkJobPrivate : public LinkJobImpl +{ +public: + UnlinkJobPrivate(UnlinkJob *parent) + : LinkJobImpl(parent) + { + } +}; + +UnlinkJob::UnlinkJob(const Collection &collection, const Item::List &items, QObject *parent) + : Job(new UnlinkJobPrivate(this), parent) +{ + Q_D(UnlinkJob); + d->destination = collection; + d->objectsToLink = items; +} + +UnlinkJob::~UnlinkJob() +{ +} + +void UnlinkJob::doStart() +{ + Q_D(UnlinkJob); + d->sendCommand(Protocol::LinkItemsCommand::Unlink); +} + +bool UnlinkJob::doHandleResponse(qint64 tag, const Protocol::Command &response) +{ + Q_D(UnlinkJob); + return d->handleResponse(tag, response); +} diff --git a/src/core/jobs/unlinkjob.h b/src/core/jobs/unlinkjob.h new file mode 100644 index 0000000..c885418 --- /dev/null +++ b/src/core/jobs/unlinkjob.h @@ -0,0 +1,97 @@ +/* + 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_UNLINKJOB_H +#define AKONADI_UNLINKJOB_H + +#include "akonadicore_export.h" +#include "job.h" +#include "item.h" + +namespace Akonadi +{ + +class Collection; +class UnlinkJobPrivate; + +/** + * @short Job that unlinks items inside the Akonadi storage. + * + * This job allows you to remove references to a set of items in a virtual + * collection. + * + * Example: + * + * @code + * + * // Unlink the given items from the given collection + * const Akonadi::Collection virtualCollection = ... + * const Akonadi::Item::List items = ... + * + * Akonadi::UnlinkJob *job = new Akonadi::UnlinkJob( virtualCollection, items ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Unlinked items successfully"; + * } + * + * @endcode + * + * @author Volker Krause + * @since 4.2 + * @see LinkJob + */ +class AKONADICORE_EXPORT UnlinkJob : public Job +{ + Q_OBJECT +public: + /** + * Creates a new unlink job. + * + * The job will remove references to the given items from the given collection. + * + * @param collection The collection from which the references should be removed. + * @param items The items of which the references should be removed. + * @param parent The parent object. + */ + UnlinkJob(const Collection &collection, const Item::List &items, QObject *parent = Q_NULLPTR); + + /** + * Destroys the unlink job. + */ + ~UnlinkJob(); + +protected: + void doStart() Q_DECL_OVERRIDE; + bool doHandleResponse(qint64 tag, const Protocol::Command &response) Q_DECL_OVERRIDE; + +private: + Q_DECLARE_PRIVATE(UnlinkJob) + template friend class LinkJobImpl; +}; + +} + +#endif diff --git a/src/core/kcfg2dbus.xsl b/src/core/kcfg2dbus.xsl new file mode 100644 index 0000000..809ef05 --- /dev/null +++ b/src/core/kcfg2dbus.xsl @@ -0,0 +1,104 @@ + + + + +interfaceName + + + + + + + save + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + s + as + ? + (iiii) + (ii) + ? + (ii) + i + u + b + d + ((iii)(iiii)i) + x + t + ai + i + s + as + s + s + as + v + + + + + + QRect + QSize + QPoint + QDateTime + QList<int> + + + + + diff --git a/src/core/kdsignalblocker.cpp b/src/core/kdsignalblocker.cpp new file mode 100644 index 0000000..3bcab47 --- /dev/null +++ b/src/core/kdsignalblocker.cpp @@ -0,0 +1,92 @@ +/**************************************************************************** +** Copyright (C) 2001-2012 Klaralvdalens Datakonsult AB. All rights reserved. +** +** This file is part of the KD Tools library. +** +** Licensees holding valid commercial KD Tools licenses may use this file in +** accordance with the KD Tools Commercial License Agreement provided with +** the Software. +** +** +** This file may be distributed and/or modified under the terms of the GNU +** Lesser General Public License version 2 and version 3 as published by the +** Free Software Foundation and appearing in the file LICENSE.LGPL included. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** Contact info@kdab.net if any conditions of this licensing are not +** clear to you. +** +**********************************************************************/ + +#include "kdsignalblocker.h" + +#include + +using namespace Akonadi; + +/*! + \class KDSignalBlocker + \ingroup raii core + \brief Exception-safe and convenient wrapper around QObject::blockSignals() + + All methods in this class are nothrow if QObject::blockSignals() and + QObject::signalsBlocked() are nothrow, which they normally are. +*/ + +/*! + Constructor. Blocks signals on \a o. + + \post o->signalsBlocked() == true +*/ +KDSignalBlocker::KDSignalBlocker(QObject *o) + : wasBlocked(o->signalsBlocked()) + , object(o) +{ + o->blockSignals(true); +} + +/*! + \overload + + \post o.signalsBlocked() == true +*/ +KDSignalBlocker::KDSignalBlocker(QObject &o) + : wasBlocked(o.signalsBlocked()) + , object(&o) +{ + o.blockSignals(true); +} + +/*! + Destructor. Unblocks signals (unless they were blocked before), if not already + done by unblock(). + + \post o->signalsBlocked() is the same as just before this instance has been constructed. +*/ +KDSignalBlocker::~KDSignalBlocker() +{ + unblock(); +} + +/* + Unblocks signals (unless they were blocked before). + You can use reblock() to block them again. + There is no need to reblock before destruction. + + \post o->signalsBlocked() is the same as just before this instance has been constructed. +*/ +void KDSignalBlocker::unblock() +{ + object->blockSignals(wasBlocked); +} + +/* + Unblocks signals (unless they were blocked before) + \post o->signalsBlocked() is the same as just before this instance has been constructed. +*/ +void KDSignalBlocker::reblock() +{ + object->blockSignals(true); +} diff --git a/src/core/kdsignalblocker.h b/src/core/kdsignalblocker.h new file mode 100644 index 0000000..01c3135 --- /dev/null +++ b/src/core/kdsignalblocker.h @@ -0,0 +1,52 @@ +/**************************************************************************** +** Copyright (C) 2001-2012 Klaralvdalens Datakonsult AB. All rights reserved. +** +** This file is part of the KD Tools library. +** +** Licensees holding valid commercial KD Tools licenses may use this file in +** accordance with the KD Tools Commercial License Agreement provided with +** the Software. +** +** +** This file may be distributed and/or modified under the terms of the GNU +** Lesser General Public License version 2 and version 3 as published by the +** Free Software Foundation and appearing in the file LICENSE.LGPL included. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** Contact info@kdab.net if any conditions of this licensing are not +** clear to you. +** +**********************************************************************/ + +#ifndef __KDTOOLS__CORE__KDSIGNALBLOCKER_H__ +#define __KDTOOLS__CORE__KDSIGNALBLOCKER_H__ + +#include + +QT_BEGIN_NAMESPACE +class QObject; +QT_END_NAMESPACE + +namespace Akonadi +{ + +class KDSignalBlocker +{ + Q_DISABLE_COPY(KDSignalBlocker) +public: + explicit KDSignalBlocker(QObject *o); + explicit KDSignalBlocker(QObject &o); + ~KDSignalBlocker(); + + void unblock(); + void reblock(); +private: + const bool wasBlocked; + QObject *const object; +}; + +} + +#endif /* __KDTOOLS__CORE__KDSIGNALBLOCKER_H__ */ diff --git a/src/core/metatypes.h b/src/core/metatypes.h new file mode 100644 index 0000000..01a1581 --- /dev/null +++ b/src/core/metatypes.h @@ -0,0 +1,30 @@ +/* + This file is part of kdepimlibs. + + Copyright (c) 2010 Will Stephenson + + 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 METATYPES_H +#define METATYPES_H + +#include +#include + +Q_DECLARE_METATYPE(QModelIndex) + +#endif // METATYPES_H diff --git a/src/core/mimetypechecker.cpp b/src/core/mimetypechecker.cpp new file mode 100644 index 0000000..fd22e3b --- /dev/null +++ b/src/core/mimetypechecker.cpp @@ -0,0 +1,182 @@ +/* + Copyright (c) 2009 Kevin Krammer + + 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 "mimetypechecker.h" + +#include "mimetypechecker_p.h" + +#include "collection.h" +#include "item.h" + +using namespace Akonadi; + +MimeTypeChecker::MimeTypeChecker() +{ + d = new MimeTypeCheckerPrivate(); +} + +MimeTypeChecker::MimeTypeChecker(const MimeTypeChecker &other) + : d(other.d) +{ +} + +MimeTypeChecker::~MimeTypeChecker() +{ +} + +MimeTypeChecker &MimeTypeChecker::operator=(const MimeTypeChecker &other) +{ + if (&other != this) { + d = other.d; + } + + return *this; +} + +QStringList MimeTypeChecker::wantedMimeTypes() const +{ + return d->mWantedMimeTypes.values(); +} + +void MimeTypeChecker::setWantedMimeTypes(const QStringList &mimeTypes) +{ + d->mWantedMimeTypes = QSet::fromList(mimeTypes); +} + +void MimeTypeChecker::addWantedMimeType(const QString &mimeType) +{ + d->mWantedMimeTypes.insert(mimeType); +} + +void MimeTypeChecker::removeWantedMimeType(const QString &mimeType) +{ + d->mWantedMimeTypes.remove(mimeType); +} + +bool MimeTypeChecker::isWantedItem(const Item &item) const +{ + if (d->mWantedMimeTypes.isEmpty() || !item.isValid()) { + return false; + } + + const QString mimeType = item.mimeType(); + if (mimeType.isEmpty()) { + return false; + } + + return d->isWantedMimeType(mimeType); +} + +bool MimeTypeChecker::isWantedCollection(const Collection &collection) const +{ + if (d->mWantedMimeTypes.isEmpty() || !collection.isValid()) { + return false; + } + + const QStringList contentMimeTypes = collection.contentMimeTypes(); + if (contentMimeTypes.isEmpty()) { + return false; + } + + foreach (const QString &mimeType, contentMimeTypes) { + if (mimeType.isEmpty()) { + continue; + } + + if (d->isWantedMimeType(mimeType)) { + return true; + } + } + + return false; +} + +bool MimeTypeChecker::isWantedItem(const Item &item, const QString &wantedMimeType) +{ + if (wantedMimeType.isEmpty() || !item.isValid()) { + return false; + } + + const QString mimeType = item.mimeType(); + if (mimeType.isEmpty()) { + return false; + } + + if (mimeType == wantedMimeType) { + return true; + } + + QMimeDatabase db; + const QMimeType mt = db.mimeTypeForName(mimeType); + if (!mt.isValid()) { + return false; + } + + return mt.inherits(wantedMimeType); +} + +bool MimeTypeChecker::isWantedCollection(const Collection &collection, const QString &wantedMimeType) +{ + if (wantedMimeType.isEmpty() || !collection.isValid()) { + return false; + } + + const QStringList contentMimeTypes = collection.contentMimeTypes(); + if (contentMimeTypes.isEmpty()) { + return false; + } + + foreach (const QString &mimeType, contentMimeTypes) { + if (mimeType.isEmpty()) { + continue; + } + + if (mimeType == wantedMimeType) { + return true; + } + + QMimeDatabase db; + const QMimeType mt = db.mimeTypeForName(mimeType); + if (!mt.isValid()) { + continue; + } + + if (mt.inherits(wantedMimeType)) { + return true; + } + } + + return false; +} + +bool MimeTypeChecker::isWantedMimeType(const QString &mimeType) const +{ + return d->isWantedMimeType(mimeType); +} + +bool MimeTypeChecker::containsWantedMimeType(const QStringList &mimeTypes) const +{ + foreach (const QString &mt, mimeTypes) { + if (d->isWantedMimeType(mt)) { + return true; + } + } + return false; +} + diff --git a/src/core/mimetypechecker.h b/src/core/mimetypechecker.h new file mode 100644 index 0000000..0254214 --- /dev/null +++ b/src/core/mimetypechecker.h @@ -0,0 +1,257 @@ +/* + Copyright (c) 2009 Kevin Krammer + + 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 MIMETYPECHECKER_H +#define MIMETYPECHECKER_H + +#include "akonadicore_export.h" + +#include + +class QString; +class QStringList; + +namespace Akonadi +{ +class Collection; +class Item; +class MimeTypeCheckerPrivate; + +/** + * @short Helper for checking MIME types of Collections and Items. + * + * When it is necessary to decide whether an item has a certain MIME type + * or whether a collection can contain a certain MIME type, direct string + * comparison might not render the desired result because MIME types can + * have aliases and be a node in an "inheritance" hierachy. + * + * For example a check like this + * @code + * if ( item.mimeType() == QLatin1String( "text/directory" ) ) + * @endcode + * would fail to detect @c "text/x-vcard" as being the same MIME type. + * + * @note KDE deals with this inside the KMimeType framework, this class is just + * a convenience helper for common Akonadi related checks. + * + * Example: Checking whether an Akonadi::Item is contact MIME type + * @code + * Akonadi::MimeTypeChecker checker; + * checker.addWantedMimeType( KContacts::Addressee::mimeType() ); + * + * if ( checker.isWantedItem( item ) ){ + * // item.mimeType() is equal KContacts::Addressee::mimeType(), an aliases + * // or a sub type. + * } + * @endcode + * + * Example: Checking whether an Akonadi::Collection could contain calendar + * items + * @code + * Akonadi::MimeTypeChecker checker; + * checker.addWantedMimeType( QLatin1String( "text/calendar" ) ); + * + * if ( checker.isWantedCollection( collection ) ) { + * // collection.contentMimeTypes() contains @c "text/calendar" + * // or a sub type. + * } + * @endcode + * + * Example: Checking whether an Akonadi::Collection could contain + * Calendar Event items (i.e. KCal::Event), making use of the respective + * MIME type "subclassing" provided by Akonadi's MIME type extensions. + * @code + * Akonadi::MimeTypeChecker checker; + * checker.addWantedMimeType( QLatin1String( "application/x-vnd.akonadi.calendar.event" ) ); + * + * if ( checker.isWantedCollection( collection ) ) { + * // collection.contentMimeTypes() contains @c "application/x-vnd.akonadi.calendar.event" + * // or a sub type, but just containing @c "text/calendar" would not + * // get here + * } + * @endcode + * + * Example: Checking for items of more than one MIME type and treat one + * of them specially. + * @code + * Akonadi::MimeTypeChecker mimeFilter; + * mimeFilter.setWantedMimeTypes( QStringList() << KContacts::Addressee::mimeType() + * << KContacts::ContactGroup::mimeType() ); + * + * if ( mimeFilter.isWantedItem( item ) ) { + * if ( Akonadi::MimeTypeChecker::isWantedItem( item, KContacts::ContactGroup::mimeType() ) { + * // treat contact group's differently + * } + * } + * @endcode + * + * This class is implicitly shared. + * + * @author Kevin Krammer + * + * @since 4.3 + */ +class AKONADICORE_EXPORT MimeTypeChecker +{ +public: + /** + * Creates an empty MIME type checker. + * + * An empty checker will not report any items or collections as wanted. + */ + MimeTypeChecker(); + + /** + * Creates a new MIME type checker from an @p other. + */ + MimeTypeChecker(const MimeTypeChecker &other); + + /** + * Destroys the MIME type checker. + */ + ~MimeTypeChecker(); + + /** + * Assigns the @p other to this checker and returns a reference to this checker. + */ + MimeTypeChecker &operator=(const MimeTypeChecker &other); + + /** + * Returns the list of wanted MIME types this instance checks against. + * + * @see setWantedMimeTypes() + */ + QStringList wantedMimeTypes() const; + + /** + * Sets the list of wanted MIME types this instance checks against. + * + * @param mimeTypes The list of MIME types to check against. + * + * @see wantedMimeTypes() + */ + void setWantedMimeTypes(const QStringList &mimeTypes); + + /** + * Adds another MIME type to the list of wanted MIME types this instance checks against. + * + * @param mimeType The MIME types to add to the checklist. + * + * @see setWantedMimeTypes() + */ + void addWantedMimeType(const QString &mimeType); + + /** + * Removes a MIME type from the list of wanted MIME types this instance checks against. + * + * @param mimeType The MIME type to remove from the checklist. + * + * @see addWantedMimeType() + */ + void removeWantedMimeType(const QString &mimeType); + + /** + * Checks whether a given @p item has one of the wanted MIME types + * + * @param item The item to check the MIME type of. + * + * @return @c true if the @p item MIME type is one of the wanted ones, + * @c false if it isn't, the item is invalid or has an empty MIME type. + * + * @see setWantedMimeTypes() + * @see Item::mimeType() + */ + bool isWantedItem(const Item &item) const; + + /** + * Checks whether a given @p collection has one of the wanted MIME types + * + * @param collection The collection to check the content MIME types of. + * + * @return @c true if one of the @p collection content MIME types is + * one of the wanted ones, @c false if non is, the collection + * is invalid or has an empty content MIME type list. + * + * @see setWantedMimeTypes() + * @see Collection::contentMimeTypes() + */ + bool isWantedCollection(const Collection &collection) const; + + /** + * Checks whether a given mime type is covered by one of the wanted MIME types. + * + * @param mimeType The mime type to check. + * + * @return @c true if the mime type @p mimeType is coverd by one of the + * wanted MIME types, @c false otherwise. + * + * @since 4.6 + */ + bool isWantedMimeType(const QString &mimeType) const; + + /** + * Checks whether any of the given MIME types is covered by one of the wanted MIME types. + * + * @param mimeTypes The MIME types to check. + * + * @return @c true if any of the MIME types in @p mimeTypes is coverd by one of the + * wanted MIME types, @c false otherwise. + * + * @since 4.6 + */ + bool containsWantedMimeType(const QStringList &mimeTypes) const; + + /** + * Checks whether a given @p item has the given wanted MIME type + * + * @param item The item to check the MIME type of. + * @param wantedMimeType The MIME type to check against. + * + * @return @c true if the @p item MIME type is the given one, + * @c false if it isn't, the item is invalid or has an empty MIME type. + * + * @see setWantedMimeTypes() + * @see Item::mimeType() + */ + static bool isWantedItem(const Item &item, const QString &wantedMimeType); + + /** + * Checks whether a given @p collection has the given MIME type + * + * @param collection The collection to check the content MIME types of. + * @param wantedMimeType The MIME type to check against. + * + * @return @c true if one of the @p collection content MIME types is + * the given wanted one, @c false if it isn't, the collection + * is invalid or has an empty content MIME type list. + * + * @see setWantedMimeTypes() + * @see Collection::contentMimeTypes() + */ + static bool isWantedCollection(const Collection &collection, const QString &wantedMimeType); + +private: + //@cond PRIVATE + QSharedDataPointer d; + //@endcond +}; + +} + +#endif diff --git a/src/core/mimetypechecker_p.h b/src/core/mimetypechecker_p.h new file mode 100644 index 0000000..7f58829 --- /dev/null +++ b/src/core/mimetypechecker_p.h @@ -0,0 +1,75 @@ +/* + Copyright (c) 2009 Kevin Krammer + + 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 MIMETYPECHECKER_P_H +#define MIMETYPECHECKER_P_H + +#include +#include + +#include +#include + +namespace Akonadi +{ + +/** + * @internal + */ +class MimeTypeCheckerPrivate : public QSharedData +{ +public: + MimeTypeCheckerPrivate() + { + } + + MimeTypeCheckerPrivate(const MimeTypeCheckerPrivate &other) + : QSharedData(other) + { + mWantedMimeTypes = other.mWantedMimeTypes; + } + + bool isWantedMimeType(const QString &mimeType) const + { + if (mWantedMimeTypes.contains(mimeType)) { + return true; + } + + QMimeDatabase db; + const QMimeType mt = db.mimeTypeForName(mimeType); + if (!mt.isValid()) { + return false; + } + + foreach (const QString &wantedMimeType, mWantedMimeTypes) { + if (mt.inherits(wantedMimeType)) { + return true; + } + } + + return false; + } + +public: + QSet mWantedMimeTypes; +}; + +} + +#endif diff --git a/src/core/models/agentfilterproxymodel.cpp b/src/core/models/agentfilterproxymodel.cpp new file mode 100644 index 0000000..aaad7c8 --- /dev/null +++ b/src/core/models/agentfilterproxymodel.cpp @@ -0,0 +1,164 @@ +/* + 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 "agentfilterproxymodel.h" + +#include "agenttypemodel.h" +#include "agentinstancemodel.h" + +#include + +#include +#include +#include + +using namespace Akonadi; + +// ensure the role numbers are equivalent for both source models +static_assert((int)AgentTypeModel::CapabilitiesRole == (int)AgentInstanceModel::CapabilitiesRole, + "AgentTypeModel::CapabilitiesRole does not match AgentInstanceModel::CapabilitiesRole"); +static_assert((int)AgentTypeModel::MimeTypesRole == (int)AgentInstanceModel::MimeTypesRole, + "AgentTypeModel::MimeTypesRole does not match AgentInstanceModel::MimeTypesRole"); + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentFilterProxyModel::Private +{ +public: + QStringList mimeTypes; + QStringList capabilities; + QStringList excludeCapabilities; + bool filterAcceptRegExp(const QModelIndex &index, const QRegExp &filterRegExpStr); +}; + +AgentFilterProxyModel::AgentFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d(new Private) +{ + setDynamicSortFilter(true); +} + +AgentFilterProxyModel::~AgentFilterProxyModel() +{ + delete d; +} + +void AgentFilterProxyModel::addMimeTypeFilter(const QString &mimeType) +{ + d->mimeTypes << mimeType; + invalidateFilter(); +} + +void AgentFilterProxyModel::addCapabilityFilter(const QString &capability) +{ + d->capabilities << capability; + invalidateFilter(); +} + +void AgentFilterProxyModel::excludeCapabilities(const QString &capability) +{ + d->excludeCapabilities << capability; + invalidateFilter(); +} + +void AgentFilterProxyModel::clearFilters() +{ + d->capabilities.clear(); + d->mimeTypes.clear(); + d->excludeCapabilities.clear(); + invalidateFilter(); +} + +bool AgentFilterProxyModel::Private::filterAcceptRegExp(const QModelIndex &index, const QRegExp &filterRegExpStr) +{ + // First see if the name matches a set regexp filter. + if (!filterRegExpStr.isEmpty()) { + if (index.data(AgentTypeModel::IdentifierRole).toString().contains(filterRegExpStr)) { + return true; + } else if (index.data().toString().contains(filterRegExpStr)) { + return true; + } else { + return false; + } + } + return true; +} + +bool AgentFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &) const +{ + const QModelIndex index = sourceModel()->index(row, 0); + + if (!d->mimeTypes.isEmpty()) { + QMimeDatabase mimeDb; + bool found = false; + foreach (const QString &mimeType, index.data(AgentTypeModel::MimeTypesRole).toStringList()) { + if (d->mimeTypes.contains(mimeType)) { + found = true; + } else { + const QMimeType mt = mimeDb.mimeTypeForName(mimeType); + if (mt.isValid()) { + foreach (const QString &type, d->mimeTypes) { + if (mt.inherits(type)) { + found = true; + break; + } + } + } + } + + if (found) { + break; + } + } + + if (!found) { + return false; + } + } + + if (!d->capabilities.isEmpty()) { + bool found = false; + foreach (const QString &capability, index.data(AgentTypeModel::CapabilitiesRole).toStringList()) { + if (d->capabilities.contains(capability)) { + found = true; + break; + } + } + + if (!found) { + return false; + } + + if (found && !d->excludeCapabilities.isEmpty()) { + foreach (const QString &capability, index.data(AgentTypeModel::CapabilitiesRole).toStringList()) { + if (d->excludeCapabilities.contains(capability)) { + found = false; + break; + } + } + + if (!found) { + return false; + } + } + } + + return d->filterAcceptRegExp(index, filterRegExp()); +} diff --git a/src/core/models/agentfilterproxymodel.h b/src/core/models/agentfilterproxymodel.h new file mode 100644 index 0000000..6348a8a --- /dev/null +++ b/src/core/models/agentfilterproxymodel.h @@ -0,0 +1,103 @@ +/* + 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_AGENTFILTERPROXYMODEL_H +#define AKONADI_AGENTFILTERPROXYMODEL_H + +#include "akonadicore_export.h" +#include + +namespace Akonadi +{ + +/** + * @short A proxy model for filtering AgentType or AgentInstance + * + * This filter proxy model works on top of a AgentTypeModel or AgentInstanceModel + * and can be used to show only AgentType or AgentInstance objects + * which provide a given mime type or capability. + * + * @code + * + * // Show only running agent instances that provide contacts + * Akonadi::AgentInstanceModel *model = new Akonadi::AgentInstanceModel( this ); + * + * Akonadi::AgentFilterProxyModel *proxy = new Akonadi::AgentFilterProxyModel( this ); + * proxy->addMimeTypeFilter( "text/directory" ); + * + * proxy->setSourceModel( model ); + * + * QListView *view = new QListView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT AgentFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + /** + * Create a new agent filter proxy model. + * By default no filtering is done. + * @param parent parent object + */ + explicit AgentFilterProxyModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the agent filter proxy model. + */ + ~AgentFilterProxyModel(); + + /** + * Accept agents supporting @p mimeType. + */ + void addMimeTypeFilter(const QString &mimeType); + + /** + * Accept agents with the given @p capability. + */ + void addCapabilityFilter(const QString &capability); + + /** + * Clear the filters ( mimeTypes & capabilities ). + */ + void clearFilters(); + + /** + * Excludes agents with the given @p capability. + * @param capability undesired agent capability + * @since 4.6 + */ + void excludeCapabilities(const QString &capability); + +protected: + bool filterAcceptsRow(int row, const QModelIndex &parent) const Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/models/agentinstancemodel.cpp b/src/core/models/agentinstancemodel.cpp new file mode 100644 index 0000000..aeb54ba --- /dev/null +++ b/src/core/models/agentinstancemodel.cpp @@ -0,0 +1,252 @@ +/* + Copyright (c) 2006 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. +*/ + +#include "agentinstancemodel.h" + +#include "agentinstance.h" +#include "agentmanager.h" + +#include +#include + +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentInstanceModel::Private +{ +public: + Private(AgentInstanceModel *parent) + : mParent(parent) + { + } + + AgentInstanceModel *mParent; + AgentInstance::List mInstances; + + void instanceAdded(const AgentInstance &); + void instanceRemoved(const AgentInstance &); + void instanceChanged(const AgentInstance &); +}; + +void AgentInstanceModel::Private::instanceAdded(const AgentInstance &instance) +{ + mParent->beginInsertRows(QModelIndex(), mInstances.count(), mInstances.count()); + mInstances.append(instance); + mParent->endInsertRows(); +} + +void AgentInstanceModel::Private::instanceRemoved(const AgentInstance &instance) +{ + const int index = mInstances.indexOf(instance); + if (index == -1) { + return; + } + + mParent->beginRemoveRows(QModelIndex(), index, index); + mInstances.removeAll(instance); + mParent->endRemoveRows(); +} + +void AgentInstanceModel::Private::instanceChanged(const AgentInstance &instance) +{ + const int numberOfInstance(mInstances.count()); + for (int i = 0; i < numberOfInstance; ++i) { + if (mInstances[i] == instance) { + mInstances[i] = instance; + + const QModelIndex idx = mParent->index(i, 0); + emit mParent->dataChanged(idx, idx); + + return; + } + } +} + +AgentInstanceModel::AgentInstanceModel(QObject *parent) + : QAbstractItemModel(parent) + , d(new Private(this)) +{ + d->mInstances = AgentManager::self()->instances(); + + connect(AgentManager::self(), SIGNAL(instanceAdded(Akonadi::AgentInstance)), + this, SLOT(instanceAdded(Akonadi::AgentInstance))); + connect(AgentManager::self(), SIGNAL(instanceRemoved(Akonadi::AgentInstance)), + this, SLOT(instanceRemoved(Akonadi::AgentInstance))); + connect(AgentManager::self(), SIGNAL(instanceStatusChanged(Akonadi::AgentInstance)), + this, SLOT(instanceChanged(Akonadi::AgentInstance))); + connect(AgentManager::self(), SIGNAL(instanceProgressChanged(Akonadi::AgentInstance)), + this, SLOT(instanceChanged(Akonadi::AgentInstance))); + connect(AgentManager::self(), SIGNAL(instanceNameChanged(Akonadi::AgentInstance)), + this, SLOT(instanceChanged(Akonadi::AgentInstance))); + connect(AgentManager::self(), SIGNAL(instanceOnline(Akonadi::AgentInstance,bool)), + this, SLOT(instanceChanged(Akonadi::AgentInstance))); +} + +AgentInstanceModel::~AgentInstanceModel() +{ + delete d; +} + +QHash AgentInstanceModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles.insert(StatusRole, "status"); + roles.insert(StatusMessageRole, "statusMessage"); + roles.insert(ProgressRole, "progress"); + roles.insert(OnlineRole, "online"); + return roles; +} + +int AgentInstanceModel::columnCount(const QModelIndex &index) const +{ + return index.isValid() ? 0 : 1; +} + +int AgentInstanceModel::rowCount(const QModelIndex &index) const +{ + return index.isValid() ? 0 : d->mInstances.count(); +} + +QVariant AgentInstanceModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (index.row() < 0 || index.row() >= d->mInstances.count()) { + return QVariant(); + } + + const AgentInstance &instance = d->mInstances[index.row()]; + + switch (role) { + case Qt::DisplayRole: + return instance.name(); + case Qt::DecorationRole: + return instance.type().icon(); + case InstanceRole: { + QVariant var; + var.setValue(instance); + return var; + } + case InstanceIdentifierRole: + return instance.identifier(); + case Qt::ToolTipRole: + return QStringLiteral("

%1

%2
").arg(instance.name(), instance.type().description()); + case StatusRole: + return instance.status(); + case StatusMessageRole: + return instance.statusMessage(); + case ProgressRole: + return instance.progress(); + case OnlineRole: + return instance.isOnline(); + case TypeRole: { + QVariant var; + var.setValue(instance.type()); + return var; + } + case TypeIdentifierRole: + return instance.type().identifier(); + case DescriptionRole: + return instance.type().description(); + case CapabilitiesRole: + return instance.type().capabilities(); + case MimeTypesRole: + return instance.type().mimeTypes(); + } + return QVariant(); +} + +QVariant AgentInstanceModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) { + return QVariant(); + } + + if (role != Qt::DisplayRole) { + return QVariant(); + } + + switch (section) { + case 0: + return i18nc("@title:column, name of a thing", "Name"); + break; + default: + return QVariant(); + break; + } +} + +QModelIndex AgentInstanceModel::index(int row, int column, const QModelIndex &) const +{ + if (row < 0 || row >= d->mInstances.count()) { + return QModelIndex(); + } + + if (column != 0) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex AgentInstanceModel::parent(const QModelIndex &) const +{ + return QModelIndex(); +} + +Qt::ItemFlags AgentInstanceModel::flags(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= d->mInstances.count()) { + return QAbstractItemModel::flags(index); + } + + return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; +} + +bool AgentInstanceModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return false; + } + + if (index.row() < 0 || index.row() >= d->mInstances.count()) { + return false; + } + + AgentInstance &instance = d->mInstances[index.row()]; + + switch (role) { + case OnlineRole: + instance.setIsOnline(value.toBool()); + emit dataChanged(index, index); + return true; + default: + return false; + } + + return false; +} + +#include "moc_agentinstancemodel.cpp" diff --git a/src/core/models/agentinstancemodel.h b/src/core/models/agentinstancemodel.h new file mode 100644 index 0000000..3e32530 --- /dev/null +++ b/src/core/models/agentinstancemodel.h @@ -0,0 +1,109 @@ +/* + Copyright (c) 2006-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_AGENTINSTANCEMODEL_H +#define AKONADI_AGENTINSTANCEMODEL_H + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ + +/** + * @short Provides a data model for agent instances. + * + * This class provides the interface of a QAbstractItemModel to + * access all available agent instances: their name, identifier, + * supported mimetypes and capabilities. + * + * @code + * + * Akonadi::AgentInstanceModel *model = new Akonadi::AgentInstanceModel( this ); + * + * QListView *view = new QListView( this ); + * view->setModel( model ); + * + * @endcode + * + * To show only agent instances that match a given mime type or special + * capabilities, use the AgentFilterProxyModel on top of this model. + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentInstanceModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + /** + * Describes the roles of this model. + */ + enum Roles { + TypeRole = Qt::UserRole + 1, ///< The agent type itself + TypeIdentifierRole, ///< The identifier of the agent type + DescriptionRole, ///< A description of the agent type + MimeTypesRole, ///< A list of supported mimetypes + CapabilitiesRole, ///< A list of supported capabilities + InstanceRole, ///< The agent instance itself + InstanceIdentifierRole, ///< The identifier of the agent instance + StatusRole, ///< The current status (numerical) of the instance + StatusMessageRole, ///< A textual presentation of the current status + ProgressRole, ///< The current progress (numerical in percent) of an operation + OnlineRole, ///< The current online/offline status + UserRole = Qt::UserRole + 42 ///< Role for user extensions + }; + + /** + * Creates a new agent instance model. + * + * @param parent The parent object. + */ + explicit AgentInstanceModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the agent instance model. + */ + virtual ~AgentInstanceModel(); + + QHash roleNames() const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + bool setData(const QModelIndex &index, const QVariant &value, int role) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void instanceAdded(const Akonadi::AgentInstance &)) + Q_PRIVATE_SLOT(d, void instanceRemoved(const Akonadi::AgentInstance &)) + Q_PRIVATE_SLOT(d, void instanceChanged(const Akonadi::AgentInstance &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/models/agenttypemodel.cpp b/src/core/models/agenttypemodel.cpp new file mode 100644 index 0000000..7ea0763 --- /dev/null +++ b/src/core/models/agenttypemodel.cpp @@ -0,0 +1,162 @@ +/* + Copyright (c) 2006 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. +*/ + +#include "agenttypemodel.h" +#include "agenttype.h" +#include "agentmanager.h" + +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentTypeModel::Private +{ +public: + Private(AgentTypeModel *parent) + : mParent(parent) + { + mTypes = AgentManager::self()->types(); + } + + AgentTypeModel *mParent; + AgentType::List mTypes; + + void typeAdded(const AgentType &agentType); + void typeRemoved(const AgentType &agentType); +}; + +void AgentTypeModel::Private::typeAdded(const AgentType &agentType) +{ + mTypes.append(agentType); + + emit mParent->layoutChanged(); +} + +void AgentTypeModel::Private::typeRemoved(const AgentType &agentType) +{ + mTypes.removeAll(agentType); + + emit mParent->layoutChanged(); +} + +AgentTypeModel::AgentTypeModel(QObject *parent) + : QAbstractItemModel(parent) + , d(new Private(this)) +{ + connect(AgentManager::self(), SIGNAL(typeAdded(Akonadi::AgentType)), + this, SLOT(typeAdded(Akonadi::AgentType))); + connect(AgentManager::self(), SIGNAL(typeRemoved(Akonadi::AgentType)), + this, SLOT(typeRemoved(Akonadi::AgentType))); +} + +AgentTypeModel::~AgentTypeModel() +{ + delete d; +} + +int AgentTypeModel::columnCount(const QModelIndex &) const +{ + return 1; +} + +int AgentTypeModel::rowCount(const QModelIndex &) const +{ + return d->mTypes.count(); +} + +QVariant AgentTypeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (index.row() < 0 || index.row() >= d->mTypes.count()) { + return QVariant(); + } + + const AgentType &type = d->mTypes[index.row()]; + + switch (role) { + case Qt::DisplayRole: + return type.name(); + break; + case Qt::DecorationRole: + return type.icon(); + break; + case TypeRole: { + QVariant var; + var.setValue(type); + return var; + break; + } + case IdentifierRole: + return type.identifier(); + break; + case DescriptionRole: + return type.description(); + break; + case MimeTypesRole: + return type.mimeTypes(); + break; + case CapabilitiesRole: + return type.capabilities(); + break; + default: + break; + } + return QVariant(); +} + +QModelIndex AgentTypeModel::index(int row, int column, const QModelIndex &) const +{ + if (row < 0 || row >= d->mTypes.count()) { + return QModelIndex(); + } + + if (column != 0) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex AgentTypeModel::parent(const QModelIndex &) const +{ + return QModelIndex(); +} + +Qt::ItemFlags AgentTypeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= d->mTypes.count()) { + return QAbstractItemModel::flags(index); + } + + const AgentType &type = d->mTypes[index.row()]; + if (type.capabilities().contains(QStringLiteral("Unique")) && + AgentManager::self()->instance(type.identifier()).isValid()) { + return QAbstractItemModel::flags(index) & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } + return QAbstractItemModel::flags(index); +} + +#include "moc_agenttypemodel.cpp" diff --git a/src/core/models/agenttypemodel.h b/src/core/models/agenttypemodel.h new file mode 100644 index 0000000..da314e9 --- /dev/null +++ b/src/core/models/agenttypemodel.h @@ -0,0 +1,97 @@ +/* + Copyright (c) 2006-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_AGENTTYPEMODEL_H +#define AKONADI_AGENTTYPEMODEL_H + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ + +/** + * @short Provides a data model for agent types. + * + * This class provides the interface of a QAbstractItemModel to + * access all available agent types: their name, identifier, + * supported mimetypes and capabilities. + * + * @code + * + * Akonadi::AgentTypeModel *model = new Akonadi::AgentTypeModel( this ); + * + * QListView *view = new QListView( this ); + * view->setModel( model ); + * + * @endcode + * + * To show only agent types that match a given mime type or special + * capabilities, use the AgentFilterProxyModel on top of this model. + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentTypeModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + /** + * Describes the roles of this model. + */ + enum Roles { + TypeRole = Qt::UserRole + 1, ///< The agent type itself + IdentifierRole, ///< The identifier of the agent type + DescriptionRole, ///< A description of the agent type + MimeTypesRole, ///< A list of supported mimetypes + CapabilitiesRole, ///< A list of supported capabilities + UserRole = Qt::UserRole + 42 ///< Role for user extensions + }; + + /** + * Creates a new agent type model. + */ + explicit AgentTypeModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the agent type model. + */ + virtual ~AgentTypeModel(); + + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void typeAdded(const Akonadi::AgentType &)) + Q_PRIVATE_SLOT(d, void typeRemoved(const Akonadi::AgentType &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/models/collectionfilterproxymodel.cpp b/src/core/models/collectionfilterproxymodel.cpp new file mode 100644 index 0000000..316a309 --- /dev/null +++ b/src/core/models/collectionfilterproxymodel.cpp @@ -0,0 +1,181 @@ +/* + Copyright (c) 2007 Bruno Virlet + + 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 "collectionfilterproxymodel.h" +#include "akonadicore_debug.h" +#include "entitytreemodel.h" +#include "mimetypechecker.h" + +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN CollectionFilterProxyModel::Private +{ +public: + Private(CollectionFilterProxyModel *parent) + : mParent(parent) + , mExcludeVirtualCollections(false) + { + mimeChecker.addWantedMimeType(QStringLiteral("text/uri-list")); + } + + bool collectionAccepted(const QModelIndex &index, bool checkResourceVisibility = true); + + QVector< QModelIndex > acceptedResources; + CollectionFilterProxyModel *mParent; + MimeTypeChecker mimeChecker; + bool mExcludeVirtualCollections; +}; + +bool CollectionFilterProxyModel::Private::collectionAccepted(const QModelIndex &index, bool checkResourceVisibility) +{ + // Retrieve supported mimetypes + const Collection collection = mParent->sourceModel()->data(index, EntityTreeModel::CollectionRole).value(); + + if (!collection.isValid()) { + return false; + } + + if (collection.isVirtual() && mExcludeVirtualCollections) { + return false; + } + + // If this collection directly contains one valid mimetype, it is accepted + if (mimeChecker.isWantedCollection(collection)) { + // The folder will be accepted, but we need to make sure the resource is visible too. + if (checkResourceVisibility) { + + // find the resource + QModelIndex resource = index; + while (resource.parent().isValid()) { + resource = resource.parent(); + } + + // See if that resource is visible, if not, invalidate the filter. + if (resource != index && !acceptedResources.contains(resource)) { + qCDebug(AKONADICORE_LOG) << "We got a new collection:" << mParent->sourceModel()->data(index).toString() + << "but the resource is not visible:" << mParent->sourceModel()->data(resource).toString(); + acceptedResources.clear(); + // defer reset, the model might still be supplying new items at this point which crashs + mParent->invalidateFilter(); + return true; + } + } + + // Keep track of all the resources that are visible. + if (!index.parent().isValid()) { + acceptedResources.append(index); + } + + return true; + } + + // If this collection has a child which contains valid mimetypes, it is accepted + QModelIndex childIndex = index.child(0, 0); + while (childIndex.isValid()) { + if (collectionAccepted(childIndex, false /* don't check visibility of the parent, as we are checking the child now */)) { + + // Keep track of all the resources that are visible. + if (!index.parent().isValid()) { + acceptedResources.append(index); + } + + return true; + } + childIndex = childIndex.sibling(childIndex.row() + 1, 0); + } + + // Or else, no reason to keep this collection. + return false; +} + +CollectionFilterProxyModel::CollectionFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d(new Private(this)) +{ +} + +CollectionFilterProxyModel::~CollectionFilterProxyModel() +{ + delete d; +} + +void CollectionFilterProxyModel::addMimeTypeFilters(const QStringList &typeList) +{ + QStringList mimeTypes = d->mimeChecker.wantedMimeTypes() + typeList; + d->mimeChecker.setWantedMimeTypes(mimeTypes); + invalidateFilter(); +} + +void CollectionFilterProxyModel::addMimeTypeFilter(const QString &type) +{ + d->mimeChecker.addWantedMimeType(type); + invalidateFilter(); +} + +bool CollectionFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + return d->collectionAccepted(sourceModel()->index(sourceRow, 0, sourceParent)); +} + +QStringList CollectionFilterProxyModel::mimeTypeFilters() const +{ + return d->mimeChecker.wantedMimeTypes(); +} + +void CollectionFilterProxyModel::clearFilters() +{ + d->mimeChecker = MimeTypeChecker(); + invalidateFilter(); +} + +void CollectionFilterProxyModel::setExcludeVirtualCollections(bool exclude) +{ + if (exclude != d->mExcludeVirtualCollections) { + d->mExcludeVirtualCollections = exclude; + invalidateFilter(); + } +} + +bool CollectionFilterProxyModel::excludeVirtualCollections() const +{ + return d->mExcludeVirtualCollections; +} + +Qt::ItemFlags CollectionFilterProxyModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) { + // Don't crash + return 0; + } + + const Collection collection = sourceModel()->data(mapToSource(index), EntityTreeModel::CollectionRole).value(); + + // If this collection directly contains one valid mimetype, it is accepted + if (d->mimeChecker.isWantedCollection(collection)) { + return QSortFilterProxyModel::flags(index); + } else { + return QSortFilterProxyModel::flags(index) & ~(Qt::ItemIsSelectable); + } +} diff --git a/src/core/models/collectionfilterproxymodel.h b/src/core/models/collectionfilterproxymodel.h new file mode 100644 index 0000000..7b90d49 --- /dev/null +++ b/src/core/models/collectionfilterproxymodel.h @@ -0,0 +1,124 @@ +/* + Copyright (c) 2007 Bruno Virlet + + 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_COLLECTIONFILTERPROXYMODEL_H +#define AKONADI_COLLECTIONFILTERPROXYMODEL_H + +#include "akonadicore_export.h" +#include + +namespace Akonadi +{ + +class CollectionModel; + +/** + * @short A proxy model that filters collections by mime type. + * + * This class can be used on top of a CollectionModel to filter out + * all collections that doesn't match a given mime type. + * + * For instance, a mail application will use addMimeType( "message/rfc822" ) to only show + * collections containing mail. + * + * @code + * + * Akonadi::CollectionModel *model = new Akonadi::CollectionModel( this ); + * + * Akonadi::CollectionFilterProxyModel *proxy = new Akonadi::CollectionFilterProxyModel(); + * proxy->addMimeTypeFilter( "message/rfc822" ); + * proxy->setSourceModel( model ); + * + * QTreeView *view = new QTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @author Bruno Virlet + */ +class AKONADICORE_EXPORT CollectionFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new collection proxy filter model. + * + * @param parent The parent object. + */ + explicit CollectionFilterProxyModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the collection proxy filter model. + */ + virtual ~CollectionFilterProxyModel(); + + /** + * Adds a list of mime types to be shown by the filter. + * + * @param mimeTypes A list of mime types to be shown. + */ + void addMimeTypeFilters(const QStringList &mimeTypes); + + /** + * Adds a mime type to be shown by the filter. + * + * @param mimeType A mime type to be shown. + */ + void addMimeTypeFilter(const QString &mimeType); + + /** + * Returns the list of mime type filters. + */ + QStringList mimeTypeFilters() const; + + /** + * Sets whether we want virtual collections to be filtered or not. + * By default, virtual collections are accepted. + * + * @param exclude If true, virtual collections aren't accepted. + * + * @since 4.7 + */ + void setExcludeVirtualCollections(bool exclude); + /* + * @since 4.12 + */ + bool excludeVirtualCollections() const; + + /** + * Clears all mime type filters. + */ + void clearFilters(); + + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/models/collectionmodel.cpp b/src/core/models/collectionmodel.cpp new file mode 100644 index 0000000..d6fad62 --- /dev/null +++ b/src/core/models/collectionmodel.cpp @@ -0,0 +1,322 @@ +/* + Copyright (c) 2006 - 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 "collectionmodel.h" +#include "collectionmodel_p.h" + +#include "collectionutils.h" +#include "collectionmodifyjob.h" +#include "entitydisplayattribute.h" +#include "monitor.h" +#include "pastehelper_p.h" +#include "session.h" + +#include +#include +#include + +#include + +using namespace Akonadi; + +CollectionModel::CollectionModel(QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(new CollectionModelPrivate(this)) +{ + Q_D(CollectionModel); + d->init(); +} + +//@cond PRIVATE +CollectionModel::CollectionModel(CollectionModelPrivate *d, QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(d) +{ + d->init(); +} +//@endcond + +CollectionModel::~CollectionModel() +{ + Q_D(CollectionModel); + d->childCollections.clear(); + d->collections.clear(); + + delete d->monitor; + d->monitor = 0; + + delete d; +} + +int CollectionModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid() && parent.column() != 0) { + return 0; + } + return 1; +} + +QVariant CollectionModel::data(const QModelIndex &index, int role) const +{ + Q_D(const CollectionModel); + if (!index.isValid()) { + return QVariant(); + } + + const Collection col = d->collections.value(index.internalId()); + if (!col.isValid()) { + return QVariant(); + } + + if (index.column() == 0 && (role == Qt::DisplayRole || role == Qt::EditRole)) { + return col.displayName(); + } + + switch (role) { + case Qt::DecorationRole: + if (index.column() == 0) { + return d->iconForCollection(col); + } + break; + case OldCollectionIdRole: // fall-through + case CollectionIdRole: + return col.id(); + case OldCollectionRole: // fall-through + case CollectionRole: + return QVariant::fromValue(col); + } + return QVariant(); +} + +QModelIndex CollectionModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_D(const CollectionModel); + if (column >= columnCount() || column < 0) { + return QModelIndex(); + } + + QVector list; + if (!parent.isValid()) { + list = d->childCollections.value(Collection::root().id()); + } else { + if (parent.column() > 0) { + return QModelIndex(); + } + list = d->childCollections.value(parent.internalId()); + } + + if (row < 0 || row >= list.size()) { + return QModelIndex(); + } + if (!d->collections.contains(list.at(row))) { + return QModelIndex(); + } + return createIndex(row, column, reinterpret_cast(d->collections.value(list.at(row)).id())); +} + +QModelIndex CollectionModel::parent(const QModelIndex &index) const +{ + Q_D(const CollectionModel); + if (!index.isValid()) { + return QModelIndex(); + } + + const Collection col = d->collections.value(index.internalId()); + if (!col.isValid()) { + return QModelIndex(); + } + + const Collection parentCol = d->collections.value(col.parentCollection().id()); + if (!parentCol.isValid()) { + return QModelIndex(); + } + QVector list; + list = d->childCollections.value(parentCol.parentCollection().id()); + + int parentRow = list.indexOf(parentCol.id()); + if (parentRow < 0) { + return QModelIndex(); + } + + return createIndex(parentRow, 0, reinterpret_cast(parentCol.id())); +} + +int CollectionModel::rowCount(const QModelIndex &parent) const +{ + const Q_D(CollectionModel); + QVector list; + if (parent.isValid()) { + list = d->childCollections.value(parent.internalId()); + } else { + list = d->childCollections.value(Collection::root().id()); + } + + return list.size(); +} + +QVariant CollectionModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + const Q_D(CollectionModel); + + if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) { + return d->headerContent; + } + return QAbstractItemModel::headerData(section, orientation, role); +} + +bool CollectionModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) +{ + Q_D(CollectionModel); + + if (section == 0 && orientation == Qt::Horizontal && role == Qt::EditRole) { + d->headerContent = value.toString(); + return true; + } + + return false; +} + +bool CollectionModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_D(CollectionModel); + if (index.column() == 0 && role == Qt::EditRole) { + // rename collection + Collection col = d->collections.value(index.internalId()); + if (!col.isValid() || value.toString().isEmpty()) { + return false; + } + col.setName(value.toString()); + CollectionModifyJob *job = new CollectionModifyJob(col, d->session); + connect(job, SIGNAL(result(KJob*)), SLOT(editDone(KJob*))); + return true; + } + return QAbstractItemModel::setData(index, value, role); +} + +Qt::ItemFlags CollectionModel::flags(const QModelIndex &index) const +{ + Q_D(const CollectionModel); + + // Pass modeltest. + if (!index.isValid()) { + return 0; + } + + Qt::ItemFlags flags = QAbstractItemModel::flags(index); + + flags = flags | Qt::ItemIsDragEnabled; + + Collection col; + if (index.isValid()) { + col = d->collections.value(index.internalId()); + Q_ASSERT(col.isValid()); + } else { + return flags | Qt::ItemIsDropEnabled; // HACK Workaround for a probable bug in Qt + } + + if (col.isValid()) { + if (col.rights() & (Collection::CanChangeCollection | + Collection::CanCreateCollection | + Collection::CanDeleteCollection | + Collection::CanCreateItem)) { + if (index.column() == 0) { + flags = flags | Qt::ItemIsEditable; + } + flags = flags | Qt::ItemIsDropEnabled; + } + } + + return flags; +} + +Qt::DropActions CollectionModel::supportedDropActions() const +{ + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList CollectionModel::mimeTypes() const +{ + return QStringList() << QStringLiteral("text/uri-list"); +} + +QMimeData *CollectionModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *data = new QMimeData(); + QList urls; + foreach (const QModelIndex &index, indexes) { + if (index.column() != 0) { + continue; + } + + urls << Collection(index.internalId()).url(); + } + data->setUrls(urls); + + return data; +} + +bool CollectionModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_D(CollectionModel); + if (!(action & supportedDropActions())) { + return false; + } + + // handle drops onto items as well as drops between items + QModelIndex idx; + if (row >= 0 && column >= 0) { + idx = index(row, column, parent); + } else { + idx = parent; + } + + if (!idx.isValid()) { + return false; + } + + const Collection parentCol = d->collections.value(idx.internalId()); + if (!parentCol.isValid()) { + return false; + } + + KJob *job = PasteHelper::paste(data, parentCol, action != Qt::MoveAction); + connect(job, SIGNAL(result(KJob*)), SLOT(dropResult(KJob*))); + return true; +} + +Collection CollectionModel::collectionForId(Collection::Id id) const +{ + Q_D(const CollectionModel); + return d->collections.value(id); +} + +void CollectionModel::fetchCollectionStatistics(bool enable) +{ + Q_D(CollectionModel); + d->fetchStatistics = enable; + d->monitor->fetchCollectionStatistics(enable); +} + +void CollectionModel::includeUnsubscribed(bool include) +{ + Q_D(CollectionModel); + d->unsubscribed = include; +} + +#include "moc_collectionmodel.cpp" diff --git a/src/core/models/collectionmodel.h b/src/core/models/collectionmodel.h new file mode 100644 index 0000000..5bdd54d --- /dev/null +++ b/src/core/models/collectionmodel.h @@ -0,0 +1,140 @@ +/* + Copyright (c) 2006 - 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_COLLECTIONMODEL_H +#define AKONADI_COLLECTIONMODEL_H + +#include "akonadicore_export.h" +#include "collection.h" + +#include + +namespace Akonadi +{ + +class CollectionModelPrivate; + +/** + * @short A model for collections. + * + * This class provides the interface of QAbstractItemModel for the + * collection tree of the Akonadi storage. + * + * @code + * + * Akonadi::CollectionModel *model = new Akonadi::CollectionModel( this ); + * + * QTreeView *view = new QTreeView( this ); + * view->setModel( model ); + * + * @endcode + * + * If you want to list only collections of a special mime type, use + * CollectionFilterProxyModel on top of this model. + * + * @author Volker Krause + * @deprecated Use Akonadi::EntityTreeModel instead + */ +class AKONADICORE_DEPRECATED_EXPORT CollectionModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + /** + * Describes the roles for collections. + */ + enum Roles { + OldCollectionIdRole = Qt::UserRole + 1, ///< The collection identifier. For binary compatibility to <4.3 + OldCollectionRole = Qt::UserRole + 2, ///< The actual collection object. For binary compatibility to <4.3 + CollectionIdRole = Qt::UserRole + 10, ///< The collection identifier. + CollectionRole = Qt::UserRole + 11, ///< The actual collection object. + UserRole = Qt::UserRole + 42 ///< Role for user extensions. + }; + + /** + * Creates a new collection model. + * + * @param parent The parent object. + */ + explicit CollectionModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the collection model. + */ + virtual ~CollectionModel(); + + /** + * Sets whether collection statistics information shall be provided + * by the model. + * + * @see CollectionStatistics. + * @param enable whether to fetch collecton statistics + */ + void fetchCollectionStatistics(bool enable); + + /** + * Sets whether unsubscribed collections shall be listed in the model. + */ + void includeUnsubscribed(bool include = true); + + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE; + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; + QMimeData *mimeData(const QModelIndexList &indexes) const Q_DECL_OVERRIDE; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) Q_DECL_OVERRIDE; + QStringList mimeTypes() const Q_DECL_OVERRIDE; + +protected: + /** + * Returns the collection for a given collection @p id. + */ + Collection collectionForId(Collection::Id id) const; + + //@cond PRIVATE + Akonadi::CollectionModelPrivate *d_ptr; + explicit CollectionModel(CollectionModelPrivate *d, QObject *parent = Q_NULLPTR); + //@endcond + +private: + Q_DECLARE_PRIVATE(CollectionModel) + + //@cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void startFirstListJob()) + Q_PRIVATE_SLOT(d_func(), void collectionRemoved(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void collectionChanged(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void updateDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void collectionStatisticsChanged(Akonadi::Collection::Id, + const Akonadi::CollectionStatistics &)) + Q_PRIVATE_SLOT(d_func(), void listDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void editDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void dropResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void collectionsChanged(const Akonadi::Collection::List &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/models/collectionmodel_p.cpp b/src/core/models/collectionmodel_p.cpp new file mode 100644 index 0000000..10ecc78 --- /dev/null +++ b/src/core/models/collectionmodel_p.cpp @@ -0,0 +1,358 @@ +/* + Copyright (c) 2006 - 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. +*/ + +//@cond PRIVATE + +#include "collectionmodel_p.h" +#include "collectionmodel.h" +#include "collectionutils.h" + +#include "collectionfetchjob.h" +#include "collectionstatistics.h" +#include "collectionstatisticsjob.h" +#include "monitor.h" +#include "session.h" +#include "collectionfetchscope.h" + +#include "akonadicore_debug.h" + +#include +#include + +#include +#include + +using namespace Akonadi; + +void CollectionModelPrivate::collectionRemoved(const Akonadi::Collection &collection) +{ + Q_Q(CollectionModel); + QModelIndex colIndex = indexForId(collection.id()); + if (colIndex.isValid()) { + QModelIndex parentIndex = q->parent(colIndex); + // collection is still somewhere in the hierarchy + removeRowFromModel(colIndex.row(), parentIndex); + } else { + if (collections.contains(collection.id())) { + // collection is orphan, ie. the parent has been removed already + collections.remove(collection.id()); + childCollections.remove(collection.id()); + } + } +} + +void CollectionModelPrivate::collectionChanged(const Akonadi::Collection &collection) +{ + Q_Q(CollectionModel); + // What kind of change is it ? + Collection::Id oldParentId = collections.value(collection.id()).parentCollection().id(); + Collection::Id newParentId = collection.parentCollection().id(); + if (newParentId != oldParentId && oldParentId >= 0) { // It's a move + removeRowFromModel(indexForId(collections[collection.id()].id()).row(), indexForId(oldParentId)); + Collection newParent; + if (newParentId == Collection::root().id()) { + newParent = Collection::root(); + } else { + newParent = collections.value(newParentId); + } + CollectionFetchJob *job = new CollectionFetchJob(newParent, CollectionFetchJob::Recursive, session); + job->fetchScope().setListFilter(unsubscribed ? CollectionFetchScope::NoFilter : CollectionFetchScope::Enabled); + job->fetchScope().setIncludeStatistics(fetchStatistics); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + q, SLOT(collectionsChanged(Akonadi::Collection::List))); + q->connect(job, SIGNAL(result(KJob*)), + q, SLOT(listDone(KJob*))); + + } else { // It's a simple change + CollectionFetchJob *job = new CollectionFetchJob(collection, CollectionFetchJob::Base, session); + job->fetchScope().setListFilter(unsubscribed ? CollectionFetchScope::NoFilter : CollectionFetchScope::Enabled); + job->fetchScope().setIncludeStatistics(fetchStatistics); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + q, SLOT(collectionsChanged(Akonadi::Collection::List))); + q->connect(job, SIGNAL(result(KJob*)), + q, SLOT(listDone(KJob*))); + } + +} + +void CollectionModelPrivate::updateDone(KJob *job) +{ + if (job->error()) { + // TODO: handle job errors + qCWarning(AKONADICORE_LOG) << "Job error:" << job->errorString(); + } else { + CollectionStatisticsJob *csjob = static_cast(job); + Collection result = csjob->collection(); + collectionStatisticsChanged(result.id(), csjob->statistics()); + } +} + +void CollectionModelPrivate::collectionStatisticsChanged(Collection::Id collection, + const Akonadi::CollectionStatistics &statistics) +{ + Q_Q(CollectionModel); + + if (!collections.contains(collection)) { + qCWarning(AKONADICORE_LOG) << "Got statistics response for non-existing collection:" << collection; + } else { + collections[collection].setStatistics(statistics); + + Collection col = collections.value(collection); + QModelIndex startIndex = indexForId(col.id()); + QModelIndex endIndex = indexForId(col.id(), q->columnCount(q->parent(startIndex)) - 1); + emit q->dataChanged(startIndex, endIndex); + } +} + +void CollectionModelPrivate::listDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << endl; + } +} + +void CollectionModelPrivate::editDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Edit failed: " << job->errorString(); + } +} + +void CollectionModelPrivate::dropResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Paste failed:" << job->errorString(); + // TODO: error handling + } +} + +void CollectionModelPrivate::collectionsChanged(const Collection::List &cols) +{ + Q_Q(CollectionModel); + + foreach (Collection col, cols) { //krazy:exclude=foreach non-const is needed here + if (collections.contains(col.id())) { + // If the collection is already known to the model, we simply update it... + col.setStatistics(collections.value(col.id()).statistics()); + collections[col.id()] = col; + QModelIndex startIndex = indexForId(col.id()); + QModelIndex endIndex = indexForId(col.id(), q->columnCount(q->parent(startIndex)) - 1); + emit q->dataChanged(startIndex, endIndex); + continue; + } + // ... otherwise we add it to the set of collections we need to handle. + m_newChildCollections[col.parentCollection().id()].append(col.id()); + m_newCollections.insert(col.id(), col); + } + + // Handle the collections in m_newChildCollections. If the collections + // parent is already in the model, the collection can be added to the model. + // Otherwise it is persisted until it has a valid parent in the model. + int currentSize = m_newChildCollections.size(); + int lastSize = -1; + + while (currentSize > 0) { + lastSize = currentSize; + + QMutableHashIterator< Collection::Id, QVector< Collection::Id > > i(m_newChildCollections); + while (i.hasNext()) { + i.next(); + + // the key is the parent of new collections. It may itself also be new, + // but that will be handled later. + Collection::Id colId = i.key(); + + QVector< Collection::Id > newChildCols = i.value(); + int newChildCount = newChildCols.size(); +// if ( newChildCount == 0 ) +// { +// // Sanity check. +// qCDebug(AKONADICORE_LOG) << "No new child collections have been added to the collection:" << colId; +// i.remove(); +// currentSize--; +// break; +// } + + if (collections.contains(colId) || colId == Collection::root().id()) { + QModelIndex parentIndex = indexForId(colId); + int currentChildCount = childCollections.value(colId).size(); + + q->beginInsertRows(parentIndex, + currentChildCount, // Start index is at the end of existing collections. + currentChildCount + newChildCount - 1); // End index is the result of the insertion. + + foreach (Collection::Id id, newChildCols) { + Collection c = m_newCollections.take(id); + collections.insert(id, c); + } + + childCollections[colId] << newChildCols; + q->endInsertRows(); + i.remove(); + currentSize--; + break; + } + } + + // We iterated through once without adding any more collections to the model. + if (currentSize == lastSize) { + // The remaining collections in the list do not have a valid parent in the model yet. They + // might arrive in the next batch from the monitor, so they're still in m_newCollections + // and m_newChildCollections. + qCDebug(AKONADICORE_LOG) << "Some collections did not have a parent in the model yet!"; + break; + } + } +} + +QModelIndex CollectionModelPrivate::indexForId(Collection::Id id, int column) const +{ + Q_Q(const CollectionModel); + if (!collections.contains(id)) { + return QModelIndex(); + } + + Collection::Id parentId = collections.value(id).parentCollection().id(); + // check if parent still exist or if this is an orphan collection + if (parentId != Collection::root().id() && !collections.contains(parentId)) { + return QModelIndex(); + } + + QVector list = childCollections.value(parentId); + int row = list.indexOf(id); + + if (row >= 0) { + return q->createIndex(row, column, reinterpret_cast(collections.value(list.at(row)).id())); + } + return QModelIndex(); +} + +bool CollectionModelPrivate::removeRowFromModel(int row, const QModelIndex &parent) +{ + Q_Q(CollectionModel); + QVector list; + Collection parentCol; + if (parent.isValid()) { + parentCol = collections.value(parent.internalId()); + Q_ASSERT(parentCol.id() == parent.internalId()); + list = childCollections.value(parentCol.id()); + } else { + parentCol = Collection::root(); + list = childCollections.value(Collection::root().id()); + } + if (row < 0 || row >= list.size()) { + qCWarning(AKONADICORE_LOG) << "Index out of bounds:" << row << " parent:" << parentCol.id(); + return false; + } + + q->beginRemoveRows(parent, row, row); + const Collection::Id delColId = list[row]; + list.remove(row); + foreach (Collection::Id childColId, childCollections[delColId]) { + collections.remove(childColId); + } + collections.remove(delColId); + childCollections.remove(delColId); // remove children of deleted collection + childCollections.insert(parentCol.id(), list); // update children of parent + q->endRemoveRows(); + + return true; +} + +bool CollectionModelPrivate::supportsContentType(const QModelIndex &index, const QStringList &contentTypes) +{ + if (!index.isValid()) { + return false; + } + Collection col = collections.value(index.internalId()); + Q_ASSERT(col.isValid()); + QStringList ct = col.contentMimeTypes(); + foreach (const QString &a, ct) { + if (contentTypes.contains(a)) { + return true; + } + } + return false; +} + +void CollectionModelPrivate::init() +{ + Q_Q(CollectionModel); + + session = new Session(QCoreApplication::instance()->applicationName().toUtf8() + + QByteArray("-CollectionModel-") + QByteArray::number(qrand()), q); + QTimer::singleShot(0, q, SLOT(startFirstListJob())); + + // monitor collection changes + monitor = new Monitor(); + monitor->setCollectionMonitored(Collection::root()); + monitor->fetchCollection(true); + + // ### Hack to get the kmail resource folder icons + KIconLoader::global()->addAppDir(QStringLiteral("kmail")); + KIconLoader::global()->addAppDir(QStringLiteral("kdepim")); + + // monitor collection changes + q->connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection)), + q, SLOT(collectionChanged(Akonadi::Collection))); + q->connect(monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)), + q, SLOT(collectionChanged(Akonadi::Collection))); + q->connect(monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), + q, SLOT(collectionRemoved(Akonadi::Collection))); + q->connect(monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), + q, SLOT(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics))); +} + +QIcon CollectionModelPrivate::iconForCollection(const Collection &col) const +{ + // Reset the cache when icon theme changes + if (mIconThemeName != QIcon::themeName()) { + mIconThemeName = QIcon::themeName(); + mIconCache.clear(); + } + + QString iconName; + if (col.hasAttribute()) { + iconName = col.attribute()->iconName(); + } + if (iconName.isEmpty()) { + iconName = CollectionUtils::defaultIconName(col); + } + + QIcon &icon = mIconCache[iconName]; + if (icon.isNull()) { + icon = QIcon::fromTheme(iconName); + } + return icon; +} + +void CollectionModelPrivate::startFirstListJob() +{ + Q_Q(CollectionModel); + + // start a list job + CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, session); + job->fetchScope().setListFilter(unsubscribed ? CollectionFetchScope::NoFilter : CollectionFetchScope::Enabled); + job->fetchScope().setIncludeStatistics(fetchStatistics); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + q, SLOT(collectionsChanged(Akonadi::Collection::List))); + q->connect(job, SIGNAL(result(KJob*)), q, SLOT(listDone(KJob*))); +} + +//@endcond diff --git a/src/core/models/collectionmodel_p.h b/src/core/models/collectionmodel_p.h new file mode 100644 index 0000000..612e9de --- /dev/null +++ b/src/core/models/collectionmodel_p.h @@ -0,0 +1,118 @@ +/* + Copyright (c) 2006 - 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_COLLECTIONMODEL_P_H +#define AKONADI_COLLECTIONMODEL_P_H + +#include "collection.h" + +#include + +#include +#include +#include +#include +#include + +class KJob; + +namespace Akonadi +{ + +class CollectionModel; +class CollectionStatistics; +class Monitor; +class Session; + +/** + * @internal + */ +class CollectionModelPrivate +{ +public: + Q_DECLARE_PUBLIC(CollectionModel) + explicit CollectionModelPrivate(CollectionModel *parent) + : q_ptr(parent) + , monitor(0) + , session(0) + , fetchStatistics(false) + , unsubscribed(false) + , headerContent(i18nc("@title:column, name of a thing", "Name")) + { + } + + virtual ~CollectionModelPrivate() + { + } + + CollectionModel *q_ptr; + QHash collections; + QHash > childCollections; + + QHash m_newCollections; + QHash< Collection::Id, QVector > m_newChildCollections; + + Monitor *monitor; + Session *session; + QStringList mimeTypes; + bool fetchStatistics; + bool unsubscribed; + QString headerContent; + + void init(); + void startFirstListJob(); + void collectionRemoved(const Akonadi::Collection &collection); + void collectionChanged(const Akonadi::Collection &collection); + void updateDone(KJob *job); + void collectionStatisticsChanged(Collection::Id, const Akonadi::CollectionStatistics &statistics); + void listDone(KJob *job); + void editDone(KJob *job); + void dropResult(KJob *job); + void collectionsChanged(const Akonadi::Collection::List &cols); + + QIcon iconForCollection(const Collection &collection) const; + + QModelIndex indexForId(Collection::Id id, int column = 0) const; + bool removeRowFromModel(int row, const QModelIndex &parent = QModelIndex()); + bool supportsContentType(const QModelIndex &index, const QStringList &contentTypes); + +private: + // FIXME: This cache is a workaround for extremly slow QIcon::fromTheme() + // caused by bottleneck in FrameworkIntegration. See bug #346644 for details. + mutable QHash mIconCache; + mutable QString mIconThemeName; + + void updateSupportedMimeTypes(const Collection &col) + { + const QStringList l = col.contentMimeTypes(); + QStringList::ConstIterator constEnd(l.constEnd()); + for (QStringList::ConstIterator it = l.constBegin(); it != constEnd; ++it) { + if ((*it) == Collection::mimeType()) { + continue; + } + if (!mimeTypes.contains(*it)) { + mimeTypes << *it; + } + } + } +}; + +} + +#endif diff --git a/src/core/models/entitymimetypefiltermodel.cpp b/src/core/models/entitymimetypefiltermodel.cpp new file mode 100644 index 0000000..0a8f669 --- /dev/null +++ b/src/core/models/entitymimetypefiltermodel.cpp @@ -0,0 +1,248 @@ +/* + Copyright (c) 2007 Bruno Virlet + 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 "entitymimetypefiltermodel.h" +#include "akonadicore_debug.h" +#include "entitytreemodel.h" +#include "mimetypechecker.h" + +#include +#include + +using namespace Akonadi; + +namespace Akonadi +{ +/** + * @internal + */ +class EntityMimeTypeFilterModelPrivate +{ +public: + EntityMimeTypeFilterModelPrivate(EntityMimeTypeFilterModel *parent) + : q_ptr(parent) + , m_headerGroup(EntityTreeModel::EntityTreeHeaders) + { + } + + Q_DECLARE_PUBLIC(EntityMimeTypeFilterModel) + EntityMimeTypeFilterModel *q_ptr; + + QStringList includedMimeTypes; + QStringList excludedMimeTypes; + + QPersistentModelIndex m_rootIndex; + + EntityTreeModel::HeaderGroup m_headerGroup; +}; + +} + +EntityMimeTypeFilterModel::EntityMimeTypeFilterModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d_ptr(new EntityMimeTypeFilterModelPrivate(this)) +{ +} + +EntityMimeTypeFilterModel::~EntityMimeTypeFilterModel() +{ + delete d_ptr; +} + +void EntityMimeTypeFilterModel::addMimeTypeInclusionFilters(const QStringList &typeList) +{ + Q_D(EntityMimeTypeFilterModel); + d->includedMimeTypes << typeList; + invalidateFilter(); +} + +void EntityMimeTypeFilterModel::addMimeTypeExclusionFilters(const QStringList &typeList) +{ + Q_D(EntityMimeTypeFilterModel); + d->excludedMimeTypes << typeList; + invalidateFilter(); +} + +void EntityMimeTypeFilterModel::addMimeTypeInclusionFilter(const QString &type) +{ + Q_D(EntityMimeTypeFilterModel); + d->includedMimeTypes << type; + invalidateFilter(); +} + +void EntityMimeTypeFilterModel::addMimeTypeExclusionFilter(const QString &type) +{ + Q_D(EntityMimeTypeFilterModel); + d->excludedMimeTypes << type; + invalidateFilter(); +} + +bool EntityMimeTypeFilterModel::filterAcceptsColumn(int sourceColumn, const QModelIndex &sourceParent) const +{ + if (sourceColumn >= columnCount(mapFromSource(sourceParent))) { + return false; + } + return QSortFilterProxyModel::filterAcceptsColumn(sourceColumn, sourceParent); +} + +bool EntityMimeTypeFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_D(const EntityMimeTypeFilterModel); + const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent); + + const QString rowMimetype = idx.data(EntityTreeModel::MimeTypeRole).toString(); + + if (d->excludedMimeTypes.contains(rowMimetype)) { + return false; + } + + if (d->includedMimeTypes.isEmpty() || d->includedMimeTypes.contains(rowMimetype)) { + const Akonadi::Item item = idx.data(EntityTreeModel::ItemRole).value(); + + if (item.isValid() && !item.hasPayload()) { + qCDebug(AKONADICORE_LOG) << "Item " << item.id() << " doesn't have payload"; + return false; + } + + return true; + } + + return false; +} + +QStringList EntityMimeTypeFilterModel::mimeTypeInclusionFilters() const +{ + Q_D(const EntityMimeTypeFilterModel); + return d->includedMimeTypes; +} + +QStringList EntityMimeTypeFilterModel::mimeTypeExclusionFilters() const +{ + Q_D(const EntityMimeTypeFilterModel); + return d->excludedMimeTypes; +} + +void EntityMimeTypeFilterModel::clearFilters() +{ + Q_D(EntityMimeTypeFilterModel); + d->includedMimeTypes.clear(); + d->excludedMimeTypes.clear(); + invalidateFilter(); +} + +void EntityMimeTypeFilterModel::setHeaderGroup(EntityTreeModel::HeaderGroup headerGroup) +{ + Q_D(EntityMimeTypeFilterModel); + d->m_headerGroup = headerGroup; +} + +QVariant EntityMimeTypeFilterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (!sourceModel()) { + return QVariant(); + } + + Q_D(const EntityMimeTypeFilterModel); + role += (EntityTreeModel::TerminalUserRole * d->m_headerGroup); + return sourceModel()->headerData(section, orientation, role); +} + +QModelIndexList EntityMimeTypeFilterModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const +{ + if (!sourceModel()) { + return QModelIndexList(); + } + + if (role < Qt::UserRole) { + return QSortFilterProxyModel::match(start, role, value, hits, flags); + } + + QModelIndexList list; + QModelIndex proxyIndex; + foreach (const QModelIndex &idx, sourceModel()->match(mapToSource(start), role, value, hits, flags)) { + proxyIndex = mapFromSource(idx); + if (proxyIndex.isValid()) { + list << proxyIndex; + } + } + + return list; +} + +int EntityMimeTypeFilterModel::columnCount(const QModelIndex &parent) const +{ + Q_D(const EntityMimeTypeFilterModel); + + if (!sourceModel()) { + return 0; + } + + const QVariant value = sourceModel()->data(mapToSource(parent), EntityTreeModel::ColumnCountRole + (EntityTreeModel::TerminalUserRole * d->m_headerGroup)); + if (!value.isValid()) { + return 0; + } + + return value.toInt(); +} + +bool EntityMimeTypeFilterModel::hasChildren(const QModelIndex &parent) const +{ + if (!sourceModel()) { + return false; + } + + // QSortFilterProxyModel implementation is buggy in that it emits rowsAboutToBeInserted etc + // only after the source model has emitted rowsInserted, instead of emitting it when the + // source model emits rowsAboutToBeInserted. That means that the source and the proxy are out + // of sync around the time of insertions, so we can't use the optimization below. + return rowCount(parent) > 0; +#if 0 + + if (!parent.isValid()) { + return sourceModel()->hasChildren(parent); + } + + Q_D(const EntityMimeTypeFilterModel); + if (EntityTreeModel::ItemListHeaders == d->m_headerGroup) { + return false; + } + + if (EntityTreeModel::CollectionTreeHeaders == d->m_headerGroup) { + QModelIndex childIndex = parent.child(0, 0); + while (childIndex.isValid()) { + Collection col = childIndex.data(EntityTreeModel::CollectionRole).value(); + if (col.isValid()) { + return true; + } + childIndex = childIndex.sibling(childIndex.row() + 1, childIndex.column()); + } + } + return false; +#endif +} + +bool EntityMimeTypeFilterModel::canFetchMore(const QModelIndex &parent) const +{ + Q_D(const EntityMimeTypeFilterModel); + if (EntityTreeModel::CollectionTreeHeaders == d->m_headerGroup) { + return false; + } + return QSortFilterProxyModel::canFetchMore(parent); +} diff --git a/src/core/models/entitymimetypefiltermodel.h b/src/core/models/entitymimetypefiltermodel.h new file mode 100644 index 0000000..7ab2141 --- /dev/null +++ b/src/core/models/entitymimetypefiltermodel.h @@ -0,0 +1,152 @@ +/* + Copyright (c) 2007 Bruno Virlet + 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_ENTITYMIMETYPEFILTERMODEL_H +#define AKONADI_ENTITYMIMETYPEFILTERMODEL_H + +#include "akonadicore_export.h" +#include "entitytreemodel.h" + +#include + +namespace Akonadi +{ + +class EntityMimeTypeFilterModelPrivate; + +/** + * @short A proxy model that filters entities by mime type. + * + * This class can be used on top of an EntityTreeModel to exclude entities by mimetype + * or to include only certain mimetypes. + * + * @code + * + * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel( this ); + * + * Akonadi::EntityMimeTypeFilterModel *proxy = new Akonadi::EntityMimeTypeFilterModel(); + * proxy->addMimeTypeInclusionFilter( "message/rfc822" ); + * proxy->setSourceModel( model ); + * + * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @li If a mimetype is in both the exclusion list and the inclusion list, it is excluded. + * @li If the mimeTypeInclusionFilter is empty, all mimetypes are + * accepted (except if they are in the exclusion filter of course). + * + * + * @author Bruno Virlet + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADICORE_EXPORT EntityMimeTypeFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new entity mime type filter model. + * + * @param parent The parent object. + */ + explicit EntityMimeTypeFilterModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the entity mime type filter model. + */ + virtual ~EntityMimeTypeFilterModel(); + + /** + * Add mime types to be shown by the filter. + * + * @param mimeTypes A list of mime types to be included. + */ + void addMimeTypeInclusionFilters(const QStringList &mimeTypes); + + /** + * Add mimetypes to filter out + * + * @param mimeTypes A list to exclude from the model. + */ + void addMimeTypeExclusionFilters(const QStringList &mimeTypes); + + /** + * Add mime type to be shown by the filter. + * + * @param mimeType A mime type to be shown. + */ + void addMimeTypeInclusionFilter(const QString &mimeType); + + /** + * Add mime type to be excluded by the filter. + * + * @param mimeType A mime type to be excluded. + */ + void addMimeTypeExclusionFilter(const QString &mimeType); + + /** + * Returns the list of mime type inclusion filters. + */ + QStringList mimeTypeInclusionFilters() const; + + /** + * Returns the list of mime type exclusion filters. + */ + QStringList mimeTypeExclusionFilters() const; + + /** + * Clear all mime type filters. + */ + void clearFilters(); + + /** + * Sets the header @p set of the filter model. + * @param headerGroup the header to set. + * \sa EntityTreeModel::HeaderGroup + */ + void setHeaderGroup(EntityTreeModel::HeaderGroup headerGroup); + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + bool hasChildren(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + + bool canFetchMore(const QModelIndex &parent) const Q_DECL_OVERRIDE; + + QModelIndexList match(const QModelIndex &start, int role, const QVariant &value, int hits = 1, Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const Q_DECL_OVERRIDE; + + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; + bool filterAcceptsColumn(int sourceColumn, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(EntityMimeTypeFilterModel) + EntityMimeTypeFilterModelPrivate *const d_ptr; + //@endcond +}; + +} + +#endif diff --git a/src/core/models/entityorderproxymodel.cpp b/src/core/models/entityorderproxymodel.cpp new file mode 100644 index 0000000..793c97a --- /dev/null +++ b/src/core/models/entityorderproxymodel.cpp @@ -0,0 +1,312 @@ +/* + Copyright (C) 2010 Klarälvdalens Datakonsult AB, + a KDAB Group company, info@kdab.net, + author 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 "entityorderproxymodel.h" + +#include + +#include +#include + +#include "collection.h" +#include "item.h" +#include "entitytreemodel.h" + +namespace Akonadi +{ + +class EntityOrderProxyModelPrivate +{ +public: + EntityOrderProxyModelPrivate(EntityOrderProxyModel *qq) + : q_ptr(qq) + { + + } + + void saveOrder(const QModelIndex &index); + + KConfigGroup m_orderConfig; + + Q_DECLARE_PUBLIC(EntityOrderProxyModel) + EntityOrderProxyModel *const q_ptr; + +}; + +} + +using namespace Akonadi; + +EntityOrderProxyModel::EntityOrderProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d_ptr(new EntityOrderProxyModelPrivate(this)) +{ + setDynamicSortFilter(true); + //setSortCaseSensitivity( Qt::CaseInsensitive ); +} + +EntityOrderProxyModel::~EntityOrderProxyModel() +{ + delete d_ptr; +} + +void EntityOrderProxyModel::setOrderConfig(KConfigGroup &configGroup) +{ + Q_D(EntityOrderProxyModel); + layoutAboutToBeChanged(); + d->m_orderConfig = configGroup; + layoutChanged(); +} + +bool EntityOrderProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + Q_D(const EntityOrderProxyModel); + + if (!d->m_orderConfig.isValid()) { + return QSortFilterProxyModel::lessThan(left, right); + } + Collection col = left.data(EntityTreeModel::ParentCollectionRole).value(); + + if (!d->m_orderConfig.hasKey(QString::number(col.id()))) { + return QSortFilterProxyModel::lessThan(left, right); + } + + const QStringList list = d->m_orderConfig.readEntry(QString::number(col.id()), QStringList()); + + if (list.isEmpty()) { + return QSortFilterProxyModel::lessThan(left, right); + } + + const QString leftValue = configString(left); + const QString rightValue = configString(right); + + const int leftPosition = list.indexOf(leftValue); + const int rightPosition = list.indexOf(rightValue); + + if (leftPosition < 0 || rightPosition < 0) { + return QSortFilterProxyModel::lessThan(left, right); + } + + return leftPosition < rightPosition; +} + +bool EntityOrderProxyModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_D(EntityOrderProxyModel); + + if (!d->m_orderConfig.isValid()) { + return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + } + + if (!data->hasFormat(QStringLiteral("text/uri-list"))) { + return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + } + + if (row == -1) { + return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + } + + bool containsMove = false; + + const QList urls = data->urls(); + + Collection parentCol; + + if (parent.isValid()) { + parentCol = parent.data(EntityTreeModel::CollectionRole).value(); + } else { + if (!hasChildren(parent)) { + return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + } + + const QModelIndex targetIndex = index(0, column, parent); + + parentCol = targetIndex.data(EntityTreeModel::ParentCollectionRole).value(); + } + + QStringList droppedList; + foreach (const QUrl &url, urls) { + Collection col = Collection::fromUrl(url); + + if (!col.isValid()) { + Item item = Item::fromUrl(url); + if (!item.isValid()) { + continue; + } + + const QModelIndexList list = EntityTreeModel::modelIndexesForItem(this, item); + if (list.isEmpty()) { + continue; + } + + if (!containsMove && list.first().data(EntityTreeModel::ParentCollectionRole).value().id() != parentCol.id()) { + containsMove = true; + } + + droppedList << configString(list.first()); + } else { + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(this, col); + if (!idx.isValid()) { + continue; + } + + if (!containsMove && idx.data(EntityTreeModel::ParentCollectionRole).value().id() != parentCol.id()) { + containsMove = true; + } + + droppedList << configString(idx); + } + } + + QStringList existingList; + if (d->m_orderConfig.hasKey(QString::number(parentCol.id()))) { + existingList = d->m_orderConfig.readEntry(QString::number(parentCol.id()), QStringList()); + } else { + const int rowCount = this->rowCount(parent); + existingList.reserve(rowCount); + for (int row = 0; row < rowCount; ++row) { + static const int column = 0; + const QModelIndex idx = this->index(row, column, parent); + existingList.append(configString(idx)); + } + } + const int numberOfDroppedElement(droppedList.size()); + for (int i = 0; i < numberOfDroppedElement; ++i) { + const QString droppedItem = droppedList.at(i); + const int existingIndex = existingList.indexOf(droppedItem); + existingList.removeAt(existingIndex); + existingList.insert(row + i - (existingIndex > row ? 0 : 1), droppedList.at(i)); + } + + d->m_orderConfig.writeEntry(QString::number(parentCol.id()), existingList); + + if (containsMove) { + bool result = QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + invalidate(); + return result; + } + invalidate(); + return true; +} + +QModelIndexList EntityOrderProxyModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const +{ + if (role < Qt::UserRole) { + return QSortFilterProxyModel::match(start, role, value, hits, flags); + } + + QModelIndexList list; + QModelIndex proxyIndex; + foreach (const QModelIndex &idx, sourceModel()->match(mapToSource(start), role, value, hits, flags)) { + proxyIndex = mapFromSource(idx); + if (proxyIndex.isValid()) { + list << proxyIndex; + } + } + + return list; +} + +void EntityOrderProxyModelPrivate::saveOrder(const QModelIndex &parent) +{ + Q_Q(const EntityOrderProxyModel); + int rowCount = q->rowCount(parent); + + if (rowCount == 0) { + return; + } + + static const int column = 0; + QModelIndex childIndex = q->index(0, column, parent); + + QString parentKey = q->parentConfigString(childIndex); + + if (parentKey.isEmpty()) { + return; + } + + QStringList list; + + list << q->configString(childIndex); + saveOrder(childIndex); + list.reserve(rowCount); + for (int row = 1; row < rowCount; ++row) { + childIndex = q->index(row, column, parent); + list << q->configString(childIndex); + saveOrder(childIndex); + } + + m_orderConfig.writeEntry(parentKey, list); +} + +QString EntityOrderProxyModel::parentConfigString(const QModelIndex &index) const +{ + const Collection col = index.data(EntityTreeModel::ParentCollectionRole).value(); + + Q_ASSERT(col.isValid()); + if (!col.isValid()) { + return QString(); + } + + return QString::number(col.id()); +} + +QString EntityOrderProxyModel::configString(const QModelIndex &index) const +{ + Item::Id iId = index.data(EntityTreeModel::ItemIdRole).toLongLong(); + if (iId != -1) { + return QLatin1String("i") + QString::number(iId); + } + Collection::Id cId = index.data(EntityTreeModel::CollectionIdRole).toLongLong(); + if (cId != -1) { + return QLatin1String("c") + QString::number(cId); + } + Q_ASSERT(!"Invalid entity"); + return QString(); +} + +void EntityOrderProxyModel::saveOrder() +{ + Q_D(EntityOrderProxyModel); + d->saveOrder(QModelIndex()); + d->m_orderConfig.sync(); +} + +void EntityOrderProxyModel::clearOrder(const QModelIndex &parent) +{ + Q_D(EntityOrderProxyModel); + + const QString parentKey = parentConfigString(index(0, 0, parent)); + + if (parentKey.isEmpty()) { + return; + } + + d->m_orderConfig.deleteEntry(parentKey); + invalidate(); +} + +void EntityOrderProxyModel::clearTreeOrder() +{ + Q_D(EntityOrderProxyModel); + d->m_orderConfig.deleteGroup(); + invalidate(); +} diff --git a/src/core/models/entityorderproxymodel.h b/src/core/models/entityorderproxymodel.h new file mode 100644 index 0000000..c932520 --- /dev/null +++ b/src/core/models/entityorderproxymodel.h @@ -0,0 +1,104 @@ +/* + Copyright (C) 2010 Klarälvdalens Datakonsult AB, + a KDAB Group company, info@kdab.net, + author 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_ENTITYORDERPROXYMODEL_H +#define AKONADI_ENTITYORDERPROXYMODEL_H + +#include + +#include "akonadicore_export.h" + +class KConfigGroup; + +namespace Akonadi +{ +class EntityOrderProxyModelPrivate; + +/** + * @short A model that keeps the order of entities persistent. + * + * This proxy maintains the order of entities in a tree. The user can re-order + * items and the new order will be persisted restored on reset or restart. + * + * @author Stephen Kelly + * @since 4.6 + */ +class AKONADICORE_EXPORT EntityOrderProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new entity order proxy model. + * + * @param parent The parent object. + */ + EntityOrderProxyModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the entity order proxy model. + */ + virtual ~EntityOrderProxyModel(); + + /** + * Sets the config @p group that will be used for storing the order. + */ + void setOrderConfig(KConfigGroup &group); + + /** + * Saves the order. + */ + void saveOrder(); + + void clearOrder(const QModelIndex &index); + void clearTreeOrder(); + + /** + * @reimp + */ + bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; + + /** + * @reimp + */ + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) Q_DECL_OVERRIDE; + + /** + * @reimp + */ + QModelIndexList match(const QModelIndex &start, int role, const QVariant &value, int hits = 1, + Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const Q_DECL_OVERRIDE; + +protected: + EntityOrderProxyModelPrivate *const d_ptr; + + virtual QString parentConfigString(const QModelIndex &index) const; + virtual QString configString(const QModelIndex &index) const; + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(EntityOrderProxyModel) + //@endcond +}; + +} + +#endif diff --git a/src/core/models/entityrightsfiltermodel.cpp b/src/core/models/entityrightsfiltermodel.cpp new file mode 100644 index 0000000..3731ee5 --- /dev/null +++ b/src/core/models/entityrightsfiltermodel.cpp @@ -0,0 +1,133 @@ +/* + Copyright (c) 2007 Bruno Virlet + 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 "entityrightsfiltermodel.h" + +#include "entitytreemodel.h" + +#include + +using namespace Akonadi; + +namespace Akonadi +{ + +/** + * @internal + */ +class EntityRightsFilterModelPrivate +{ +public: + EntityRightsFilterModelPrivate(EntityRightsFilterModel *parent) + : q_ptr(parent) + , mAccessRights(Collection::AllRights) + { + } + + bool rightsMatches(const QModelIndex &index) const + { + if (mAccessRights == Collection::AllRights || + mAccessRights == Collection::ReadOnly) { + return true; + } + + const Collection collection = index.data(EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + return (mAccessRights & collection.rights()); + } else { + const Item item = index.data(EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + const Collection collection = index.data(EntityTreeModel::ParentCollectionRole).value(); + return (mAccessRights & collection.rights()); + } else { + return false; + } + } + } + + Q_DECLARE_PUBLIC(EntityRightsFilterModel) + EntityRightsFilterModel *q_ptr; + + Collection::Rights mAccessRights; +}; + +} + +EntityRightsFilterModel::EntityRightsFilterModel(QObject *parent) + : KRecursiveFilterProxyModel(parent) + , d_ptr(new EntityRightsFilterModelPrivate(this)) +{ +} + +EntityRightsFilterModel::~EntityRightsFilterModel() +{ + delete d_ptr; +} + +void EntityRightsFilterModel::setAccessRights(Collection::Rights rights) +{ + Q_D(EntityRightsFilterModel); + d->mAccessRights = rights; + invalidateFilter(); +} + +Collection::Rights EntityRightsFilterModel::accessRights() const +{ + Q_D(const EntityRightsFilterModel); + return d->mAccessRights; +} + +bool EntityRightsFilterModel::acceptRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_D(const EntityRightsFilterModel); + + const QModelIndex modelIndex = sourceModel()->index(sourceRow, 0, sourceParent); + + return d->rightsMatches(modelIndex); +} + +Qt::ItemFlags EntityRightsFilterModel::flags(const QModelIndex &index) const +{ + Q_D(const EntityRightsFilterModel); + + if (d->rightsMatches(index)) { + return KRecursiveFilterProxyModel::flags(index); + } else { + return KRecursiveFilterProxyModel::flags(index) & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } +} + +QModelIndexList EntityRightsFilterModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const +{ + if (role < Qt::UserRole) { + return QSortFilterProxyModel::match(start, role, value, hits, flags); + } + + QModelIndexList list; + QModelIndex proxyIndex; + foreach (const QModelIndex &idx, sourceModel()->match(mapToSource(start), role, value, hits, flags)) { + proxyIndex = mapFromSource(idx); + if (proxyIndex.isValid()) { + list << proxyIndex; + } + } + + return list; +} diff --git a/src/core/models/entityrightsfiltermodel.h b/src/core/models/entityrightsfiltermodel.h new file mode 100644 index 0000000..7ba2129 --- /dev/null +++ b/src/core/models/entityrightsfiltermodel.h @@ -0,0 +1,114 @@ +/* + Copyright (c) 2009 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_ENTITYRIGHTSFILTERMODEL_H +#define AKONADI_ENTITYRIGHTSFILTERMODEL_H + +#include "entitytreemodel.h" + +#include + +#include "akonadicore_export.h" + +namespace Akonadi +{ + +class EntityRightsFilterModelPrivate; + +/** + * @short A proxy model that filters entities by access rights. + * + * This class can be used on top of an EntityTreeModel to exclude entities by access type + * or to include only certain entities with special access rights. + * + * @code + * + * using namespace Akonadi; + * + * EntityTreeModel *model = new EntityTreeModel( this ); + * + * EntityRightsFilterModel *filter = new EntityRightsFilterModel(); + * filter->setAccessRights( Collection::CanCreateItem | Collection::CanCreateCollection ); + * filter->setSourceModel( model ); + * + * EntityTreeView *view = new EntityTreeView( this ); + * view->setModel( filter ); + * + * @endcode + * + * @li For collections the access rights are checked against the collections own rights. + * @li For items the access rights are checked against the item's parent collection rights. + * + * @author Tobias Koenig + * @since 4.6 + */ +class AKONADICORE_EXPORT EntityRightsFilterModel : public KRecursiveFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new entity rights filter model. + * + * @param parent The parent object. + */ + explicit EntityRightsFilterModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the entity rights filter model. + */ + virtual ~EntityRightsFilterModel(); + + /** + * Sets the access @p rights the entities shall be filtered + * against. If no rights are set explicitly, Collection::AllRights + * is assumed. + * @param rights the access rights filter values + */ + void setAccessRights(Collection::Rights rights); + + /** + * Returns the access rights that are used for filtering. + */ + Collection::Rights accessRights() const; + + /** + * @reimp + */ + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + + /** + * @reimp + */ + QModelIndexList match(const QModelIndex &start, int role, const QVariant &value, int hits = 1, + Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const Q_DECL_OVERRIDE; + +protected: + bool acceptRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(EntityRightsFilterModel) + EntityRightsFilterModelPrivate *const d_ptr; + //@endcond +}; + +} + +#endif diff --git a/src/core/models/entitytreemodel.cpp b/src/core/models/entitytreemodel.cpp new file mode 100644 index 0000000..13a8bb1 --- /dev/null +++ b/src/core/models/entitytreemodel.cpp @@ -0,0 +1,1204 @@ +/* + 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 "akonadicore_debug.h" +#include "monitor_p.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "attributefactory.h" +#include "changerecorder.h" +#include "collectionmodifyjob.h" +#include "entitydisplayattribute.h" +#include "transactionsequence.h" +#include "itemmodifyjob.h" +#include "session.h" +#include "collectionfetchscope.h" + +#include "collectionutils.h" + +#include "pastehelper_p.h" + +Q_DECLARE_METATYPE(QSet) + +using namespace Akonadi; + +EntityTreeModel::EntityTreeModel(ChangeRecorder *monitor, QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(new EntityTreeModelPrivate(this)) +{ + Q_D(EntityTreeModel); + d->init(monitor); +} + +EntityTreeModel::EntityTreeModel(ChangeRecorder *monitor, EntityTreeModelPrivate *d, QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(d) +{ + d->init(monitor); +} + +EntityTreeModel::~EntityTreeModel() +{ + Q_D(EntityTreeModel); + + foreach (const QList &list, d->m_childEntities) { + QList::const_iterator it = list.constBegin(); + const QList::const_iterator end = list.constEnd(); + for (; it != end; ++it) { + delete *it; + } + } + + d->m_rootNode = 0; + + delete d_ptr; +} + +CollectionFetchScope::ListFilter EntityTreeModel::listFilter() const +{ + Q_D(const EntityTreeModel); + return d->m_listFilter; +} + +void EntityTreeModel::setListFilter(CollectionFetchScope::ListFilter filter) +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + d->m_listFilter = filter; + d->m_monitor->setAllMonitored(filter == CollectionFetchScope::NoFilter); + d->endResetModel(); +} + +void EntityTreeModel::setCollectionsMonitored(const Collection::List &collections) +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + foreach (const Akonadi::Collection &col, d->m_monitor->collectionsMonitored()) { + d->m_monitor->setCollectionMonitored(col, false); + } + foreach (const Akonadi::Collection &col, collections) { + d->m_monitor->setCollectionMonitored(col, true); + } + d->endResetModel(); +} + +void EntityTreeModel::setCollectionMonitored(const Collection &col, bool monitored) +{ + Q_D(EntityTreeModel); + d->m_monitor->setCollectionMonitored(col, monitored); +} + +void EntityTreeModel::setCollectionReferenced(const Akonadi::Collection &col, bool referenced) +{ + Q_D(EntityTreeModel); + Akonadi::Collection referencedCollection = col; + referencedCollection.setReferenced(referenced); + //We have to use the same session as the monitor, so the monitor can fetch the collection afterwards + new Akonadi::CollectionModifyJob(referencedCollection, d->m_monitor->session()); +} + +bool EntityTreeModel::systemEntitiesShown() const +{ + Q_D(const EntityTreeModel); + return d->m_showSystemEntities; +} + +void EntityTreeModel::setShowSystemEntities(bool show) +{ + Q_D(EntityTreeModel); + d->m_showSystemEntities = show; +} + +void EntityTreeModel::clearAndReset() +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + d->endResetModel(); +} + +QHash EntityTreeModel::roleNames() const +{ + QHash names = QAbstractItemModel::roleNames(); + names.insert(EntityTreeModel::UnreadCountRole, "unreadCount"); + names.insert(EntityTreeModel::FetchStateRole, "fetchState"); + names.insert(EntityTreeModel::ItemIdRole, "itemId"); + return names; +} + +int EntityTreeModel::columnCount(const QModelIndex &parent) const +{ +// TODO: Statistics? + if (parent.isValid() && + parent.column() != 0) { + return 0; + } + + return qMax(entityColumnCount(CollectionTreeHeaders), entityColumnCount(ItemListHeaders)); +} + +QVariant EntityTreeModel::entityData(const Item &item, int column, int role) const +{ + Q_D(const EntityTreeModel); + + if (column == 0) { + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + if (item.hasAttribute() && + !item.attribute()->displayName().isEmpty()) { + return item.attribute()->displayName(); + } else { + if (!item.remoteId().isEmpty()) { + return item.remoteId(); + } + return QString(QStringLiteral("<") + QString::number(item.id()) + QStringLiteral(">")); + } + break; + case Qt::DecorationRole: + if (item.hasAttribute() && + !item.attribute()->iconName().isEmpty()) { + return d->iconForName(item.attribute()->iconName()); + } + break; + default: + break; + } + } + + return QVariant(); +} + +QVariant EntityTreeModel::entityData(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(); + } + } + + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + if (column == 0) { + const QString displayName = collection.displayName(); + if (!displayName.isEmpty()) { + return displayName; + } else { + return i18n("Loading..."); + } + } + break; + case Qt::DecorationRole: + if (collection.hasAttribute() && + !collection.attribute()->iconName().isEmpty()) { + return d->iconForName(collection.attribute()->iconName()); + } + return d->iconForName(CollectionUtils::defaultIconName(collection)); + 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)); + } + + // Ugly, but at least the API is clean. + const HeaderGroup headerGroup = static_cast((role / static_cast(TerminalUserRole))); + + role %= TerminalUserRole; + if (!index.isValid()) { + if (ColumnCountRole != role) { + return QVariant(); + } + + return entityColumnCount(headerGroup); + } + + if (ColumnCountRole == role) { + return entityColumnCount(headerGroup); + } + + const Node *node = reinterpret_cast(index.internalPointer()); + + if (ParentCollectionRole == role && + d->m_collectionFetchStrategy != FetchNoCollections) { + 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(); + } + + switch (role) { + case MimeTypeRole: + return collection.mimeType(); + break; + case RemoteIdRole: + return collection.remoteId(); + break; + case CollectionIdRole: + return collection.id(); + break; + case ItemIdRole: + // QVariant().toInt() is 0, not -1, so we have to handle the ItemIdRole + // and CollectionIdRole (below) specially + return -1; + break; + case CollectionRole: + return QVariant::fromValue(collection); + break; + case EntityUrlRole: + return collection.url().url(); + break; + case UnreadCountRole: { + CollectionStatistics statistics = collection.statistics(); + return statistics.unreadCount(); + } + case FetchStateRole: { + return d->m_pendingCollectionRetrieveJobs.contains(collection.id()) ? FetchingState : IdleState; + } + case IsPopulatedRole: { + return d->m_populatedCols.contains(collection.id()); + } + case OriginalCollectionNameRole: { + return entityData(collection, index.column(), Qt::DisplayRole); + } + case Qt::BackgroundRole: { + if (collection.hasAttribute()) { + EntityDisplayAttribute *eda = collection.attribute(); + QColor color = eda->backgroundColor(); + if (color.isValid()) { + return color; + } + } + // fall through. + } + default: + return entityData(collection, index.column(), role); + break; + } + + } else if (Node::Item == node->type) { + const Item item = d->m_items.value(node->id); + if (!item.isValid()) { + return QVariant(); + } + + switch (role) { + case ParentCollectionRole: + return QVariant::fromValue(item.parentCollection()); + 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 CollectionIdRole: + return -1; + break; + case LoadedPartsRole: + return QVariant::fromValue(item.loadedPayloadParts()); + break; + case AvailablePartsRole: + return QVariant::fromValue(item.availablePayloadParts()); + break; + case EntityUrlRole: + return item.url(Akonadi::Item::UrlWithMimeType).url(); + break; + case Qt::BackgroundRole: { + if (item.hasAttribute()) { + EntityDisplayAttribute *eda = item.attribute(); + const QColor color = eda->backgroundColor(); + if (color.isValid()) { + return color; + } + } + // fall through. + } + default: + return entityData(item, index.column(), role); + break; + } + } + + return QVariant(); +} + +Qt::ItemFlags EntityTreeModel::flags(const QModelIndex &index) const +{ + Q_D(const EntityTreeModel); + // Pass modeltest. + if (!index.isValid()) { + return 0; + } + + Qt::ItemFlags flags = QAbstractItemModel::flags(index); + + const Node *node = reinterpret_cast(index.internalPointer()); + + if (Node::Collection == node->type) { + // cut out entities will be shown as inactive + if (d->m_pendingCutCollections.contains(node->id)) { + return Qt::ItemIsSelectable; + } + + 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) { + if (index.column() == 0) { + 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::CanCreateCollection | Collection::CanCreateItem | Collection::CanLinkItem)) { + // Can we drop new collections and items into this collection? + flags |= Qt::ItemIsDropEnabled; + } + + // dragging is always possible, even for read-only objects, but they can only be copied, not moved. + flags |= Qt::ItemIsDragEnabled; + + } + } else if (Node::Item == node->type) { + if (d->m_pendingCutItems.contains(node->id)) { + return Qt::ItemIsSelectable; + } + + // Rights come from the parent collection. + + Collection parentCollection; + if (!index.parent().isValid()) { + parentCollection = d->m_rootCollection; + } else { + const Node *parentNode = reinterpret_cast(index.parent().internalPointer()); + + parentCollection = d->m_collections.value(parentNode->id); + } + if (parentCollection.isValid()) { + const int rights = parentCollection.rights(); + + // Can't drop onto items. + if (rights & Collection::CanChangeItem && index.column() == 0) { + flags = flags | Qt::ItemIsEditable; + } + // dragging is always possible, even for read-only objects, but they can only be copied, not moved. + flags |= Qt::ItemIsDragEnabled; + } + } + + return flags; +} + +Qt::DropActions EntityTreeModel::supportedDropActions() const +{ + return (Qt::CopyAction | Qt::MoveAction | Qt::LinkAction); +} + +QStringList EntityTreeModel::mimeTypes() const +{ + // TODO: Should this return the mimetypes that the items provide? Allow dragging a contact from here for example. + return QStringList() << QStringLiteral("text/uri-list"); +} + +bool EntityTreeModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(row); + Q_UNUSED(column); + Q_D(EntityTreeModel); + + // Can't drop onto Collection::root. + if (!parent.isValid()) { + return false; + } + + // 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. + // Needs to be handled when ordering is accounted for. + + // Handle dropping between items as well as on items. +// if ( row != -1 && column != -1 ) +// { +// } + + 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; + + Node *node = reinterpret_cast(parent.internalId()); + + Q_ASSERT(node); + + if (Node::Item == node->type) { + if (!parent.parent().isValid()) { + // The drop is somehow on an item with no parent (shouldn't happen) + // The drop should be considered handled anyway. + qCWarning(AKONADICORE_LOG) << "Dropped onto item with no parent collection"; + return true; + } + + // A drop onto an item should be considered as a drop onto its parent collection + node = reinterpret_cast(parent.parent().internalId()); + } + + 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()) { + // Accept the event so that it doesn't propagate. + return true; + } + + if (data->hasFormat(QStringLiteral("text/uri-list"))) { + + MimeTypeChecker mimeChecker; + mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes()); + + const QList urls = data->urls(); + foreach (const QUrl &url, urls) { + const Collection collection = d->m_collections.value(Collection::fromUrl(url).id()); + if (collection.isValid()) { + if (collection.parentCollection().id() == destCollection.id() && + action != Qt::CopyAction) { + qCWarning(AKONADICORE_LOG) << "Error: source and destination of move are the same."; + return false; + } + + if (!mimeChecker.isWantedCollection(collection)) { + qCDebug(AKONADICORE_LOG) << "unwanted collection" << mimeChecker.wantedMimeTypes() << collection.contentMimeTypes(); + return false; + } + + QUrlQuery query(url); + if (query.hasQueryItem(QStringLiteral("name"))) { + const QString collectionName = query.queryItemValue(QStringLiteral("name")); + const QStringList collectionNames = d->childCollectionNames(destCollection); + + if (collectionNames.contains(collectionName)) { + QMessageBox::critical(0, i18n("Error"), + i18n("The target collection '%1' contains already\na collection with name '%2'.", + destCollection.name(), collection.name())); + return false; + } + } + } else { + const Item item = d->m_items.value(Item::fromUrl(url).id()); + if (item.isValid()) { + if (item.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) { + qCWarning(AKONADICORE_LOG) << "Error: source and destination of move are the same."; + return false; + } + + if (!mimeChecker.isWantedItem(item)) { + qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType(); + return false; + } + } + } + } + + KJob *job = PasteHelper::pasteUriList(data, destCollection, action, d->m_session); + if (!job) { + return false; + } + + connect(job, SIGNAL(result(KJob*)), SLOT(pasteJobDone(KJob*))); + + // Accpet the event so that it doesn't propagate. + return true; + } 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(); + } + + if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || + d->m_collectionFetchStrategy == FetchNoCollections) { + 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)); + } + } + + Q_ASSERT(collection.parentCollection().isValid()); + const int row = d->indexOf(d->m_childEntities.value(collection.parentCollection().id()), collection.id()); + + Q_ASSERT(row >= 0); + 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); + + if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || + d->m_collectionFetchStrategy == FetchNoCollections) { + if (parent.isValid()) { + return 0; + } else { + return d->m_items.size(); + } + } + + 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(); + } + return d->m_childEntities.value(d->m_rootCollection.id()).size(); + } + + if (parent.column() != 0) { + return 0; + } + + const Node *node = reinterpret_cast(parent.internalPointer()); + + if (!node) { + return 0; + } + + if (Node::Item == node->type) { + return 0; + } + + Q_ASSERT(parent.isValid()); + return d->m_childEntities.value(node->id).size(); +} + +int EntityTreeModel::entityColumnCount(HeaderGroup headerGroup) const +{ + // Not needed in this model. + Q_UNUSED(headerGroup); + + return 1; +} + +QVariant EntityTreeModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup headerGroup) const +{ + Q_D(const EntityTreeModel); + // Not needed in this model. + Q_UNUSED(headerGroup); + + if (section == 0 && + orientation == Qt::Horizontal && + role == Qt::DisplayRole) { + if (d->m_rootCollection == Collection::root()) { + return i18nc("@title:column Name of a thing", "Name"); + } + return d->m_rootCollection.name(); + } + + return QAbstractItemModel::headerData(section, orientation, role); +} + +QVariant EntityTreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + const HeaderGroup headerGroup = static_cast((role / static_cast(TerminalUserRole))); + + role %= TerminalUserRole; + return entityHeaderData(section, orientation, role, headerGroup); +} + +QMimeData *EntityTreeModel::mimeData(const QModelIndexList &indexes) const +{ + Q_D(const EntityTreeModel); + + QMimeData *data = new QMimeData(); + QList 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(Collection::UrlWithName); + } else if (Node::Item == node->type) { + QUrl url = d->m_items.value(node->id).url(Item::Item::UrlWithMimeType); + QUrlQuery query(url); + query.addQueryItem(QStringLiteral("parent"), QString::number(node->parent)); + url.setQuery(query); + urls << url; + } else { // if that happens something went horrible wrong + Q_ASSERT(false); + } + } + + data->setUrls(urls); + + 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 (role == PendingCutRole) { + if (index.isValid() && value.toBool()) { + if (Node::Collection == node->type) { + d->m_pendingCutCollections.append(node->id); + } + + if (Node::Item == node->type) { + d->m_pendingCutItems.append(node->id); + } + } else { + d->m_pendingCutCollections.clear(); + d->m_pendingCutItems.clear(); + } + return true; + } + + if (index.isValid() && + node->type == Node::Collection && + (role == CollectionRefRole || + role == CollectionDerefRole)) { + const Collection collection = index.data(CollectionRole).value(); + Q_ASSERT(collection.isValid()); + + if (role == CollectionDerefRole) { + d->deref(collection.id()); + } else if (role == CollectionRefRole) { + d->ref(collection.id()); + } + return true; + } + + 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()); + } + } + + if (Qt::BackgroundRole == role) { + QColor color = value.value(); + + if (!color.isValid()) { + return false; + } + + EntityDisplayAttribute *eda = collection.attribute(Collection::AddIfMissing); + eda->setBackgroundColor(color); + } + + 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(Item::AddIfMissing); + displayAttribute->setDisplayName(value.toString()); + } + } + + if (Qt::BackgroundRole == role) { + QColor color = value.value(); + + if (!color.isValid()) { + return false; + } + + EntityDisplayAttribute *eda = item.attribute(Item::AddIfMissing); + eda->setBackgroundColor(color); + } + + if (ItemRole == role) { + item = value.value(); + Q_ASSERT(item.id() == node->id); + } + + 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_UNUSED(parent) + return false; +} + +void EntityTreeModel::fetchMore(const QModelIndex &parent) +{ + Q_D(EntityTreeModel); + + if (!d->canFetchMore(parent)) { + return; + } + + if (d->m_collectionFetchStrategy == InvisibleCollectionFetch) { + 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); + + if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || + d->m_collectionFetchStrategy == FetchNoCollections) { + return parent.isValid() ? false : !d->m_items.isEmpty(); + } + + // 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::isCollectionTreeFetched() const +{ + Q_D(const EntityTreeModel); + + return d->m_collectionTreeFetched; +} + +bool EntityTreeModel::isCollectionPopulated(Collection::Id id) const +{ + Q_D(const EntityTreeModel); + return d->m_populatedCols.contains(id); +} + +bool EntityTreeModel::isFullyPopulated() const +{ + Q_D(const EntityTreeModel); + return d->m_collectionTreeFetched && d->m_pendingCollectionRetrieveJobs.isEmpty(); +} + +QModelIndexList EntityTreeModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const +{ + Q_D(const EntityTreeModel); + + if (role == CollectionIdRole || role == CollectionRole) { + Collection::Id id; + if (role == CollectionRole) { + const Collection collection = value.value(); + id = collection.id(); + } else { + id = value.toLongLong(); + } + + QModelIndexList list; + + const Collection collection = d->m_collections.value(id); + + if (!collection.isValid()) { + return list; + } + + const QModelIndex collectionIndex = d->indexForCollection(collection); + Q_ASSERT(collectionIndex.isValid()); + list << collectionIndex; + + return list; + } + + if (role == ItemIdRole || role == ItemRole) { + Item::Id id; + if (role == ItemRole) { + const Item item = value.value(); + id = item.id(); + } else { + id = value.toLongLong(); + } + QModelIndexList list; + + const Item item = d->m_items.value(id); + if (!item.isValid()) { + return list; + } + + return d->indexesForItem(item); + } + + if (role == EntityUrlRole) { + const QUrl url(value.toString()); + const Item item = Item::fromUrl(url); + + if (item.isValid()) { + return d->indexesForItem(d->m_items.value(item.id())); + } + + const Collection collection = Collection::fromUrl(url); + QModelIndexList list; + if (collection.isValid()) { + list << d->indexForCollection(collection); + } + + return list; + } + + return QAbstractItemModel::match(start, role, value, hits, flags); +} + +bool EntityTreeModel::insertRows(int, int, const QModelIndex &) +{ + return false; +} + +bool EntityTreeModel::insertColumns(int, int, const QModelIndex &) +{ + return false; +} + +bool EntityTreeModel::removeRows(int, int, const QModelIndex &) +{ + return false; +} + +bool EntityTreeModel::removeColumns(int, int, const QModelIndex &) +{ + return false; +} + +void EntityTreeModel::setItemPopulationStrategy(ItemPopulationStrategy strategy) +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + d->m_itemPopulation = strategy; + + if (strategy == NoItemPopulation) { + disconnect(d->m_monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)), + this, SLOT(monitoredItemAdded(Akonadi::Item,Akonadi::Collection))); + disconnect(d->m_monitor, SIGNAL(itemChanged(Akonadi::Item,QSet)), + this, SLOT(monitoredItemChanged(Akonadi::Item,QSet))); + disconnect(d->m_monitor, SIGNAL(itemRemoved(Akonadi::Item)), + this, SLOT(monitoredItemRemoved(Akonadi::Item))); + disconnect(d->m_monitor, SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)), + this, SLOT(monitoredItemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); + + disconnect(d->m_monitor, SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection)), + this, SLOT(monitoredItemLinked(Akonadi::Item,Akonadi::Collection))); + disconnect(d->m_monitor, SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection)), + this, SLOT(monitoredItemUnlinked(Akonadi::Item,Akonadi::Collection))); + } + + d->m_monitor->d_ptr->useRefCounting = (strategy == LazyPopulation); + + d->endResetModel(); +} + +EntityTreeModel::ItemPopulationStrategy EntityTreeModel::itemPopulationStrategy() const +{ + Q_D(const EntityTreeModel); + return d->m_itemPopulation; +} + +void EntityTreeModel::setIncludeRootCollection(bool include) +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + d->m_showRootCollection = include; + d->endResetModel(); +} + +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->beginResetModel(); + d->m_collectionFetchStrategy = strategy; + + if (strategy == FetchNoCollections || + strategy == InvisibleCollectionFetch) { + disconnect(d->m_monitor, SIGNAL(collectionChanged(Akonadi::Collection)), + this, SLOT(monitoredCollectionChanged(Akonadi::Collection))); + disconnect(d->m_monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)), + this, SLOT(monitoredCollectionAdded(Akonadi::Collection,Akonadi::Collection))); + disconnect(d->m_monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), + this, SLOT(monitoredCollectionRemoved(Akonadi::Collection))); + disconnect(d->m_monitor, + SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)), + this, SLOT(monitoredCollectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))); + d->m_monitor->fetchCollection(false); + } else { + d->m_monitor->fetchCollection(true); + } + + d->endResetModel(); +} + +EntityTreeModel::CollectionFetchStrategy EntityTreeModel::collectionFetchStrategy() const +{ + Q_D(const EntityTreeModel); + return d->m_collectionFetchStrategy; +} + +static QPair, const EntityTreeModel *> proxiesAndModel(const QAbstractItemModel *model) +{ + QList proxyChain; + const QAbstractProxyModel *proxy = qobject_cast(model); + const QAbstractItemModel *_model = model; + while (proxy) { + proxyChain.prepend(proxy); + _model = proxy->sourceModel(); + proxy = qobject_cast(_model); + } + + const EntityTreeModel *etm = qobject_cast(_model); + return qMakePair(proxyChain, etm); +} + +static QModelIndex proxiedIndex(const QModelIndex &idx, const QList &proxyChain) +{ + QListIterator it(proxyChain); + QModelIndex _idx = idx; + while (it.hasNext()) { + _idx = it.next()->mapFromSource(_idx); + } + return _idx; +} + +QModelIndex EntityTreeModel::modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection) +{ + QPair, const EntityTreeModel *> pair = proxiesAndModel(model); + + Q_ASSERT(pair.second); + QModelIndex idx = pair.second->d_ptr->indexForCollection(collection); + return proxiedIndex(idx, pair.first); +} + +QModelIndexList EntityTreeModel::modelIndexesForItem(const QAbstractItemModel *model, const Item &item) +{ + QPair, const EntityTreeModel *> pair = proxiesAndModel(model); + + if (!pair.second) { + qCWarning(AKONADICORE_LOG) << "Couldn't find an EntityTreeModel"; + return QModelIndexList(); + } + + QModelIndexList list = pair.second->d_ptr->indexesForItem(item); + QModelIndexList proxyList; + foreach (const QModelIndex &idx, list) { + const QModelIndex pIdx = proxiedIndex(idx, pair.first); + if (pIdx.isValid()) { + proxyList << pIdx; + } + } + return proxyList; +} + +#include "moc_entitytreemodel.cpp" diff --git a/src/core/models/entitytreemodel.h b/src/core/models/entitytreemodel.h new file mode 100644 index 0000000..1cd318c --- /dev/null +++ b/src/core/models/entitytreemodel.h @@ -0,0 +1,732 @@ +/* + 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 "akonadicore_export.h" +#include "collection.h" +#include "collectionfetchscope.h" +#include "item.h" + +#include +#include + +namespace Akonadi +{ + +class ChangeRecorder; +class CollectionStatistics; +class Item; +class ItemFetchScope; +class Monitor; +class Session; + +class EntityTreeModelPrivate; + +/** + * @short A model for collections and items together. + * + * Akonadi models and views provide a high level way to interact with the akonadi server. + * Most applications will use these classes. + * + * Models provide an interface for viewing, updating, deleting and moving Items and Collections. + * Additionally, the models are updated automatically if another application changes the + * data or inserts of deletes items etc. + * + * @note The EntityTreeModel should be used with the EntityTreeView or the EntityListView class + * either directly or indirectly via proxy models. + * + *

Retrieving Collections and Items from the model

+ * + * If you want to retrieve and Item or Collection from the model, and already have a valid + * QModelIndex for the correct row, the Collection can be retrieved like this: + * + * @code + * Collection col = index.data( EntityTreeModel::CollectionRole ).value(); + * @endcode + * + * And similarly for Items. This works even if there is a proxy model between the calling code + * and the EntityTreeModel. + * + * If you want to retrieve a Collection for a particular Collection::Id and you do not yet + * have a valid QModelIndex, use modelIndexForCollection. + * + *

Using EntityTreeModel in your application

+ * + * The responsibilities which fall to the application developer are + * - Configuring the ChangeRecorder and EntityTreeModel + * - Making use of this class via proxy models + * - Subclassing for type specific display information + * + *

Creating and configuring the EntityTreeModel

+ * + * This class is a wrapper around a Akonadi::ChangeRecorder object. The model represents a + * part of the collection and item tree configured in the ChangeRecorder. The structure of the + * model mirrors the structure of Collections and Items on the %Akonadi server. + * + * The following code creates a model which fetches items and collections relevant to + * addressees (contacts), and automatically manages keeping the items up to date. + * + * @code + * + * ChangeRecorder *changeRecorder = new ChangeRecorder( this ); + * changeRecorder->setCollectionMonitored( Collection::root() ); + * changeRecorder->setMimeTypeMonitored( KContacts::addresseeMimeType() ); + * changeRecorder->setSession( session ); + * + * EntityTreeModel *model = new EntityTreeModel( changeRecorder, this ); + * + * EntityTreeView *view = new EntityTreeView( this ); + * view->setModel( model ); + * + * @endcode + * + * The EntityTreeModel will show items of a different type by changing the line + * + * @code + * changeRecorder->setMimeTypeMonitored( KContacts::addresseeMimeType() ); + * @endcode + * + * to a different mimetype. KContacts::addresseeMimeType() is an alias for "text/directory". If changed to KMime::Message::mimeType() + * (an alias for "message/rfc822") the model would instead contain emails. The model can be configured to contain items of any mimetype + * known to %Akonadi. + * + * @note The EntityTreeModel does some extra configuration on the Monitor, such as setting itemFetchScope() and collectionFetchScope() + * to retrieve all ancestors. This is necessary for proper function of the model. + * + * @see Akonadi::ItemFetchScope::AncestorRetrieval. + * + * @see akonadi-mimetypes. + * + * The EntityTreeModel can be further configured for certain behaviours such as fetching of collections and items. + * + * The model can be configured to not fetch items into the model (ie, fetch collections only) by setting + * + * @code + * entityTreeModel->setItemPopulationStrategy( EntityTreeModel::NoItemPopulation ); + * @endcode + * + * The items may be fetched lazily, i.e. not inserted into the model until request by the user for performance reasons. + * + * The Collection tree is always built immediately if Collections are to be fetched. + * + * @code + * entityTreeModel->setItemPopulationStrategy( EntityTreeModel::LazyPopulation ); + * @endcode + * + * This will typically be used with a EntityMimeTypeFilterModel in a configuration such as KMail4.5 or AkonadiConsole. + * + * The CollectionFetchStrategy determines how the model will be populated with Collections. That is, if FetchNoCollections is set, + * no collections beyond the root of the model will be fetched. This can be used in combination with setting a particular Collection to monitor. + * + * @code + * // Get an collection id from a config file. + * Collection::Id id; + * monitor->setCollectionMonitored( Collection( id ) ); + * // ... Other initialization code. + * entityTree->setCollectionFetchStrategy( FetchNoCollections ); + * @endcode + * + * This has the effect of creating a model of only a list of Items, and not collections. This is similar in behaviour and aims to the ItemModel. + * By using FetchFirstLevelCollections instead, a mixed list of entities can be created. + * + * @note It is important that you set only one Collection to be monitored in the monitor object. This one collection will be the root of the tree. + * If you need a model with a more complex structure, consider monitoring a common ancestor and using a SelectionProxyModel. + * + * @see lazy-model-population + * + * It is also possible to show the root Collection as part of the selectable model: + * + * @code + * entityTree->setIncludeRootCollection( true ); + * @endcode + * + * + * By default the displayed name of the root collection is '[*]', because it doesn't require i18n, and is generic. It can be changed too. + * + * @code + * entityTree->setIncludeRootCollection( true ); + * entityTree->setRootCollectionDisplayName( i18nc( "Name of top level for all addressbooks in the application", "[All AddressBooks]" ) ) + * @endcode + * + * This feature is used in KAddressBook. + * + * If items are to be fetched by the model, it is necessary to specify which parts of the items + * are to be fetched, using the ItemFetchScope class. By default, only the basic metadata is + * fetched. To fetch all item data, including all attributes: + * + * @code + * changeRecorder->itemFetchScope().fetchFullPayload(); + * changeRecorder->itemFetchScope().fetchAllAttributes(); + * @endcode + * + *

Using EntityTreeModel with Proxy models

+ * + * An Akonadi::SelectionProxyModel can be used to simplify managing selection in one view through multiple proxy models to a representation in another view. + * The selectionModel of the initial view is used to create a proxied model which filters out anything not related to the current selection. + * + * @code + * // ... create an EntityTreeModel + * + * collectionTree = new EntityMimeTypeFilterModel( this ); + * collectionTree->setSourceModel( entityTreeModel ); + * + * // Include only collections in this proxy model. + * collectionTree->addMimeTypeInclusionFilter( Collection::mimeType() ); + * collectionTree->setHeaderGroup( EntityTreeModel::CollectionTreeHeaders ); + * + * treeview->setModel(collectionTree); + * + * // SelectionProxyModel can handle complex selections: + * treeview->setSelectionMode( QAbstractItemView::ExtendedSelection ); + * + * SelectionProxyModel *selProxy = new SelectionProxyModel( treeview->selectionModel(), this ); + * selProxy->setSourceModel( entityTreeModel ); + * + * itemList = new EntityMimeTypeFilterModel( this ); + * itemList->setSourceModel( selProxy ); + * + * // Filter out collections. Show only items. + * itemList->addMimeTypeExclusionFilter( Collection::mimeType() ); + * itemList->setHeaderGroup( EntityTreeModel::ItemListHeaders ); + * + * EntityTreeView *itemView = new EntityTreeView( splitter ); + * itemView->setModel( itemList ); + * @endcode + * + * The SelectionProxyModel can handle complex selections. + * + * See the KSelectionProxyModel documentation for the valid configurations of a Akonadi::SelectionProxyModel. + * + * Obviously, the SelectionProxyModel may be used in a view, or further processed with other proxy models. Typically, the result + * from this model will be further filtered to remove collections from the item list as in the above example. + * + * There are several advantages of using EntityTreeModel with the SelectionProxyModel, namely the items can be fetched and cached + * instead of being fetched many times, and the chain of proxies from the core model to the view is automatically handled. There is + * no need to manage all the mapToSource and mapFromSource calls manually. + * + * A KDescendantsProxyModel can be used to represent all descendants of a model as a flat list. + * For example, to show all descendant items in a selected Collection in a list: + * @code + * collectionTree = new EntityMimeTypeFilterModel( this ); + * collectionTree->setSourceModel( entityTreeModel ); + * + * // Include only collections in this proxy model. + * collectionTree->addMimeTypeInclusionFilter( Collection::mimeType() ); + * collectionTree->setHeaderGroup( EntityTreeModel::CollectionTreeHeaders ); + * + * treeview->setModel( collectionTree ); + * + * SelectionProxyModel *selProxy = new SelectionProxyModel( treeview->selectionModel(), this ); + * selProxy->setSourceModel( entityTreeModel ); + * + * descendedList = new DescendantEntitiesProxyModel( this ); + * descendedList->setSourceModel( selProxy ); + * + * itemList = new EntityMimeTypeFilterModel( this ); + * itemList->setSourceModel( descendedList ); + * + * // Exclude collections from the list view. + * itemList->addMimeTypeExclusionFilter( Collection::mimeType() ); + * itemList->setHeaderGroup( EntityTreeModel::ItemListHeaders ); + * + * listView = new EntityTreeView( this ); + * listView->setModel( itemList ); + * @endcode + * + * + * Note that it is important in this case to use the DescendantEntitesProxyModel before the EntityMimeTypeFilterModel. + * Otherwise, by filtering out the collections first, you would also be filtering out their child items. + * + * This pattern is used in KAddressBook. + * + * It would not make sense to use a KDescendantsProxyModel with LazyPopulation. + * + *

Subclassing EntityTreeModel

+ * + * Usually an application will create a subclass of an EntityTreeModel and use that in several views via proxy models. + * + * The subclassing is necessary in order for the data in the model to have type-specific representation in applications + * + * For example, the headerData for an EntityTreeModel will be different depending on whether it is in a view showing only Collections + * in which case the header data should be "AddressBooks" for example, or only Items, in which case the headerData would be + * for example "Family Name", "Given Name" and "Email addres" for contacts or "Subject", "Sender", "Date" in the case of emails. + * + * Additionally, the actual data shown in the rows of the model should be type specific. + * + * In summary, it must be possible to have different numbers of columns, different data in hte rows of those columns, and different + * titles for each column depending on the contents of the view. + * + * The way this is accomplished is by using the EntityMimeTypeFilterModel for splitting the model into a "CollectionTree" and an "Item List" + * as in the above example, and using a type-specific EntityTreeModel subclass to return the type-specific data, typically for only one type (for example, contacts or emails). + * + * The following protected virtual methods should be implemented in the subclass: + * - int entityColumnCount( HeaderGroup headerGroup ) const; + * -- Implement to return the number of columns for a HeaderGroup. If the HeaderGroup is CollectionTreeHeaders, return the number of columns to display for the + * Collection tree, and if it is ItemListHeaders, return the number of columns to display for the item. In the case of addressee, this could be for example, + * two (for given name and family name) or for emails it could be three (for subject, sender, date). This is a decision of the subclass implementor. + * - QVariant entityHeaderData( int section, Qt::Orientation orientation, int role, HeaderGroup headerGroup ) const; + * -- Implement to return the data for each section for a HeaderGroup. For example, if the header group is CollectionTreeHeaders in a contacts model, + * the string "Address books" might be returned for column 0, whereas if the headerGroup is ItemListHeaders, the strings "Given Name", "Family Name", + * "Email Address" might be returned for the columns 0, 1, and 2. + * - QVariant entityData( const Collection &collection, int column, int role = Qt::DisplayRole ) const; + * -- Implement to return data for a particular Collection. Typically this will be the name of the collection or the EntityDisplayAttribute. + * - QVariant entityData( const Item &item, int column, int role = Qt::DisplayRole ) const; + * -- Implement to return the data for a particular item and column. In the case of email for example, this would be the actual subject, sender and date of the email. + * + * @note The entityData methods are just for convenience. the QAbstractItemMOdel::data method can be overridden if required. + * + * The application writer must then properly configure proxy models for the views, so that the correct data is shown in the correct view. + * That is the purpose of these lines in the above example + * + * @code + * collectionTree->setHeaderGroup( EntityTreeModel::CollectionTreeHeaders ); + * itemList->setHeaderGroup( EntityTreeModel::ItemListHeaders ); + * @endcode + * + *

Progress reporting

+ * + * The EntityTreeModel uses asynchronous Akonadi::Job instances to fill and update itself. + * For example, a job is run to fetch the contents of collections (that is, list the items in it). + * Additionally, individual Akonadi::Items can be fetched in different parts at different times. + * + * To indicate that such a job is underway, the EntityTreeModel makes the FetchState available. The + * FetchState returned from a QModelIndex representing a Akonadi::Collection will be FetchingState if a + * listing of the items in that collection is underway, otherwise the state is IdleState. + * + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADICORE_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 + 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, ///< @internal The Session used by this model + CollectionRefRole, ///< @internal Used to increase the reference count on a Collection + CollectionDerefRole, ///< @internal Used to decrease the reference count on a Collection + PendingCutRole, ///< @internal Used to indicate items which are to be cut + EntityUrlRole, ///< The akonadi:/ Url of the entity as a string. Item urls will contain the mimetype. + UnreadCountRole, ///< Returns the number of unread items in a collection. @since 4.5 + FetchStateRole, ///< Returns the FetchState of a particular item. @since 4.5 + IsPopulatedRole, ///< Returns whether a Collection has been populated, i.e. whether its items have been fetched. @since 4.10 + OriginalCollectionNameRole, ///< Returns original name for collection @since 4.14 + UserRole = Qt::UserRole + 500, ///< First role for user extensions. + TerminalUserRole = 2000, ///< Last role for user extensions. Don't use a role beyond this or headerData will break. + EndRole = 65535 + }; + + /** + * Describes the state of fetch jobs related to particular collections. + * + * @code + * QModelIndex collectionIndex = getIndex(); + * if (collectionIndex.data(EntityTreeModel::FetchStateRole).toLongLong() == FetchingState) { + * // There is a fetch underway + * } else { + * // There is no fetch underway. + * } + * @endcode + * + * @since 4.5 + */ + enum FetchState { + IdleState, ///< There is no fetch of items in this collection in progress. + FetchingState ///< There is a fetch of items in this collection in progress. + // TODO: Change states for reporting of fetching payload parts of items. + }; + + /** + * 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 = 10, ///< Last header information for submodel extensions + EndHeaderGroup = 32 ///< Last headergroup role. Don't use a role beyond this or headerData will break. + // Note that we're splitting up available roles for the header data hack and int(EndRole / TerminalUserRole) == 32 + }; + + /** + * Creates a new entity tree model. + * + * @param monitor The ChangeRecorder whose entities should be represented in the model. + * @param parent The parent object. + */ + explicit EntityTreeModel(ChangeRecorder *monitor, QObject *parent = Q_NULLPTR); + + /** + * 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) + }; + + /** + * Some Entities are hidden in the model, but exist for internal purposes, for example, custom object + * directories in groupware resources. + * They are hidden by default, but can be shown by setting @p show to true. + * @param show enabled displaying of hidden entities if set as @c true + * Most applications will not need to use this feature. + */ + void setShowSystemEntities(bool show); + + /** + * Returns @c true if internal system entities are shown, and @c false otherwise. + */ + bool systemEntitiesShown() const; + + /** + * Returns the currently used listfilter. + * + * @since 4.14 + */ + Akonadi::CollectionFetchScope::ListFilter listFilter() const; + + /** + * Sets the currently used listfilter. + * + * @since 4.14 + */ + void setListFilter(Akonadi::CollectionFetchScope::ListFilter filter); + + /** + * Monitors the specified collections and resets the model. + * + * @since 4.14 + */ + void setCollectionsMonitored(const Akonadi::Collection::List &collections); + + /** + * Adds or removes a specific collection from the monitored set without resetting the model. + * Only call this if you're monitoring specific collections (not mimetype/resources/items). + * + * @since 4.14 + * @see setCollectionsMonitored() + */ + void setCollectionMonitored(const Akonadi::Collection &col, bool monitored = true); + + /** + * References a collection and starts to monitor it. + * + * Use this to temporarily include a collection that is not enabled. + * + * @since 4.14 + */ + void setCollectionReferenced(const Akonadi::Collection &col, bool referenced = true); + + /** + * 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 whether the root collection shall be provided by the model. + * @param include enables root collection if set as @c true + * @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 "[*]". + * @param name the name to display for the root collection + * @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. + InvisibleCollectionFetch ///< Fetches collections, but does not put them in the model. This can be used to create a list of items in all collections. The ParentCollectionRole can still be used to retrieve the parent collection of an Item. @since 4.5 + }; + + /** + * Sets the collection fetch @p strategy of the model. + */ + void setCollectionFetchStrategy(CollectionFetchStrategy strategy); + + /** + * Returns the collection fetch strategy of the model. + */ + CollectionFetchStrategy collectionFetchStrategy() const; + + QHash roleNames() const Q_DECL_OVERRIDE; + + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + QStringList mimeTypes() const Q_DECL_OVERRIDE; + + Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; + QMimeData *mimeData(const QModelIndexList &indexes) const Q_DECL_OVERRIDE; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) Q_DECL_OVERRIDE; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + + // TODO: Review the implementations of these. I think they could be better. + bool canFetchMore(const QModelIndex &parent) const Q_DECL_OVERRIDE; + void fetchMore(const QModelIndex &parent) Q_DECL_OVERRIDE; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + + /** + * Returns whether the collection tree has been fetched at initialisation. + * + * @see collectionTreeFetched + * @since 4.10 + */ + bool isCollectionTreeFetched() const; + + /** + * Returns whether the collection has been populated. + * + * @see collectionPopulated + * @since 4.12 + */ + bool isCollectionPopulated(Akonadi::Collection::Id) const; + + /** + * Returns whether the model is fully populated. + * + * Returns true once the collection tree has been fetched and all collections have been populated. + * + * @see isCollectionPopulated + * @see isCollectionTreeFetched + * @since 4.14 + */ + bool isFullyPopulated() const; + + /** + * Reimplemented to handle the AmazingCompletionRole. + */ + QModelIndexList match(const QModelIndex &start, int role, const QVariant &value, int hits = 1, Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const Q_DECL_OVERRIDE; + + /** + * Returns a QModelIndex in @p model which points to @p collection. + * This method can be used through proxy models if @p model is a proxy model. + * @code + * EntityTreeModel *model = getEntityTreeModel(); + * QSortFilterProxyModel *proxy1 = new QSortFilterProxyModel; + * proxy1->setSourceModel(model); + * QSortFilterProxyModel *proxy2 = new QSortFilterProxyModel; + * proxy2->setSourceModel(proxy1); + * + * ... + * + * QModelIndex idx = EntityTreeModel::modelIndexForCollection(proxy2, Collection(colId)); + * if (!idx.isValid()) + * // Collection with id colId is not in the proxy2. + * // Maybe it is filtered out if proxy 2 is only showing items? Make sure you use the correct proxy. + * return; + * + * Collection collection = idx.data( EntityTreeModel::CollectionRole ).value(); + * // collection has the id colId, and all other attributes already fetched by the model such as name, remoteId, Akonadi::Attributes etc. + * + * @endcode + * + * This can be useful for example if an id is stored in a config file and needs to be used in the application. + * + * Note however, that to restore view state such as scrolling, selection and expansion of items in trees, the ETMViewStateSaver can be used for convenience. + * + * @see modelIndexesForItem + * @since 4.5 + */ + static QModelIndex modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection); + + /** + * Returns a QModelIndex in @p model which points to @p item. + * This method can be used through proxy models if @p model is a proxy model. + * @param model the model to query for the item + * @param item the item to look for + * @see modelIndexForCollection + * @since 4.5 + */ + static QModelIndexList modelIndexesForItem(const QAbstractItemModel *model, const Item &item); + +Q_SIGNALS: + /** + * Signal emitted when the collection tree has been fetched for the first time. + * @param collections list of collections which have been fetched + * + * @see isCollectionTreeFetched, collectionPopulated + * @since 4.10 + */ + void collectionTreeFetched(const Akonadi::Collection::List &collections); + + /** + * Signal emitted when a collection has been populated, i.e. its items have been fetched. + * @param collectionId id of the collection which has been populated + * + * @see collectionTreeFetched + * @since 4.10 + */ + void collectionPopulated(Akonadi::Collection::Id collectionId); + /** + * Emitted once a collection has been fetched for the very first time. + * This is like a dataChanged(), but specific to the initial loading, in order to update + * the GUI (window caption, state of actions). + * Usually, the GUI uses Akonadi::Monitor to be notified of further changes to the collections. + * @param collectionId the identifier of the fetched collection + * @since 4.9.3 + */ + void collectionFetched(int collectionId); + +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 entityData(const Item &item, int column, int role = Qt::DisplayRole) const; + + /** + * Provided for convenience of subclasses. + */ + virtual QVariant entityData(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 entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup headerGroup) const; + + virtual int entityColumnCount(HeaderGroup headerGroup) const; + +protected: + //@cond PRIVATE + Q_DECLARE_PRIVATE(EntityTreeModel) + EntityTreeModelPrivate *d_ptr; + EntityTreeModel(ChangeRecorder *monitor, EntityTreeModelPrivate *d, QObject *parent = Q_NULLPTR); + //@endcond + +private: + //@cond PRIVATE + // Make these private, they shouldn't be called by applications + bool insertRows(int row, int count, const QModelIndex &index = QModelIndex()) Q_DECL_OVERRIDE; + bool insertColumns(int column, int count, const QModelIndex &index = QModelIndex()) Q_DECL_OVERRIDE; + bool removeColumns(int column, int count, const QModelIndex &index = QModelIndex()) Q_DECL_OVERRIDE; + bool removeRows(int row, int count, const QModelIndex &index = QModelIndex()) Q_DECL_OVERRIDE; + + 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 serverStarted()) + + Q_PRIVATE_SLOT(d_func(), void itemFetchJobDone(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void collectionFetchJobDone(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void rootFetchJobDone(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void pasteJobDone(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 collectionListFetched(Akonadi::Collection::List)) + Q_PRIVATE_SLOT(d_func(), void topLevelCollectionsFetched(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 monitoredCollectionsChanged(const Akonadi::Collection &, bool)) + Q_PRIVATE_SLOT(d_func(), void monitoredItemsChanged(const Akonadi::Item &, bool)) + Q_PRIVATE_SLOT(d_func(), void monitoredResourcesChanged(const QByteArray &, 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 &)) + Q_PRIVATE_SLOT(d_func(), void changeFetchState(const Akonadi::Collection &)) + + Q_PRIVATE_SLOT(d_func(), void agentInstanceRemoved(Akonadi::AgentInstance)) + Q_PRIVATE_SLOT(d_func(), void monitoredItemsRetrieved(KJob *job)) + //@endcond +}; + +} // namespace + +#endif diff --git a/src/core/models/entitytreemodel_p.cpp b/src/core/models/entitytreemodel_p.cpp new file mode 100644 index 0000000..670337f --- /dev/null +++ b/src/core/models/entitytreemodel_p.cpp @@ -0,0 +1,1955 @@ +/* + 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 "agentmanagerinterface.h" +#include "KDBusConnectionPool" +#include "monitor_p.h" // For friend ref/deref +#include "servermanager.h" +#include "vectorhelper.h" + +#include + +#include "agentmanager.h" +#include "agenttype.h" +#include "changerecorder.h" +#include "collectioncopyjob.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmovejob.h" +#include "collectionstatistics.h" +#include "collectionstatisticsjob.h" +#include "entityhiddenattribute.h" +#include "itemcopyjob.h" +#include "itemfetchjob.h" +#include "itemmodifyjob.h" +#include "itemmovejob.h" +#include "linkjob.h" +#include "session.h" +#include "private/protocol_p.h" + +#include "akonadicore_debug.h" + +#include +#include + +QHash jobTimeTracker; + +Q_LOGGING_CATEGORY(DebugETM, "org.kde.akonadi.ETM") + +using namespace Akonadi; + +static CollectionFetchJob::Type getFetchType(EntityTreeModel::CollectionFetchStrategy strategy) +{ + switch (strategy) { + case EntityTreeModel::FetchFirstLevelChildCollections: + return CollectionFetchJob::FirstLevel; + case EntityTreeModel::InvisibleCollectionFetch: + case EntityTreeModel::FetchCollectionsRecursive: + default: + break; + } + return CollectionFetchJob::Recursive; +} + +EntityTreeModelPrivate::EntityTreeModelPrivate(EntityTreeModel *parent) + : q_ptr(parent) + , m_rootNode(0) + , m_collectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive) + , m_itemPopulation(EntityTreeModel::ImmediatePopulation) + , m_listFilter(CollectionFetchScope::NoFilter) + , m_includeStatistics(false) + , m_showRootCollection(false) + , m_collectionTreeFetched(false) + , m_showSystemEntities(false) +{ + // using collection as a parameter of a queued call in runItemFetchJob() + qRegisterMetaType(); + + Akonadi::AgentManager *agentManager = Akonadi::AgentManager::self(); + QObject::connect(agentManager, SIGNAL(instanceRemoved(Akonadi::AgentInstance)), + q_ptr, SLOT(agentInstanceRemoved(Akonadi::AgentInstance))); + +} + +EntityTreeModelPrivate::~EntityTreeModelPrivate() +{ +} + +void EntityTreeModelPrivate::init(ChangeRecorder *monitor) +{ + Q_Q(EntityTreeModel); + m_monitor = monitor; + // The default is to FetchCollectionsRecursive, so we tell the monitor to fetch collections + // That way update signals from the monitor will contain the full collection. + // This may be updated if the CollectionFetchStrategy is changed. + m_monitor->fetchCollection(true); + m_session = m_monitor->session(); + + m_monitor->setChangeRecordingEnabled(false); + + m_rootCollectionDisplayName = QStringLiteral("[*]"); + + m_includeStatistics = true; + m_monitor->fetchCollectionStatistics(true); + m_monitor->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); + + q->connect(monitor, SIGNAL(mimeTypeMonitored(QString,bool)), + SLOT(monitoredMimeTypeChanged(QString,bool))); + q->connect(monitor, SIGNAL(collectionMonitored(Akonadi::Collection,bool)), + SLOT(monitoredCollectionsChanged(Akonadi::Collection,bool))); + q->connect(monitor, SIGNAL(itemMonitored(Akonadi::Item,bool)), + SLOT(monitoredItemsChanged(Akonadi::Item,bool))); + q->connect(monitor, SIGNAL(resourceMonitored(QByteArray,bool)), + SLOT(monitoredResourcesChanged(QByteArray,bool))); + + // monitor collection changes + q->connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection)), + SLOT(monitoredCollectionChanged(Akonadi::Collection))); + q->connect(monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)), + SLOT(monitoredCollectionAdded(Akonadi::Collection,Akonadi::Collection))); + q->connect(monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), + SLOT(monitoredCollectionRemoved(Akonadi::Collection))); + q->connect(monitor, + SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)), + SLOT(monitoredCollectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))); + + // Monitor item changes. + q->connect(monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)), + SLOT(monitoredItemAdded(Akonadi::Item,Akonadi::Collection))); + q->connect(monitor, SIGNAL(itemChanged(Akonadi::Item,QSet)), + SLOT(monitoredItemChanged(Akonadi::Item,QSet))); + q->connect(monitor, SIGNAL(itemRemoved(Akonadi::Item)), + SLOT(monitoredItemRemoved(Akonadi::Item))); + q->connect(monitor, SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)), + SLOT(monitoredItemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); + + q->connect(monitor, SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection)), + SLOT(monitoredItemLinked(Akonadi::Item,Akonadi::Collection))); + q->connect(monitor, SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection)), + SLOT(monitoredItemUnlinked(Akonadi::Item,Akonadi::Collection))); + + q->connect(monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), + SLOT(monitoredCollectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics))); + + Akonadi::ServerManager *serverManager = Akonadi::ServerManager::self(); + q->connect(serverManager, SIGNAL(started()), SLOT(serverStarted())); + + fillModel(); +} + +void EntityTreeModelPrivate::serverStarted() +{ + // Don't emit about to be reset. Too late for that + endResetModel(); +} + +void EntityTreeModelPrivate::changeFetchState(const Collection &parent) +{ + Q_Q(EntityTreeModel); + const QModelIndex collectionIndex = indexForCollection(parent); + if (!collectionIndex.isValid()) { + // Because we are called delayed, it is possible that @p parent has been deleted. + return; + } + q->dataChanged(collectionIndex, collectionIndex); +} + +void EntityTreeModelPrivate::agentInstanceRemoved(const Akonadi::AgentInstance &instance) +{ + Q_Q(EntityTreeModel); + if (!instance.type().capabilities().contains(QStringLiteral("Resource"))) { + return; + } + + if (m_rootCollection.isValid()) { + if (m_rootCollection != Collection::root()) { + if (m_rootCollection.resource() == instance.identifier()) { + q->clearAndReset(); + } + return; + } + foreach (Node *node, m_childEntities[Collection::root().id()]) { + Q_ASSERT(node->type == Node::Collection); + + const Collection collection = m_collections[node->id]; + if (collection.resource() == instance.identifier()) { + monitoredCollectionRemoved(collection); + } + } + } +} + +void EntityTreeModelPrivate::fetchItems(const Collection &parent) +{ + Q_Q(const EntityTreeModel); + Q_ASSERT(parent.isValid()); + Q_ASSERT(m_collections.contains(parent.id())); + // TODO: Use a more specific fetch scope to get only the envelope for mails etc. + ItemFetchJob *itemFetchJob = new Akonadi::ItemFetchJob(parent, m_session); + itemFetchJob->setFetchScope(m_monitor->itemFetchScope()); + itemFetchJob->fetchScope().setAncestorRetrieval(ItemFetchScope::All); + itemFetchJob->fetchScope().setIgnoreRetrievalErrors(true); + itemFetchJob->setDeliveryOption(ItemFetchJob::EmitItemsInBatches); + + itemFetchJob->setProperty(FetchCollectionId().constData(), QVariant(parent.id())); + + if (m_showRootCollection || parent != m_rootCollection) { + m_pendingCollectionRetrieveJobs.insert(parent.id()); + + // If collections are not in the model, there will be no valid index for them. + if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && + (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections)) { + // We need to invoke this delayed because we would otherwise be emitting a sequence like + // - beginInsertRows + // - dataChanged + // - endInsertRows + // which would confuse proxies. + QMetaObject::invokeMethod(const_cast(q), "changeFetchState", Qt::QueuedConnection, Q_ARG(Akonadi::Collection, parent)); + } + } + + q->connect(itemFetchJob, SIGNAL(itemsReceived(Akonadi::Item::List)), + q, SLOT(itemsFetched(Akonadi::Item::List))); + q->connect(itemFetchJob, SIGNAL(result(KJob*)), + q, SLOT(itemFetchJobDone(KJob*))); + qCDebug(DebugETM) << "collection:" << parent.name(); jobTimeTracker[itemFetchJob].start(); +} + +void EntityTreeModelPrivate::fetchCollections(Akonadi::CollectionFetchJob *job) +{ + Q_Q(EntityTreeModel); + + job->fetchScope().setListFilter(m_listFilter); + job->fetchScope().setContentMimeTypes(m_monitor->mimeTypesMonitored()); + m_pendingCollectionFetchJobs.insert(static_cast(job)); + + if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) { + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + q, SLOT(collectionListFetched(Akonadi::Collection::List))); + } else { + job->fetchScope().setIncludeStatistics(m_includeStatistics); + job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + q, SLOT(collectionsFetched(Akonadi::Collection::List))); + } + q->connect(job, SIGNAL(result(KJob*)), + q, SLOT(collectionFetchJobDone(KJob*))); + + qCDebug(DebugETM) << "collection:" << job->collections(); jobTimeTracker[job].start(); +} + +void EntityTreeModelPrivate::fetchCollections(const Collection::List &collections, CollectionFetchJob::Type type) +{ + fetchCollections(new CollectionFetchJob(collections, type, m_session)); +} + +void EntityTreeModelPrivate::fetchCollections(const Collection &collection, CollectionFetchJob::Type type) +{ + Q_ASSERT(collection.isValid()); + CollectionFetchJob *job = new CollectionFetchJob(collection, type, m_session); + fetchCollections(job); +} + +namespace Akonadi +{ + +template +inline bool EntityTreeModelPrivate::isHiddenImpl(const T &entity, Node::Type type) const +{ + if (m_showSystemEntities) { + return false; + } + + if (type == Node::Collection && + entity.id() == m_rootCollection.id()) { + return false; + } + + // entity.hasAttribute() does not compile w/ GCC for + // some reason + if (entity.hasAttribute(EntityHiddenAttribute().type())) { + return true; + } + + const Collection parent = entity.parentCollection(); + if (parent.isValid()) { + return isHiddenImpl(parent, Node::Collection); + } + + return false; +} + +} + +bool EntityTreeModelPrivate::isHidden(const Akonadi::Collection &collection) const +{ + return isHiddenImpl(collection, Node::Collection); +} + +bool EntityTreeModelPrivate::isHidden(const Akonadi::Item &item) const +{ + return isHiddenImpl(item, Node::Item); +} + +void EntityTreeModelPrivate::collectionListFetched(const Akonadi::Collection::List &collections) +{ + QVectorIterator it(collections); + + while (it.hasNext()) { + const Collection collection = it.next(); + + if (isHidden(collection)) { + continue; + } + + m_collections.insert(collection.id(), collection); + + Node *node = new Node; + node->id = collection.id(); + node->parent = -1; + node->type = Node::Collection; + m_childEntities[-1].prepend(node); + + fetchItems(collection); + } +} + +static QSet getChildren(Collection::Id parent, const QHash &childParentMap) +{ + QSet children; + for (auto it = childParentMap.cbegin(), e = childParentMap.cend(); it != e; ++it) { + if (it.value() == parent) { + children << it.key(); + children += getChildren(it.key(), childParentMap); + } + } + return children; +} + +void EntityTreeModelPrivate::collectionsFetched(const Akonadi::Collection::List &collections) +{ + Q_Q(EntityTreeModel); + QTime t; + t.start(); + + QVectorIterator it(collections); + + QHash collectionsToInsert; + + while (it.hasNext()) { + const Collection collection = it.next(); + + const Collection::Id collectionId = collection.id(); + + if (isHidden(collection)) { + continue; + } + + if (m_collections.contains(collectionId)) { + // This is probably the result of a parent of a previous collection already being in the model. + // Replace the dummy collection with the real one and move on. + + // This could also be the result of a monitor signal having already inserted the collection + // into this model. There's no way to tell, so we just emit dataChanged. + + m_collections[collectionId] = collection; + + const QModelIndex collectionIndex = indexForCollection(collection); + dataChanged(collectionIndex, collectionIndex); + emit q->collectionFetched(collectionId); + continue; + } + + //If we're monitoring collections somewhere in the tree we need to retrieve their ancestors now + if (collection.parentCollection() != m_rootCollection && m_monitor->collectionsMonitored().contains(collection)) { + retrieveAncestors(collection, false); + } + + collectionsToInsert.insert(collectionId, collection); + } + + //Build a list of subtrees to insert, with the root of the subtree on the left, and the complete subtree including root on the right + QHash > subTreesToInsert; + { + //Build a child-parent map that allows us to build the subtrees afterwards + QHash childParentMap; + Q_FOREACH (const Collection &col, collectionsToInsert) { + childParentMap.insert(col.id(), col.parentCollection().id()); + + //Complete the subtree up to the last known parent + Collection parent = col.parentCollection(); + while (parent.isValid() && parent != m_rootCollection && !m_collections.contains(parent.id())) { + childParentMap.insert(parent.id(), parent.parentCollection().id()); + + if (!collectionsToInsert.contains(parent.id())) { + collectionsToInsert.insert(parent.id(), parent); + } + parent = parent.parentCollection(); + } + } + + QSet parents; + + //Find toplevel parents of the subtrees + for (auto it = childParentMap.cbegin(), e = childParentMap.cend(); it != e; ++it) { + //The child has a parent without parent (it's a toplevel node that is not yet in m_collections) + if (!childParentMap.contains(it.value())) { + Q_ASSERT(!m_collections.contains(it.key())); + parents << it.key(); + } + } + + //Find children of each subtree + Q_FOREACH (Collection::Id p, parents) { + QSet children; + //We add the parent itself as well so it can be inserted below as part of the same loop + children << p; + children += getChildren(p, childParentMap); + subTreesToInsert[p] = children; + } + } + + const int row = 0; + + QHashIterator > collectionIt(subTreesToInsert); + while (collectionIt.hasNext()) { + collectionIt.next(); + + const Collection::Id topCollectionId = collectionIt.key(); + qCDebug(DebugETM) << "Subtree: " << topCollectionId << collectionIt.value(); + + Q_ASSERT(!m_collections.contains(topCollectionId)); + Collection topCollection = collectionsToInsert.value(topCollectionId); + Q_ASSERT(topCollection.isValid()); + + //The toplevels parent must already be part of the model + Q_ASSERT(m_collections.contains(topCollection.parentCollection().id())); + const QModelIndex parentIndex = indexForCollection(topCollection.parentCollection()); + + q->beginInsertRows(parentIndex, row, row); + Q_ASSERT(!collectionIt.value().isEmpty()); + + foreach (Collection::Id collectionId, collectionIt.value()) { + const Collection collection = collectionsToInsert.take(collectionId); + Q_ASSERT(collection.isValid()); + + m_collections.insert(collectionId, collection); + + Node *node = new Node; + node->id = collectionId; + Q_ASSERT(collection.parentCollection().isValid()); + node->parent = collection.parentCollection().id(); + node->type = Node::Collection; + m_childEntities[node->parent].prepend(node); + } + q->endInsertRows(); + + if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { + foreach (const Collection::Id &collectionId, collectionIt.value()) { + fetchItems(m_collections.value(collectionId)); + } + } + } +} + +void EntityTreeModelPrivate::itemsFetched(const Akonadi::Item::List &items) +{ + Q_Q(EntityTreeModel); + + const Collection::Id collectionId = q->sender()->property(FetchCollectionId().constData()).value(); + + itemsFetched(collectionId, items); +} + +void EntityTreeModelPrivate::itemsFetched(const Collection::Id collectionId, const Akonadi::Item::List &items) +{ + Q_Q(EntityTreeModel); + + if (!m_collections.contains(collectionId)) { + qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items"; + return; + } + + Item::List itemsToInsert; + + const Collection collection = m_collections.value(collectionId); + + Q_ASSERT(collection.isValid()); + + // if there are any items at all, remove from set of collections known to be empty + if (!items.isEmpty()) { + m_collectionsWithoutItems.remove(collectionId); + } + + foreach (const Item &item, items) { + + if (isHidden(item)) { + continue; + } + + if ((m_mimeChecker.wantedMimeTypes().isEmpty() || + m_mimeChecker.isWantedItem(item))) { + // When listing virtual collections we might get results for items which are already in + // the model if their concrete collection has already been listed. + // In that case the collectionId should be different though. + + // As an additional complication, new items might be both part of fetch job results and + // part of monitor notifications. We only insert items which are not already in the model + // considering their (possibly virtual) parent. + bool isNewItem = true; + if (m_items.contains(item.id())) { + const Akonadi::Collection::List parents = getParentCollections(item); + foreach (const Akonadi::Collection &parent, parents) { + if (parent.id() == collectionId) { + qCWarning(AKONADICORE_LOG) << "Fetched an item which is already in the model"; + // Update it in case the revision changed; + m_items[item.id()].apply(item); + isNewItem = false; + break; + } + } + } + + if (isNewItem) { + itemsToInsert << item; + } + } + } + + if (itemsToInsert.size() > 0) { + const Collection::Id colId = m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch ? m_rootCollection.id() + : m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections ? m_rootCollection.id() + : collectionId; + const int startRow = m_childEntities.value(colId).size(); + + Q_ASSERT(m_collections.contains(colId)); + + const QModelIndex parentIndex = indexForCollection(m_collections.value(colId)); + q->beginInsertRows(parentIndex, startRow, startRow + itemsToInsert.size() - 1); + + foreach (const Item &item, itemsToInsert) { + const Item::Id itemId = item.id(); + // Don't reinsert when listing virtual collections. + if (!m_items.contains(item.id())) { + m_items.insert(itemId, item); + } + + Node *node = new Node; + node->id = itemId; + node->parent = collectionId; + node->type = Node::Item; + + m_childEntities[colId].append(node); + } + q->endInsertRows(); + } +} + +void EntityTreeModelPrivate::monitoredMimeTypeChanged(const QString &mimeType, bool monitored) +{ + beginResetModel(); + if (monitored) { + m_mimeChecker.addWantedMimeType(mimeType); + } else { + m_mimeChecker.removeWantedMimeType(mimeType); + } + endResetModel(); +} + +void EntityTreeModelPrivate::monitoredCollectionsChanged(const Akonadi::Collection &collection, bool monitored) +{ + if (monitored) { + const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy); + fetchCollections(collection, CollectionFetchJob::Base); + fetchCollections(collection, fetchType); + } else { + //If a collection is derefernced and no longer explicitly monitored it might still match other filters + if (!shouldBePartOfModel(collection)) { + monitoredCollectionRemoved(collection); + } + } +} + +void EntityTreeModelPrivate::monitoredItemsChanged(const Akonadi::Item &item, bool monitored) +{ + Q_UNUSED(item) + Q_UNUSED(monitored) + beginResetModel(); + endResetModel(); +} + +void EntityTreeModelPrivate::monitoredResourcesChanged(const QByteArray &resource, bool monitored) +{ + Q_UNUSED(resource) + Q_UNUSED(monitored) + beginResetModel(); + endResetModel(); +} + +void EntityTreeModelPrivate::retrieveAncestors(const Akonadi::Collection &collection, bool insertBaseCollection) +{ + Q_Q(EntityTreeModel); + + Collection parentCollection = collection.parentCollection(); + + Q_ASSERT(parentCollection.isValid()); + Q_ASSERT(parentCollection != Collection::root()); + + Collection::List ancestors; + + while (parentCollection != Collection::root() && !m_collections.contains(parentCollection.id())) { + // Put a temporary node in the tree later. + ancestors.prepend(parentCollection); + + parentCollection = parentCollection.parentCollection(); + } + Q_ASSERT(parentCollection.isValid()); + // if m_rootCollection is Collection::root(), we always have common ancestor and do the retrival + // if we traversed up to Collection::root() but are looking at a subtree only (m_rootCollection != Collection::root()) + // we have no common ancestor, and we don't have to retrieve anything + if (parentCollection == Collection::root() && m_rootCollection != Collection::root()) { + return; + } + + if (ancestors.isEmpty() && !insertBaseCollection) { + //Nothing to do, avoid emitting insert signals + return; + } + + if (!ancestors.isEmpty()) { + // Fetch the real ancestors + CollectionFetchJob *job = new CollectionFetchJob(ancestors, CollectionFetchJob::Base, m_session); + job->fetchScope().setListFilter(m_listFilter); + job->fetchScope().setIncludeStatistics(m_includeStatistics); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + q, SLOT(ancestorsFetched(Akonadi::Collection::List))); + q->connect(job, SIGNAL(result(KJob*)), + q, SLOT(collectionFetchJobDone(KJob*))); + } + +// Q_ASSERT( parentCollection != m_rootCollection ); + const QModelIndex parent = 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 = ancestors.constBegin(); + const Collection::List::const_iterator end = ancestors.constEnd(); + + for (; it != end; ++it) { + const Collection ancestor = *it; + Q_ASSERT(ancestor.parentCollection().isValid()); + m_collections.insert(ancestor.id(), ancestor); + + Node *node = new Node; + node->id = ancestor.id(); + node->parent = ancestor.parentCollection().id(); + node->type = Node::Collection; + m_childEntities[node->parent].prepend(node); + } + + if (insertBaseCollection) { + m_collections.insert(collection.id(), collection); + Node *node = new Node; + node->id = collection.id(); + // Can't just use parentCollection because that doesn't necessarily refer to collection. + node->parent = collection.parentCollection().id(); + node->type = Node::Collection; + m_childEntities[node->parent].prepend(node); + } + + q->endInsertRows(); +} + +void EntityTreeModelPrivate::ancestorsFetched(const Akonadi::Collection::List &collectionList) +{ + foreach (const Collection &collection, collectionList) { + m_collections[collection.id()] = collection; + + const QModelIndex index = indexForCollection(collection); + Q_ASSERT(index.isValid()); + dataChanged(index, index); + } +} + +void EntityTreeModelPrivate::insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + Q_ASSERT(collection.isValid()); + Q_ASSERT(parent.isValid()); + + Q_Q(EntityTreeModel); + + const int row = 0; + const QModelIndex parentIndex = 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(); +} + +bool EntityTreeModelPrivate::hasChildCollection(const Collection &collection) const +{ + foreach (Node *node, m_childEntities[collection.id()]) { + if (node->type == Node::Collection) { + const Collection subcol = m_collections[node->id]; + if (shouldBePartOfModel(subcol)) { + return true; + } + } + } + return false; +} + +bool EntityTreeModelPrivate::isAncestorMonitored(const Collection &collection) const +{ + Akonadi::Collection parent = collection.parentCollection(); + while (parent.isValid()) { + if (m_monitor->collectionsMonitored().contains(parent)) { + return true; + } + parent = parent.parentCollection(); + } + return false; +} + +bool EntityTreeModelPrivate::shouldBePartOfModel(const Collection &collection) const +{ + if (isHidden(collection)) { + return false; + } + + // We want a parent collection if it has at least one child that matches the + // wanted mimetype + if (hasChildCollection(collection)) { + return true; + } + + //Explicitly monitored collection + if (m_monitor->collectionsMonitored().contains(collection)) { + return true; + } + + //We're explicitly monitoring collections, but didn't match the filter + if (m_mimeChecker.wantedMimeTypes().isEmpty() && !m_monitor->collectionsMonitored().isEmpty()) { + //The collection should be included if one of the parents is monitored + if (isAncestorMonitored(collection)) { + return true; + } + return false; + } + + // 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 false; + } + + if (m_listFilter == CollectionFetchScope::Enabled) { + if (!collection.enabled() && !collection.referenced()) { + return false; + } + } else if (m_listFilter == CollectionFetchScope::Display) { + if (!collection.shouldList(Collection::ListDisplay)) { + return false; + } + } else if (m_listFilter == CollectionFetchScope::Sync) { + if (!collection.shouldList(Collection::ListSync)) { + return false; + } + } else if (m_listFilter == CollectionFetchScope::Index) { + if (!collection.shouldList(Collection::ListIndex)) { + return false; + } + } + + return true; +} + +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 + + // If a fetch job is started and a collection is added to akonadi after the fetch job is started, the + // new collection will be added to the fetch job results. It will also be notified through the monitor. + // We return early here in that case. + if (m_collections.contains(collection.id())) { + return; + } + + //If the resource is explicitly monitored all other checks are skipped. topLevelCollectionsFetched still checks the hidden attribute. + if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && + collection.parentCollection() == Collection::root()) { + return topLevelCollectionsFetched(Collection::List() << collection); + } + + if (!shouldBePartOfModel(collection)) { + return; + } + + 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. + if (collection != Collection::root()) { + retrieveAncestors(collection); + } + if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { + fetchItems(collection); + } + return; + } + + insertCollection(collection, parent); + if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { + fetchItems(collection); + } +} + +void EntityTreeModelPrivate::monitoredCollectionRemoved(const Akonadi::Collection &collection) +{ + //if an explictly monitored collection is removed, we would also have to remove collections which were included to show it (as in the move case) + if ((collection == m_rootCollection) || + m_monitor->collectionsMonitored().contains(collection)) { + beginResetModel(); + endResetModel(); + return; + } + + Collection::Id parentId = collection.parentCollection().id(); + + if (parentId < 0) { + parentId = -1; + } + + if (!m_collections.contains(parentId)) { + return; + } + + // This may be a signal for a collection we've already removed by removing its ancestor. + // Or the collection may have been hidden. + if (!m_collections.contains(collection.id())) { + return; + } + + Q_Q(EntityTreeModel); + + Q_ASSERT(m_childEntities.contains(parentId)); + + const int row = indexOf(m_childEntities.value(parentId), collection.id()); + + Q_ASSERT(row >= 0); + + Q_ASSERT(m_collections.contains(parentId)); + + const Collection parentCollection = m_collections.value(parentId); + + m_populatedCols.remove(collection.id()); + + const QModelIndex parentIndex = indexForCollection(parentCollection); + + q->beginRemoveRows(parentIndex, row, row); + + // Delete all descendant collections and items. + removeChildEntities(collection.id()); + + // Remove deleted collection from its parent. + delete m_childEntities[parentId].takeAt(row); + + // Remove deleted collection itself. + m_collections.remove(collection.id()); + + q->endRemoveRows(); + + // After removing a collection, check whether it's parent should be removed too + if (!shouldBePartOfModel(parentCollection)) { + monitoredCollectionRemoved(parentCollection); + } +} + +void EntityTreeModelPrivate::removeChildEntities(Collection::Id collectionId) +{ + QList childList = m_childEntities.value(collectionId); + QList::const_iterator it = childList.constBegin(); + const QList::const_iterator end = childList.constEnd(); + for (; it != end; ++it) { + if (Node::Item == (*it)->type) { + m_items.remove((*it)->id); + } else { + removeChildEntities((*it)->id); + m_collections.remove((*it)->id); + m_populatedCols.remove((*it)->id); + } + } + + qDeleteAll(m_childEntities.take(collectionId)); +} + +QStringList EntityTreeModelPrivate::childCollectionNames(const Collection &collection) const +{ + QStringList names; + + foreach (Node *node, m_childEntities[collection.id()]) { + if (node->type == Node::Collection) { + names << m_collections.value(node->id).name(); + } + } + + return names; +} + +void EntityTreeModelPrivate::monitoredCollectionMoved(const Akonadi::Collection &collection, + const Akonadi::Collection &sourceCollection, + const Akonadi::Collection &destCollection) +{ + if (isHidden(collection)) { + return; + } + + if (isHidden(sourceCollection)) { + if (isHidden(destCollection)) { + return; + } + + monitoredCollectionAdded(collection, destCollection); + return; + } else if (isHidden(destCollection)) { + monitoredCollectionRemoved(collection); + return; + } + + if (!m_collections.contains(collection.id())) { + return; + } + + if (m_monitor->collectionsMonitored().contains(collection)) { + //if we don't reset here, we would have to make sure that destination collection is actually available, + //and remove the sources parents if they were only included as parents of the moved collection + beginResetModel(); + endResetModel(); + return; + } + Q_Q(EntityTreeModel); + + const QModelIndex srcParentIndex = indexForCollection(sourceCollection); + const QModelIndex destParentIndex = indexForCollection(destCollection); + + Q_ASSERT(collection.parentCollection().isValid()); + Q_ASSERT(destCollection.isValid()); + Q_ASSERT(collection.parentCollection() == destCollection); + + const int srcRow = indexOf(m_childEntities.value(sourceCollection.id()), collection.id()); + const int destRow = 0; // Prepend collections + + if (!q->beginMoveRows(srcParentIndex, srcRow, srcRow, destParentIndex, destRow)) { + qCWarning(AKONADICORE_LOG) << "Invalid move"; + return; + } + + Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow); + // collection has the correct parentCollection etc. We need to set it on the + // internal data structure to not corrupt things. + m_collections.insert(collection.id(), collection); + node->parent = destCollection.id(); + m_childEntities[destCollection.id()].prepend(node); + q->endMoveRows(); +} + +void EntityTreeModelPrivate::monitoredCollectionChanged(const Akonadi::Collection &collection) +{ + if (!m_collections.contains(collection.id())) { + // This can happen if + // * we get a change notification after removing the collection. + // * a collection of a non-monitored mimetype is changed elsewhere. Monitor does not + // filter by content mimetype of Collections so we get notifications for all of them. + + //We might match the filter now, retry adding the collection + monitoredCollectionAdded(collection, collection.parentCollection()); + return; + } + + if (!shouldBePartOfModel(collection)) { + monitoredCollectionRemoved(collection); + return; + } + + m_collections[collection.id()] = collection; + + if (!m_showRootCollection && + collection == m_rootCollection) { + // If the root of the model is not Collection::root it might be modified. + // But it doesn't exist in the accessible model structure, so we need to early return + return; + } + + const QModelIndex index = indexForCollection(collection); + Q_ASSERT(index.isValid()); + dataChanged(index, index); +} + +void EntityTreeModelPrivate::monitoredCollectionStatisticsChanged(Akonadi::Collection::Id id, + const Akonadi::CollectionStatistics &statistics) +{ + if (!m_collections.contains(id)) { + return; + } + + m_collections[id].setStatistics(statistics); + + // if the item count becomes 0, add to set of collections we know to be empty + // otherwise remove if in there + if (statistics.count() == 0) { + m_collectionsWithoutItems.insert(id); + } else { + m_collectionsWithoutItems.remove(id); + } + + if (!m_showRootCollection && + id == m_rootCollection.id()) { + // If the root of the model is not Collection::root it might be modified. + // But it doesn't exist in the accessible model structure, so we need to early return + return; + } + + const QModelIndex index = indexForCollection(m_collections[id]); + dataChanged(index, index); +} + +void EntityTreeModelPrivate::monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_Q(EntityTreeModel); + + if (isHidden(item)) { + return; + } + + if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch && + !m_collections.contains(collection.id())) { + qCWarning(AKONADICORE_LOG) << "Got a stale notification for an item whose collection was already removed." << item.id() << item.remoteId(); + return; + } + + if (m_items.contains(item.id())) { + return; + } + + Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collection.id()) : true); + + if (!m_mimeChecker.wantedMimeTypes().isEmpty() && + !m_mimeChecker.isWantedItem(item)) { + return; + } + + //Adding items to not yet populated collections would block fetchMore, resulting in only new items showing up in the collection + //This is only a problem with lazy population, otherwise fetchMore is not used at all + if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(collection.id())) { + return; + } + + int row; + QModelIndex parentIndex; + if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) { + row = m_childEntities.value(collection.id()).size(); + parentIndex = indexForCollection(m_collections.value(collection.id())); + } else { + row = q->rowCount(); + } + 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; + if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) { + m_childEntities[collection.id()].append(node); + } else { + m_childEntities[m_rootCollection.id()].append(node); + } + q->endInsertRows(); +} + +void EntityTreeModelPrivate::monitoredItemRemoved(const Akonadi::Item &item) +{ + Q_Q(EntityTreeModel); + + if (isHidden(item)) { + return; + } + + const Collection::List parents = getParentCollections(item); + if (parents.isEmpty()) { + return; + } + + if (!m_items.contains(item.id())) { + qCWarning(AKONADICORE_LOG) << "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())); + Q_ASSERT(m_childEntities.contains(collection.id())); + + const int row = indexOf(m_childEntities.value(collection.id()), item.id()); + Q_ASSERT(row >= 0); + + const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id())); + + q->beginRemoveRows(parentIndex, row, row); + m_items.remove(item.id()); + delete m_childEntities[collection.id()].takeAt(row); + q->endRemoveRows(); +} + +void EntityTreeModelPrivate::monitoredItemChanged(const Akonadi::Item &item, const QSet &) +{ + if (isHidden(item)) { + return; + } + + if (!m_items.contains(item.id())) { + qCWarning(AKONADICORE_LOG) << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + + // Notifications about itemChange are always dispatched for real collection + // and also all virtual collections the item belongs to. In order to preserve + // the original storage collection when we need to have special handling for + // notifications for virtual collections + if (item.parentCollection().isVirtual()) { + const Collection originalParent = m_items[item.id()].parentCollection(); + m_items[item.id()].apply(item); + m_items[item.id()].setParentCollection(originalParent); + } else { + m_items[item.id()].apply(item); + } + + const QModelIndexList indexes = indexesForItem(item); + foreach (const QModelIndex &index, indexes) { + if (!index.isValid()) { + qCWarning(AKONADICORE_LOG) << "item has invalid index:" << item.id() << item.remoteId(); + } else { + dataChanged(index, index); + } + } +} + +void EntityTreeModelPrivate::monitoredItemMoved(const Akonadi::Item &item, + const Akonadi::Collection &sourceCollection, + const Akonadi::Collection &destCollection) +{ + + if (isHidden(item)) { + return; + } + + if (isHidden(sourceCollection)) { + if (isHidden(destCollection)) { + return; + } + + monitoredItemAdded(item, destCollection); + return; + } else if (isHidden(destCollection)) { + monitoredItemRemoved(item); + return; + } else { + monitoredItemRemoved(item); + monitoredItemAdded(item, destCollection); + return; + } + // "Temporarily" commented out as it's likely the best course to + // avoid the dreaded "reset storm" (or layoutChanged storm). The + // whole itemMoved idea is great but not practical until all the + // other proxy models play nicely with it, right now they just + // transform moved signals in layout changed, which explodes into + // a reset of the source model inside of the message list (ouch!) +#if 0 + if (!m_items.contains(item.id())) { + qCWarning(AKONADICORE_LOG) << "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 QModelIndex srcIndex = indexForCollection(sourceCollection); + const QModelIndex destIndex = indexForCollection(destCollection); + + // Where should it go? Always append items and prepend collections and reorganize them with separate reactions to Attributes? + + const Item::Id itemId = item.id(); + + const int srcRow = indexOf(m_childEntities.value(sourceCollection.id()), itemId); + const int destRow = q->rowCount(destIndex); + + Q_ASSERT(srcRow >= 0); + Q_ASSERT(destRow >= 0); + if (!q->beginMoveRows(srcIndex, srcRow, srcRow, destIndex, destRow)) { + qCWarning(AKONADICORE_LOG) << "Invalid move"; + return; + } + + Q_ASSERT(m_childEntities.contains(sourceCollection.id())); + Q_ASSERT(m_childEntities[sourceCollection.id()].size() > srcRow); + + Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow); + m_items.insert(item.id(), item); + node->parent = destCollection.id(); + m_childEntities[destCollection.id()].append(node); + q->endMoveRows(); +#endif +} + +void EntityTreeModelPrivate::monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_Q(EntityTreeModel); + + if (isHidden(item)) { + return; + } + + const Collection::Id collectionId = collection.id(); + const Item::Id itemId = item.id(); + + Q_ASSERT(m_collections.contains(collectionId)); + + if (!m_mimeChecker.wantedMimeTypes().isEmpty() && + !m_mimeChecker.isWantedItem(item)) { + return; + } + + QList &collectionEntities = m_childEntities[collectionId]; + + int existingPosition = indexOf(collectionEntities, itemId); + + if (existingPosition > 0) { + qCWarning(AKONADICORE_LOG) << "Item with id " << itemId << " already in virtual collection with id " << collectionId; + return; + } + + const int row = collectionEntities.size(); + + const QModelIndex parentIndex = indexForCollection(m_collections.value(collectionId)); + + q->beginInsertRows(parentIndex, row, row); + if (!m_items.contains(itemId)) { + m_items.insert(itemId, item); + } + Node *node = new Node; + node->id = itemId; + node->parent = collectionId; + node->type = Node::Item; + collectionEntities.append(node); + q->endInsertRows(); +} + +void EntityTreeModelPrivate::monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_Q(EntityTreeModel); + + if (isHidden(item)) { + return; + } + + if (!m_items.contains(item.id())) { + qCWarning(AKONADICORE_LOG) << "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()); + if (row < 0 || row >= m_childEntities[ collection.id() ].size()) { + qCWarning(AKONADICORE_LOG) << "couldn't find index of unlinked item " << item.id() << collection.id() << row; + Q_ASSERT(false); + return; + } + + const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id())); + + q->beginRemoveRows(parentIndex, row, row); + delete m_childEntities[collection.id()].takeAt(row); + q->endRemoveRows(); +} + +void EntityTreeModelPrivate::collectionFetchJobDone(KJob *job) +{ + m_pendingCollectionFetchJobs.remove(job); + CollectionFetchJob *cJob = static_cast(job); + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << cJob->collections() << endl; + return; + } + + if (!m_collectionTreeFetched && m_pendingCollectionFetchJobs.isEmpty()) { + m_collectionTreeFetched = true; + emit q_ptr->collectionTreeFetched(Akonadi::valuesToVector(m_collections)); + } + + qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec"; + qCDebug(DebugETM) << "was collection fetch job: collections:" << cJob->collections().size(); + if (!cJob->collections().isEmpty()) { + qCDebug(DebugETM) << "first fetched collection:" << cJob->collections().at(0).name(); + } +} + +void EntityTreeModelPrivate::itemFetchJobDone(KJob *job) +{ + const Collection::Id collectionId = job->property(FetchCollectionId().constData()).value(); + m_pendingCollectionRetrieveJobs.remove(collectionId); + + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << collectionId << endl; + return; + } + if (!m_collections.contains(collectionId)) { + qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items"; + return; + } + ItemFetchJob *iJob = static_cast(job); + qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec"; + qCDebug(DebugETM) << "was item fetch job: items:" << iJob->count(); + + if (!iJob->count()) { + m_collectionsWithoutItems.insert(collectionId); + } else { + m_collectionsWithoutItems.remove(collectionId); + } + + m_populatedCols.insert(collectionId); + emit q_ptr->collectionPopulated(collectionId); + + // If collections are not in the model, there will be no valid index for them. + if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && + (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections) && + !(!m_showRootCollection && collectionId == m_rootCollection.id())) { + const QModelIndex index = indexForCollection(Collection(collectionId)); + Q_ASSERT(index.isValid()); + //To notify about the changed fetch and population state + emit dataChanged(index, index); + } +} + +void EntityTreeModelPrivate::pasteJobDone(KJob *job) +{ + if (job->error()) { + QString errorMsg; + if (qobject_cast(job)) { + errorMsg = i18n("Could not copy item:"); + } else if (qobject_cast(job)) { + errorMsg = i18n("Could not copy collection:"); + } else if (qobject_cast(job)) { + errorMsg = i18n("Could not move item:"); + } else if (qobject_cast(job)) { + errorMsg = i18n("Could not move collection:"); + } else if (qobject_cast(job)) { + errorMsg = i18n("Could not link entity:"); + } + + errorMsg += QLatin1Char(' ') + job->errorString(); + + QMessageBox::critical(0, i18n("Error"), errorMsg); + } +} + +void EntityTreeModelPrivate::updateJobDone(KJob *job) +{ + if (job->error()) { + // TODO: handle job errors + qCWarning(AKONADICORE_LOG) << "Job error:" << job->errorString(); + } else { + + //FIXME: This seems pretty pointless since we'll get an update through the monitor anyways + ItemModifyJob *modifyJob = qobject_cast(job); + if (!modifyJob) { + return; + } + + const Item item = modifyJob->item(); + + Q_ASSERT(item.isValid()); + + m_items[item.id()].apply(item); + const QModelIndexList list = indexesForItem(item); + + foreach (const QModelIndex &index, list) { + dataChanged(index, index); + } + } +} + +void EntityTreeModelPrivate::rootFetchJobDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; + } + CollectionFetchJob *collectionJob = qobject_cast(job); + const Collection::List list = collectionJob->collections(); + + Q_ASSERT(list.size() == 1); + m_rootCollection = list.first(); + startFirstListJob(); +} + +void EntityTreeModelPrivate::startFirstListJob() +{ + Q_Q(EntityTreeModel); + + if (m_collections.size() > 0) { + return; + } + + // Even if the root collection is the invalid collection, we still need to start + // the first list job with Collection::root. + if (m_showRootCollection) { + // Notify the outside that we're putting collection::root into the model. + q->beginInsertRows(QModelIndex(), 0, 0); + m_collections.insert(m_rootCollection.id(), m_rootCollection); + delete m_rootNode; + m_rootNode = new Node; + m_rootNode->id = m_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. + delete m_rootNode; + m_rootNode = new Node; + m_rootNode->id = m_rootCollection.id(); + m_rootNode->parent = -1; + m_rootNode->type = Node::Collection; + m_collections.insert(m_rootCollection.id(), m_rootCollection); + } + + const bool noMimetypes = m_mimeChecker.wantedMimeTypes().isEmpty(); + const bool noResources = m_monitor->resourcesMonitored().isEmpty(); + const bool multipleCollections = m_monitor->collectionsMonitored().size() > 1; + const bool generalPopulation = !noMimetypes || (noMimetypes && noResources); + + const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy); + + //Collections can only be monitored if no resources and no mimetypes are monitored + if (multipleCollections && noMimetypes && noResources) { + fetchCollections(m_monitor->collectionsMonitored(), CollectionFetchJob::Base); + fetchCollections(m_monitor->collectionsMonitored(), fetchType); + return; + } + + qCDebug(DebugETM) << "GEN" << generalPopulation << noMimetypes << noResources; + if (generalPopulation) { + fetchCollections(m_rootCollection, fetchType); + } + + // If the root collection is not collection::root, then it could have items, and they will need to be + // retrieved now. + // Only fetch items NOT if there is NoItemPopulation, or if there is Lazypopulation and the root is visible + // (if the root is not visible the lazy population can not be triggered) + if ((m_itemPopulation != EntityTreeModel::NoItemPopulation) && + !((m_itemPopulation == EntityTreeModel::LazyPopulation) && + m_showRootCollection)) { + if (m_rootCollection != Collection::root()) { + fetchItems(m_rootCollection); + } + } + + // Resources which are explicitly monitored won't have appeared yet if their mimetype didn't match. + // We fetch the top level collections and examine them for whether to add them. + // This fetches virtual collections into the tree. + if (!m_monitor->resourcesMonitored().isEmpty()) { + fetchTopLevelCollections(); + } +} + +void EntityTreeModelPrivate::fetchTopLevelCollections() const +{ + Q_Q(const EntityTreeModel); + CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, m_session); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + q, SLOT(topLevelCollectionsFetched(Akonadi::Collection::List))); + q->connect(job, SIGNAL(result(KJob*)), + q, SLOT(collectionFetchJobDone(KJob*))); + qCDebug(DebugETM) << ""; jobTimeTracker[job].start(); +} + +void EntityTreeModelPrivate::topLevelCollectionsFetched(const Akonadi::Collection::List &list) +{ + Q_Q(EntityTreeModel); + foreach (const Collection &collection, list) { + // These collections have been explicitly shown in the Monitor, + // but hidden trumps that for now. This may change in the future if we figure out a use for it. + if (isHidden(collection)) { + continue; + } + + if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && + !m_collections.contains(collection.id())) { + const QModelIndex parentIndex = indexForCollection(collection.parentCollection()); + // Prepending new collections. + const int row = 0; + q->beginInsertRows(parentIndex, row, row); + + m_collections.insert(collection.id(), collection); + Node *node = new Node; + node->id = collection.id(); + Q_ASSERT(collection.parentCollection() == Collection::root()); + node->parent = collection.parentCollection().id(); + node->type = Node::Collection; + m_childEntities[collection.parentCollection().id()].prepend(node); + + q->endInsertRows(); + + if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { + fetchItems(collection); + } + + Q_ASSERT(collection.isValid()); + fetchCollections(collection, CollectionFetchJob::Recursive); + } + } +} + +Akonadi::Collection::List EntityTreeModelPrivate::getParentCollections(const Item &item) const +{ + Collection::List list; + QHashIterator > iter(m_childEntities); + while (iter.hasNext()) { + iter.next(); + int nodeIndex = indexOf(iter.value(), item.id()); + if (nodeIndex != -1 && + iter.value().at(nodeIndex)->type == Node::Item) { + list << m_collections.value(iter.key()); + } + } + + return list; +} + +void EntityTreeModelPrivate::ref(Collection::Id id) +{ + m_monitor->d_ptr->ref(id); +} + +bool EntityTreeModelPrivate::shouldPurge(Collection::Id id) +{ + // reference counted collections should never be purged + // they first have to be deref'ed until they reach 0. + // if the collection is buffered, keep it. + if (m_monitor->d_ptr->isMonitored(id)) { + return false; + } + + // otherwise we can safely purge this item + return true; +} + +bool EntityTreeModelPrivate::isMonitored(Collection::Id id) +{ + return m_monitor->d_ptr->isMonitored(id); +} + +bool EntityTreeModelPrivate::isBuffered(Collection::Id id) +{ + return m_monitor->d_ptr->m_buffer.isBuffered(id); +} + +void EntityTreeModelPrivate::deref(Collection::Id id) +{ + const Collection::Id bumpedId = m_monitor->d_ptr->deref(id); + + if (bumpedId < 0) { + return; + } + + //The collection has already been removed, don't purge + if (!m_collections.contains(bumpedId)) { + return; + } + + if (shouldPurge(bumpedId)) { + purgeItems(bumpedId); + } +} + +QList::iterator EntityTreeModelPrivate::skipCollections(QList::iterator it, QList::iterator end, int *pos) +{ + for (; it != end; ++it) { + if ((*it)->type == Node::Item) { + break; + } + + ++(*pos); + } + + return it; +} + +QList::iterator EntityTreeModelPrivate::removeItems(QList::iterator it, QList::iterator end, int *pos, const Collection &collection) +{ + Q_Q(EntityTreeModel); + + QList::iterator startIt = it; + + // figure out how many items we will delete + int start = *pos; + for (; it != end; ++it) { + if ((*it)->type != Node::Item) { + break; + } + + ++(*pos); + } + it = startIt; + + const QModelIndex parentIndex = indexForCollection(collection); + + q->beginRemoveRows(parentIndex, start, (*pos) - 1); + const int toDelete = (*pos) - start; + Q_ASSERT(toDelete > 0); + + QList &es = m_childEntities[collection.id()]; + //NOTE: .erase will invalidate all iterators besides "it"! + for (int i = 0; i < toDelete; ++i) { + Q_ASSERT(es.count(*it) == 1); + // don't keep implicitly shared data alive + Q_ASSERT(m_items.contains((*it)->id)); + m_items.remove((*it)->id); + // delete actual node + delete *it; + it = es.erase(it); + } + q->endRemoveRows(); + + return it; +} + +void EntityTreeModelPrivate::purgeItems(Collection::Id id) +{ + QList &childEntities = m_childEntities[id]; + + const Collection collection = m_collections.value(id); + Q_ASSERT(collection.isValid()); + + QList::iterator begin = childEntities.begin(); + QList::iterator end = childEntities.end(); + + int pos = 0; + while ((begin = skipCollections(begin, end, &pos)) != end) { + begin = removeItems(begin, end, &pos, collection); + end = childEntities.end(); + } + m_populatedCols.remove(id); + //if an empty collection is purged and we leave it in here, itemAdded will be ignored for the collection + //and the collection is never populated by fetchMore (but maybe by statistics changed?) + m_collectionsWithoutItems.remove(id); +} + +void EntityTreeModelPrivate::dataChanged(const QModelIndex &top, const QModelIndex &bottom) +{ + Q_Q(EntityTreeModel); + + QModelIndex rightIndex; + + Node *node = static_cast(bottom.internalPointer()); + + if (!node) { + return; + } + + if (node->type == Node::Collection) { + rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::CollectionTreeHeaders) - 1); + } + if (node->type == Node::Item) { + rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::ItemListHeaders) - 1); + } + + emit q->dataChanged(top, rightIndex); +} + +QModelIndex EntityTreeModelPrivate::indexForCollection(const Collection &collection) const +{ + Q_Q(const EntityTreeModel); + + if (!collection.isValid()) { + return QModelIndex(); + } + + if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) { + return QModelIndex(); + } + + // 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. + Collection::Id parentId = -1; + + if ((collection == m_rootCollection)) { + if (m_showRootCollection) { + return q->createIndex(0, 0, static_cast(m_rootNode)); + } + return QModelIndex(); + } + + if (collection == Collection::root()) { + parentId = -1; + } else if (collection.parentCollection().isValid()) { + parentId = collection.parentCollection().id(); + } else { + QHash >::const_iterator it = m_childEntities.constBegin(); + const QHash >::const_iterator end = m_childEntities.constEnd(); + for (; it != end; ++it) { + const int row = indexOf(it.value(), collection.id()); + if (row < 0) { + continue; + } + + Node *node = it.value().at(row); + return q->createIndex(row, 0, static_cast(node)); + } + return QModelIndex(); + } + + const int row = indexOf(m_childEntities.value(parentId), collection.id()); + + if (row < 0) { + return QModelIndex(); + } + + Node *node = m_childEntities.value(parentId).at(row); + + return q->createIndex(row, 0, static_cast(node)); +} + +QModelIndexList EntityTreeModelPrivate::indexesForItem(const Item &item) const +{ + Q_Q(const EntityTreeModel); + QModelIndexList indexes; + + if (m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections) { + Q_ASSERT(m_childEntities.contains(m_rootCollection.id())); + QList nodeList = m_childEntities.value(m_rootCollection.id()); + const int row = indexOf(nodeList, item.id()); + Q_ASSERT(row >= 0); + Q_ASSERT(row < nodeList.size()); + Node *node = nodeList.at(row); + + indexes << q->createIndex(row, 0, static_cast(node)); + return indexes; + } + + const Collection::List collections = getParentCollections(item); + + indexes.reserve(collections.size()); + foreach (const Collection &collection, collections) { + const int row = indexOf(m_childEntities.value(collection.id()), item.id()); + Q_ASSERT(row >= 0); + Q_ASSERT(m_childEntities.contains(collection.id())); + QList nodeList = m_childEntities.value(collection.id()); + Q_ASSERT(row < nodeList.size()); + Node *node = nodeList.at(row); + + indexes << q->createIndex(row, 0, static_cast(node)); + } + + return indexes; +} + +void EntityTreeModelPrivate::beginResetModel() +{ + Q_Q(EntityTreeModel); + q->beginResetModel(); +} + +void EntityTreeModelPrivate::endResetModel() +{ + Q_Q(EntityTreeModel); + foreach (Akonadi::Job *job, m_session->findChildren()) { + job->disconnect(q); + } + m_collections.clear(); + m_collectionsWithoutItems.clear(); + m_populatedCols.clear(); + m_items.clear(); + m_pendingCollectionFetchJobs.clear(); + m_pendingCollectionRetrieveJobs.clear(); + m_collectionTreeFetched = false; + + foreach (const QList &list, m_childEntities) { + qDeleteAll(list); + } + m_childEntities.clear(); + m_rootNode = 0; + + q->endResetModel(); + fillModel(); +} + +void EntityTreeModelPrivate::monitoredItemsRetrieved(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; + } + + Q_Q(EntityTreeModel); + + ItemFetchJob *fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + Item::List list = fetchJob->items(); + + q->beginResetModel(); + foreach (const Item &item, list) { + Node *node = new Node; + node->id = item.id(); + node->parent = m_rootCollection.id(); + node->type = Node::Item; + + m_childEntities[-1].append(node); + m_items.insert(item.id(), item); + } + + q->endResetModel(); +} + +void EntityTreeModelPrivate::fillModel() +{ + Q_Q(EntityTreeModel); + + m_mimeChecker.setWantedMimeTypes(m_monitor->mimeTypesMonitored()); + + const Collection::List collections = m_monitor->collectionsMonitored(); + + if (collections.isEmpty() && + m_monitor->numMimeTypesMonitored() == 0 && + m_monitor->numResourcesMonitored() == 0 && + m_monitor->numItemsMonitored() != 0) { + m_rootCollection = Collection(-1); + m_collectionTreeFetched = true; + emit q_ptr->collectionTreeFetched(collections); // there are no collections to fetch + + Item::List items; + items.reserve(m_monitor->itemsMonitoredEx().size()); + Q_FOREACH (Item::Id id, m_monitor->itemsMonitoredEx()) { + items.append(Item(id)); + } + ItemFetchJob *itemFetch = new ItemFetchJob(items, m_session); + itemFetch->setFetchScope(m_monitor->itemFetchScope()); + itemFetch->fetchScope().setIgnoreRetrievalErrors(true); + q->connect(itemFetch, SIGNAL(finished(KJob*)), q, SLOT(monitoredItemsRetrieved(KJob*))); + return; + } + // In case there is only a single collection monitored, we can use this + // collection as root of the node tree, in all other cases + // Collection::root() is used + if (collections.size() == 1) { + m_rootCollection = collections.first(); + } else { + m_rootCollection = Collection::root(); + } + + if (m_rootCollection == Collection::root()) { + QTimer::singleShot(0, q, SLOT(startFirstListJob())); + } else { + Q_ASSERT(m_rootCollection.isValid()); + CollectionFetchJob *rootFetchJob = new CollectionFetchJob(m_rootCollection, CollectionFetchJob::Base, m_session); + q->connect(rootFetchJob, SIGNAL(result(KJob*)), + SLOT(rootFetchJobDone(KJob*))); + qCDebug(DebugETM) << ""; jobTimeTracker[rootFetchJob].start(); + } +} + +bool EntityTreeModelPrivate::canFetchMore(const QModelIndex &parent) const +{ + const Item item = parent.data(EntityTreeModel::ItemRole).value(); + + if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) { + return false; + } + + 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(EntityTreeModel::CollectionIdRole).toULongLong(); + + // But the root collection can't... + if (Collection::root().id() == colId) { + return false; + } + + // Collections which contain no items at all can't contain more + if (m_collectionsWithoutItems.contains(colId)) { + return false; + } + + // Don't start the same job multiple times. + if (m_pendingCollectionRetrieveJobs.contains(colId)) { + return false; + } + + // Can't fetch more if the collection's items have already been fetched + if (m_populatedCols.contains(colId)) { + return false; + } + + foreach (Node *node, 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. + return false; + } + } + + return true; + } +} + +QIcon EntityTreeModelPrivate::iconForName(const QString &name) const +{ + if (m_iconThemeName != QIcon::themeName()) { + m_iconThemeName = QIcon::themeName(); + m_iconCache.clear(); + } + + QIcon &icon = m_iconCache[name]; + if (icon.isNull()) { + icon = QIcon::fromTheme(name); + } + return icon; +} diff --git a/src/core/models/entitytreemodel_p.h b/src/core/models/entitytreemodel_p.h new file mode 100644 index 0000000..8a66097 --- /dev/null +++ b/src/core/models/entitytreemodel_p.h @@ -0,0 +1,307 @@ +/* + 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 "item.h" +#include "collectionfetchjob.h" +#include "itemfetchscope.h" +#include "mimetypechecker.h" + +#include "entitytreemodel.h" + +#include "akonaditests_export.h" + +#include + +Q_DECLARE_LOGGING_CATEGORY(DebugETM) + +namespace Akonadi +{ +class ItemFetchJob; +class ChangeRecorder; +class AgentInstance; +} + +struct Node { + typedef qint64 Id; + + Id id; + Akonadi::Collection::Id parent; + + enum Type { + Item, + Collection + }; + + int type; +}; + +namespace Akonadi +{ +/** + * @internal + */ +class AKONADI_TESTS_EXPORT EntityTreeModelPrivate +{ +public: + + explicit EntityTreeModelPrivate(EntityTreeModel *parent); + ~EntityTreeModelPrivate(); + EntityTreeModel *const q_ptr; + + enum RetrieveDepth { + Base, + Recursive + }; + + void init(ChangeRecorder *monitor); + + void fetchCollections(const Collection &collection, CollectionFetchJob::Type type = CollectionFetchJob::FirstLevel); + void fetchCollections(const Collection::List &collections, CollectionFetchJob::Type type = CollectionFetchJob::FirstLevel); + void fetchCollections(Akonadi::CollectionFetchJob *job); + void fetchItems(const Collection &collection); + void collectionsFetched(const Akonadi::Collection::List &collections); + void collectionListFetched(const Akonadi::Collection::List &collections); + void itemsFetched(const Akonadi::Item::List &items); + void itemsFetched(const Collection::Id collectionId, const Akonadi::Item::List &items); + + void monitoredCollectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + void monitoredCollectionRemoved(const Akonadi::Collection &collection); + void monitoredCollectionChanged(const Akonadi::Collection &collection); + void monitoredCollectionStatisticsChanged(Akonadi::Collection::Id, const Akonadi::CollectionStatistics &statistics); + void monitoredCollectionMoved(const Akonadi::Collection &collection, + const Akonadi::Collection &sourceCollection, + const Akonadi::Collection &destCollection); + + void monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); + void monitoredItemRemoved(const Akonadi::Item &item); + void monitoredItemChanged(const Akonadi::Item &item, const QSet &); + void monitoredItemMoved(const Akonadi::Item &item, const Akonadi::Collection &, const Akonadi::Collection &); + + void monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &); + void monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &); + + void monitoredMimeTypeChanged(const QString &mimeType, bool monitored); + void monitoredCollectionsChanged(const Akonadi::Collection &collection, bool monitored); + void monitoredItemsChanged(const Akonadi::Item &item, bool monitored); + void monitoredResourcesChanged(const QByteArray &resource, bool monitored); + + Collection::List getParentCollections(const Item &item) const; + void removeChildEntities(Collection::Id collectionId); + + /** + * Returns the list of names of the child collections of @p collection. + */ + QStringList childCollectionNames(const Collection &collection) const; + + /** + * Fetch parent collections and insert this @p collection and its parents into the node tree + */ + void retrieveAncestors(const Akonadi::Collection &collection, bool insertBaseCollection = true); + void ancestorsFetched(const Akonadi::Collection::List &collectionList); + void insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + + void beginResetModel(); + void endResetModel(); + /** + * Start function for filling the Model, finds and fetches the root of the node tree + * Next relevant function for filling the model is startFirstListJob() + */ + void fillModel(); + + void changeFetchState(const Collection &parent); + void agentInstanceRemoved(const Akonadi::AgentInstance &instace); + + QIcon iconForName(const QString &name) const; + + QHash m_collections; + QHash m_items; + QHash > m_childEntities; + QSet m_populatedCols; + QSet m_collectionsWithoutItems; + + QVector m_pendingCutItems; + QVector m_pendingCutCollections; + mutable QSet m_pendingCollectionRetrieveJobs; + mutable QSet m_pendingCollectionFetchJobs; + + // Icon cache to workaround QIcon::fromTheme being very slow (bug #346644) + mutable QHash m_iconCache; + mutable QString m_iconThemeName; + + ChangeRecorder *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; + CollectionFetchScope::ListFilter m_listFilter; + bool m_includeStatistics; + bool m_showRootCollection; + bool m_collectionTreeFetched; + + /** + * Called after the root collection was fetched by fillModel + * + * Initiates further fetching of collections depending on the monitored collections + * (in the monitor) and the m_collectionFetchStrategy. + * + * Further collections are either fetched directly with fetchCollections and + * fetchItems or, in case that collections or resources are monitored explicitly + * via fetchTopLevelCollections + */ + void startFirstListJob(); + + void serverStarted(); + + void monitoredItemsRetrieved(KJob *job); + void rootFetchJobDone(KJob *job); + void collectionFetchJobDone(KJob *job); + void itemFetchJobDone(KJob *job); + void updateJobDone(KJob *job); + void pasteJobDone(KJob *job); + + /** + * Returns the index of the node in @p list with the id @p id. Returns -1 if not found. + */ + template + int indexOf(const QList &nodes, Node::Id id) const + { + int i = 0; + Q_FOREACH (const Node *node, nodes) { + if (node->id == id && node->type == Type) { + return i; + } + i++; + } + + return -1; + } + + /** + * 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 FetchCollectionId() + { + return "FetchCollectionId"; + } + + Session *m_session; + + Q_DECLARE_PUBLIC(EntityTreeModel) + + void fetchTopLevelCollections() const; + void topLevelCollectionsFetched(const Akonadi::Collection::List &collectionList); + + /** + @returns True if @p item or one of its descemdants is hidden. + */ + bool isHidden(const Item &item) const; + bool isHidden(const Collection &collection) const; + + template + bool isHiddenImpl(const T &entity, Node::Type type) const; + + bool m_showSystemEntities; + + void ref(Collection::Id id); + void deref(Collection::Id id); + + /** + * @returns true if the collection is actively monitored (referenced or buffered with refcounting enabled) + * + * purely for testing + */ + bool isMonitored(Collection::Id id); + + /** + * @returns true if the collection is buffered + * + * purely for testing + */ + bool isBuffered(Collection::Id id); + + /** + @returns true if the Collection with the id of @p id should be purged. + */ + bool shouldPurge(Collection::Id id); + + /** + Purges the items in the Collection @p id + */ + void purgeItems(Collection::Id id); + + /** + Removes the items starting from @p it and up to a maximum of @p end in Collection @p col. @p pos should be the index of @p it + in the m_childEntities before calling, and is updated to the position of the next Collection in m_childEntities afterward. + This is required to emit model remove signals properly. + + @returns an iterator pointing to the next Collection after @p it, or at @p end + */ + QList::iterator removeItems(QList::iterator it, QList::iterator end, + int *pos, const Collection &col); + + /** + Skips over Collections in m_childEntities up to a maximum of @p end. @p it is an iterator pointing to the first Collection + in a block of Collections, and @p pos initially describes the index of @p it in m_childEntities and is updated to point to + the index of the next Item in the list. + + @returns an iterator pointing to the next Item after @p it, or at @p end + */ + QList::iterator skipCollections(QList::iterator it, QList::iterator end, int *pos); + + /** + Emits the data changed signal for the entire row as in the subclass, instead of just for the first column. + */ + void dataChanged(const QModelIndex &top, const QModelIndex &bottom); + + /** + * 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; + + bool canFetchMore(const QModelIndex &parent) const; + + /** + * Returns true if the collection matches all filters and should be part of the model. + * This method checks all properties that could change by modifying the collection. + * Currently that includes: + * * hidden attribute + * * content mime types + */ + bool shouldBePartOfModel(const Collection &collection) const; + bool hasChildCollection(const Collection &collection) const; + bool isAncestorMonitored(const Collection &collection) const; +}; + +} + +#endif diff --git a/src/core/models/favoritecollectionsmodel.cpp b/src/core/models/favoritecollectionsmodel.cpp new file mode 100644 index 0000000..d93e1e8 --- /dev/null +++ b/src/core/models/favoritecollectionsmodel.cpp @@ -0,0 +1,460 @@ +/* + Copyright (c) 2009 Kevin Ottens + + 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 "favoritecollectionsmodel.h" +#include "akonadicore_debug.h" +#include +#include + +#include +#include +#include +#include +#include + +#include "entitytreemodel.h" +#include "mimetypechecker.h" +#include "pastehelper_p.h" + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN FavoriteCollectionsModel::Private +{ +public: + Private(const KConfigGroup &group, FavoriteCollectionsModel *parent) + : q(parent) + , configGroup(group) + { + } + + QString labelForCollection(Collection::Id collectionId) const + { + if (labelMap.contains(collectionId)) { + return labelMap[collectionId]; + } + + const QModelIndex collectionIdx = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); + + QString accountName; + + const QString nameOfCollection = collectionIdx.data().toString(); + + QModelIndex idx = collectionIdx.parent(); + while (idx != QModelIndex()) { + accountName = idx.data(EntityTreeModel::OriginalCollectionNameRole).toString(); + idx = idx.parent(); + } + if (accountName.isEmpty()) { + return nameOfCollection; + } else { + return nameOfCollection + QStringLiteral(" (") + accountName + QLatin1Char(')'); + } + } + + void insertIfAvailable(Collection::Id col) + { + if (collectionIds.contains(col)) { + select(col); + if (!referencedCollections.contains(col)) { + reference(col); + } + } + } + + void insertIfAvailable(const QModelIndex &idx) + { + insertIfAvailable(idx.data(EntityTreeModel::CollectionIdRole).value()); + } + + /** + * Stuff changed (e.g. new rows inserted into sorted model), reload everything. + */ + void reload() + { + //don't clear the selection model here. Otherwise we mess up the users selection as collections get removed and re-inserted. + foreach (const Collection::Id &collectionId, collectionIds) { + insertIfAvailable(collectionId); + } + //TODO remove what's no longer here + } + + void rowsInserted(const QModelIndex &parent, int begin, int end) + { + for (int row = begin; row <= end; row++) { + const QModelIndex child = parent.child(row, 0); + if (!child.isValid()) { + continue; + } + insertIfAvailable(child); + const int childRows = q->sourceModel()->rowCount(child); + if (childRows > 0) { + rowsInserted(child, 0, childRows - 1); + } + } + } + + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) + { + for (int row = topLeft.row(); row <= bottomRight.row(); row++) { + const QModelIndex idx = topLeft.parent().child(row, 0); + insertIfAvailable(idx); + } + } + + /** + * Selects the index in the internal selection model to make the collection visible in the model + */ + void select(const Collection::Id &collectionId) + { + const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); + if (index.isValid()) { + q->selectionModel()->select(index, QItemSelectionModel::Select); + } + } + + void deselect(const Collection::Id &collectionId) + { + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); + if (idx.isValid()) { + q->selectionModel()->select(idx, QItemSelectionModel::Deselect); + } + } + + void reference(const Collection::Id &collectionId) + { + if (referencedCollections.contains(collectionId)) { + qCWarning(AKONADICORE_LOG) << "already referenced " << collectionId; + return; + } + const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); + if (index.isValid()) { + if (q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionRefRole)) { + referencedCollections << collectionId; + } else { + qCWarning(AKONADICORE_LOG) << "failed to reference collection"; + } + q->sourceModel()->fetchMore(index); + } + } + + void dereference(const Collection::Id &collectionId) + { + if (!referencedCollections.contains(collectionId)) { + qCWarning(AKONADICORE_LOG) << "not referenced " << collectionId; + return; + } + const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); + if (index.isValid()) { + q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionDerefRole); + referencedCollections.remove(collectionId); + } + } + + void clearReferences() + { + foreach (const Collection::Id &collectionId, referencedCollections) { + dereference(collectionId); + } + } + + /** + * Adds a collection to the favorite collections + */ + void add(const Collection::Id &collectionId) + { + if (collectionIds.contains(collectionId)) { + qCDebug(AKONADICORE_LOG) << "already in model " << collectionId; + return; + } + collectionIds << collectionId; + reference(collectionId); + select(collectionId); + } + + void remove(const Collection::Id &collectionId) + { + collectionIds.removeAll(collectionId); + labelMap.remove(collectionId); + dereference(collectionId); + deselect(collectionId); + } + + void set(const QList &collections) + { + QList colIds = collectionIds; + foreach (const Collection::Id &col, collections) { + const int removed = colIds.removeAll(col); + const bool isNewCollection = removed <= 0; + if (isNewCollection) { + add(col); + } + } + //Remove what's left + foreach (Akonadi::Collection::Id colId, colIds) { + remove(colId); + } + } + + void set(const Akonadi::Collection::List &collections) + { + QList colIds; + colIds.reserve(collections.count()); + foreach (const Akonadi::Collection &col, collections) { + colIds << col.id(); + } + set(colIds); + } + + void loadConfig() + { + const QList collections = configGroup.readEntry("FavoriteCollectionIds", QList()); + const QStringList labels = configGroup.readEntry("FavoriteCollectionLabels", QStringList()); + const int numberOfLabels(labels.size()); + for (int i = 0; i < collections.size(); ++i) { + if (i < numberOfLabels) { + labelMap[collections[i]] = labels[i]; + } + add(collections[i]); + } + } + + void saveConfig() + { + QStringList labels; + labels.reserve(collectionIds.count()); + foreach (const Collection::Id &collectionId, collectionIds) { + labels << labelForCollection(collectionId); + } + + configGroup.writeEntry("FavoriteCollectionIds", collectionIds); + configGroup.writeEntry("FavoriteCollectionLabels", labels); + configGroup.config()->sync(); + } + + FavoriteCollectionsModel *const q; + + QList collectionIds; + QSet referencedCollections; + QHash labelMap; + KConfigGroup configGroup; +}; + +FavoriteCollectionsModel::FavoriteCollectionsModel(QAbstractItemModel *source, const KConfigGroup &group, QObject *parent) + : Akonadi::SelectionProxyModel(new QItemSelectionModel(source, parent), parent) + , d(new Private(group, this)) +{ + //This should only be a KRecursiveFilterProxyModel, but remains a SelectionProxyModel for backwards compatibility. + // We therefore disable what we anyways don't want (the referencing is handled separately). + disconnect(this, SIGNAL(rootIndexAdded(QModelIndex)), this, SLOT(rootIndexAdded(QModelIndex))); + disconnect(this, SIGNAL(rootIndexAboutToBeRemoved(QModelIndex)), this, SLOT(rootIndexAboutToBeRemoved(QModelIndex))); + + setSourceModel(source); + setFilterBehavior(ExactSelection); + + d->loadConfig(); + //React to various changes in the source model + connect(source, SIGNAL(modelReset()), this, SLOT(reload())); + connect(source, SIGNAL(layoutChanged()), this, SLOT(reload())); + connect(source, &QAbstractItemModel::rowsInserted, this, &QAbstractItemModel::rowsInserted); + connect(source, &QAbstractItemModel::dataChanged, this, &QAbstractItemModel::dataChanged); +} + +FavoriteCollectionsModel::~FavoriteCollectionsModel() +{ + delete d; +} + +void FavoriteCollectionsModel::setCollections(const Collection::List &collections) +{ + d->set(collections); + d->saveConfig(); +} + +void FavoriteCollectionsModel::addCollection(const Collection &collection) +{ + d->add(collection.id()); + d->saveConfig(); +} + +void FavoriteCollectionsModel::removeCollection(const Collection &collection) +{ + d->remove(collection.id()); + d->saveConfig(); +} + +Akonadi::Collection::List FavoriteCollectionsModel::collections() const +{ + Collection::List cols; + cols.reserve(d->collectionIds.count()); + foreach (const Collection::Id &colId, d->collectionIds) { + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(colId)); + const Collection collection = sourceModel()->data(idx, EntityTreeModel::CollectionRole).value(); + cols << collection; + } + return cols; +} + +QList FavoriteCollectionsModel::collectionIds() const +{ + return d->collectionIds; +} + +void Akonadi::FavoriteCollectionsModel::setFavoriteLabel(const Collection &collection, const QString &label) +{ + Q_ASSERT(d->collectionIds.contains(collection.id())); + d->labelMap[collection.id()] = label; + d->saveConfig(); + + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), collection); + + if (!idx.isValid()) { + return; + } + + const QModelIndex index = mapFromSource(idx); + emit dataChanged(index, index); +} + +QVariant Akonadi::FavoriteCollectionsModel::data(const QModelIndex &index, int role) const +{ + if (index.column() == 0 && + (role == Qt::DisplayRole || + role == Qt::EditRole)) { + const QModelIndex sourceIndex = mapToSource(index); + const Collection::Id collectionId = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionIdRole).toLongLong(); + + return d->labelForCollection(collectionId); + } else { + return KSelectionProxyModel::data(index, role); + } +} + +bool FavoriteCollectionsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.isValid() && index.column() == 0 && + role == Qt::EditRole) { + const QString newLabel = value.toString(); + if (newLabel.isEmpty()) { + return false; + } + const QModelIndex sourceIndex = mapToSource(index); + const Collection collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value(); + setFavoriteLabel(collection, newLabel); + return true; + } + return Akonadi::SelectionProxyModel::setData(index, value, role); +} + +QString Akonadi::FavoriteCollectionsModel::favoriteLabel(const Akonadi::Collection &collection) +{ + if (!collection.isValid()) { + return QString(); + } + return d->labelForCollection(collection.id()); +} + +QVariant FavoriteCollectionsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (section == 0 && + orientation == Qt::Horizontal && + role == Qt::DisplayRole) { + return i18n("Favorite Folders"); + } else { + return KSelectionProxyModel::headerData(section, orientation, role); + } +} + +bool FavoriteCollectionsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(action); + Q_UNUSED(row); + Q_UNUSED(column); + if (data->hasFormat(QStringLiteral("text/uri-list"))) { + const QList urls = data->urls(); + + const QModelIndex sourceIndex = mapToSource(parent); + const Collection destCollection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value(); + + MimeTypeChecker mimeChecker; + mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes()); + + foreach (const QUrl &url, urls) { + const Collection col = Collection::fromUrl(url); + if (col.isValid()) { + addCollection(col); + } else { + const Item item = Item::fromUrl(url); + if (item.isValid()) { + if (item.parentCollection().id() == destCollection.id() && + action != Qt::CopyAction) { + qCDebug(AKONADICORE_LOG) << "Error: source and destination of move are the same."; + return false; + } +#if 0 + if (!mimeChecker.isWantedItem(item)) { + qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType(); + return false; + } +#endif + KJob *job = PasteHelper::pasteUriList(data, destCollection, action); + if (!job) { + return false; + } + connect(job, &KJob::result, this, &FavoriteCollectionsModel::pasteJobDone); + // Accept the event so that it doesn't propagate. + return true; + + } + } + + } + return true; + } + return false; +} + +QStringList FavoriteCollectionsModel::mimeTypes() const +{ + QStringList mts = Akonadi::SelectionProxyModel::mimeTypes(); + if (!mts.contains(QStringLiteral("text/uri-list"))) { + mts.append(QStringLiteral("text/uri-list")); + } + return mts; +} + +Qt::ItemFlags FavoriteCollectionsModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags fs = Akonadi::SelectionProxyModel::flags(index); + if (!index.isValid()) { + fs |= Qt::ItemIsDropEnabled; + } + return fs; +} + +void FavoriteCollectionsModel::pasteJobDone(KJob *job) +{ + if (job->error()) { + qCDebug(AKONADICORE_LOG) << job->errorString(); + } +} + +#include "moc_favoritecollectionsmodel.cpp" diff --git a/src/core/models/favoritecollectionsmodel.h b/src/core/models/favoritecollectionsmodel.h new file mode 100644 index 0000000..b8f9016 --- /dev/null +++ b/src/core/models/favoritecollectionsmodel.h @@ -0,0 +1,168 @@ +/* + Copyright (c) 2009 Kevin Ottens + + 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_FAVORITECOLLECTIONSMODEL_H +#define AKONADI_FAVORITECOLLECTIONSMODEL_H + +#include "akonadicore_export.h" +#include "selectionproxymodel.h" +#include "collection.h" + +class KConfigGroup; +class KJob; + +namespace Akonadi +{ + +class EntityTreeModel; + +/** + * @short A model that lists a set of favorite collections. + * + * In some applications you want to provide fast access to a list + * of often used collections (e.g. Inboxes from different email accounts + * in a mail application). Therefor you can use the FavoriteCollectionsModel + * which stores the list of favorite collections in a given configuration + * file. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * EntityTreeModel *sourceModel = new EntityTreeModel( ... ); + * + * const KConfigGroup group = KGlobal::config()->group( "Favorite Collections" ); + * + * FavoriteCollectionsModel *model = new FavoriteCollectionsModel( sourceModel, group, this ); + * + * EntityListView *view = new EntityListView( this ); + * view->setModel( model ); + * + * @endcode + * + * @author Kevin Ottens + * @since 4.4 + */ +//TODO_KDE5: Make this a KRecursiveFilterProxyModel instead of a SelectionProxyModel +class AKONADICORE_EXPORT FavoriteCollectionsModel : public Akonadi::SelectionProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new favorite collections model. + * + * @param model The source model where the favorite collections + * come from. + * @param group The config group that shall be used to save the + * selection of favorite collections. + * @param parent The parent object. + */ + FavoriteCollectionsModel(QAbstractItemModel *model, const KConfigGroup &group, QObject *parent = Q_NULLPTR); + + /** + * Destroys the favorite collections model. + */ + virtual ~FavoriteCollectionsModel(); + + /** + * Returns the list of favorite collections. + * @deprecated Use collectionIds instead. + */ + Collection::List collections() const; + + /** + * Returns the list of ids of favorite collections set on the FavoriteCollectionsModel. + * + * Note that if you want Collections with actual data + * you should use something like this instead: + * + * @code + * FavoriteCollectionsModel* favs = getFavsModel(); + * Collection::List cols; + * const int rowCount = favs->rowCount(); + * for (int row = 0; row < rowcount; ++row) { + * static const int column = 0; + * const QModelIndex index = favs->index(row, column); + * const Collection col = index.data(EntityTreeModel::CollectionRole).value(); + * cols << col; + * } + * @endcode + * + * Note that due to the asynchronous nature of the model, this method returns collection ids + * of collections which may not be in the model yet. If you want the ids of the collections + * that are actually in the model, use a loop similar to above with the CollectionIdRole. + */ + QList collectionIds() const; + + /** + * Return associate label for collection + */ + QString favoriteLabel(const Akonadi::Collection &col); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) Q_DECL_OVERRIDE; + QStringList mimeTypes() const Q_DECL_OVERRIDE; + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + +public Q_SLOTS: + /** + * Sets the @p collections as favorite collections. + */ + void setCollections(const Collection::List &collections); + + /** + * Adds a @p collection to the list of favorite collections. + */ + void addCollection(const Collection &collection); + + /** + * Removes a @p collection from the list of favorite collections. + */ + void removeCollection(const Collection &collection); + + /** + * Sets a custom @p label that will be used when showing the + * favorite @p collection. + */ + void setFavoriteLabel(const Collection &collection, const QString &label); + +private Q_SLOTS: + void pasteJobDone(KJob *job); + +private: + //@cond PRIVATE + using KSelectionProxyModel::setSourceModel; + + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void reload()) + Q_PRIVATE_SLOT(d, void rowsInserted(QModelIndex, int, int)) + Q_PRIVATE_SLOT(d, void dataChanged(QModelIndex, QModelIndex)) + //@endcond +}; + +} + +#endif diff --git a/src/core/models/itemmodel.cpp b/src/core/models/itemmodel.cpp new file mode 100644 index 0000000..98393ae --- /dev/null +++ b/src/core/models/itemmodel.cpp @@ -0,0 +1,470 @@ +/* + 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 "itemmodel.h" +#include "akonadicore_debug.h" +#include "itemfetchjob.h" +#include "collectionfetchjob.h" +#include "itemfetchscope.h" +#include "monitor.h" +#include "pastehelper_p.h" +#include "session.h" + +#include +#include + +#include +#include + +using namespace Akonadi; + +/** + * @internal + * + * This struct is used for optimization reasons. + * because it embeds the row. + * + * Semantically, we could have used an item instead. + */ +struct ItemContainer { + ItemContainer(const Item &i, int r) + : item(i) + , row(r) + { + } + Item item; + int row; +}; + +/** + * @internal + */ +class Q_DECL_HIDDEN ItemModel::Private +{ +public: + Private(ItemModel *parent) + : mParent(parent) + , monitor(new Monitor()) + { + session = new Session(QCoreApplication::instance()->applicationName().toUtf8() + + QByteArray("-ItemModel-") + QByteArray::number(qrand()), mParent); + + monitor->ignoreSession(session); + + mParent->connect(monitor, SIGNAL(itemChanged(Akonadi::Item,QSet)), + mParent, SLOT(itemChanged(Akonadi::Item,QSet))); + mParent->connect(monitor, SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)), + mParent, SLOT(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); + mParent->connect(monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)), + mParent, SLOT(itemAdded(Akonadi::Item))); + mParent->connect(monitor, SIGNAL(itemRemoved(Akonadi::Item)), + mParent, SLOT(itemRemoved(Akonadi::Item))); + mParent->connect(monitor, SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection)), + mParent, SLOT(itemAdded(Akonadi::Item))); + mParent->connect(monitor, SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection)), + mParent, SLOT(itemRemoved(Akonadi::Item))); + } + + ~Private() + { + delete monitor; + } + + void listingDone(KJob *job); + void collectionFetchResult(KJob *job); + void itemChanged(const Akonadi::Item &item, const QSet &); + void itemsAdded(const Akonadi::Item::List &list); + void itemAdded(const Akonadi::Item &item); + void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &src, const Akonadi::Collection &dst); + void itemRemoved(const Akonadi::Item &item); + int rowForItem(const Akonadi::Item &item); + bool collectionIsCompatible() const; + + ItemModel *mParent; + + QList items; + QHash itemHash; + + Collection collection; + Monitor *monitor; + Session *session; +}; + +bool ItemModel::Private::collectionIsCompatible() const +{ + // in the generic case, we show any collection + if (mParent->mimeTypes() == QStringList(QStringLiteral("text/uri-list"))) { + return true; + } + // if the model's mime types are more specific, limit to those + // collections that have matching types + Q_FOREACH (const QString &type, mParent->mimeTypes()) { + if (collection.contentMimeTypes().contains(type)) { + return true; + } + } + return false; +} + +void ItemModel::Private::listingDone(KJob *job) +{ + ItemFetchJob *fetch = static_cast(job); + Q_UNUSED(fetch); + if (job->error()) { + // TODO + qCWarning(AKONADICORE_LOG) << "Item query failed:" << job->errorString(); + } +} + +void ItemModel::Private::collectionFetchResult(KJob *job) +{ + CollectionFetchJob *fetch = static_cast(job); + + if (fetch->collections().isEmpty()) { + return; + } + + Q_ASSERT(fetch->collections().count() == 1); // we only listed base + Collection c = fetch->collections().at(0); + // avoid recursion, if this fails for some reason + if (!c.contentMimeTypes().isEmpty()) { + mParent->setCollection(c); + } else { + qCWarning(AKONADICORE_LOG) << "Failed to retrieve the contents mime type of the collection: " << c; + mParent->setCollection(Collection()); + } +} + +int ItemModel::Private::rowForItem(const Akonadi::Item &item) +{ + ItemContainer *container = itemHash.value(item); + if (!container) { + return -1; + } + + /* Try to find the item directly; + + If items have been removed, this first try won't succeed because + the ItemContainer rows have not been updated (costs too much). + */ + if (container->row < items.count() + && items.at(container->row) == container) { + return container->row; + } else { + // Slow solution if the fist one has not succeeded + int row = -1; + const int numberOfItems(items.size()); + for (int i = 0; i < numberOfItems; ++i) { + if (items.at(i)->item == item) { + row = i; + break; + } + } + return row; + } + +} + +void ItemModel::Private::itemChanged(const Akonadi::Item &item, const QSet &) +{ + int row = rowForItem(item); + if (row < 0) { + return; + } + + items[row]->item = item; + itemHash.remove(item); + itemHash[item] = items[row]; + + QModelIndex start = mParent->index(row, 0, QModelIndex()); + QModelIndex end = mParent->index(row, mParent->columnCount(QModelIndex()) - 1, QModelIndex()); + + mParent->dataChanged(start, end); +} + +void ItemModel::Private::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &colSrc, const Akonadi::Collection &colDst) +{ + if (colSrc == collection && colDst != collection) { + // item leaving this model + itemRemoved(item); + return; + } + + if (colDst == collection && colSrc != collection) { + itemAdded(item); + return; + } +} + +void ItemModel::Private::itemsAdded(const Akonadi::Item::List &list) +{ + if (list.isEmpty()) { + return; + } + mParent->beginInsertRows(QModelIndex(), items.count(), items.count() + list.count() - 1); + foreach (const Item &item, list) { + ItemContainer *c = new ItemContainer(item, items.count()); + items.append(c); + itemHash[item] = c; + } + mParent->endInsertRows(); +} + +void ItemModel::Private::itemAdded(const Akonadi::Item &item) +{ + Item::List l; + l << item; + itemsAdded(l); +} + +void ItemModel::Private::itemRemoved(const Akonadi::Item &_item) +{ + int row = rowForItem(_item); + if (row < 0) { + return; + } + + mParent->beginRemoveRows(QModelIndex(), row, row); + const Item item = items.at(row)->item; + Q_ASSERT(item.isValid()); + itemHash.remove(item); + delete items.takeAt(row); + mParent->endRemoveRows(); +} + +ItemModel::ItemModel(QObject *parent) + : QAbstractTableModel(parent) + , d(new Private(this)) +{ +} + +ItemModel::~ItemModel() +{ + delete d; +} + +QVariant ItemModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + if (index.row() >= d->items.count()) { + return QVariant(); + } + const Item item = d->items.at(index.row())->item; + if (!item.isValid()) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + switch (index.column()) { + case Id: + return QString::number(item.id()); + case RemoteId: + return item.remoteId(); + case MimeType: + return item.mimeType(); + default: + return QVariant(); + } + } + + if (role == IdRole) { + return item.id(); + } + + if (role == ItemRole) { + QVariant var; + var.setValue(item); + return var; + } + + if (role == MimeTypeRole) { + return item.mimeType(); + } + + return QVariant(); +} + +int ItemModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return d->items.count(); + } + return 0; +} + +int ItemModel::columnCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return 3; // keep in sync with Column enum + } + return 0; +} + +QVariant ItemModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + switch (section) { + case Id: + return i18n("Id"); + case RemoteId: + return i18n("Remote Id"); + case MimeType: + return i18n("MimeType"); + default: + return QString(); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +void ItemModel::setCollection(const Collection &collection) +{ + qCDebug(AKONADICORE_LOG); + if (d->collection == collection) { + return; + } + + // if we don't know anything about this collection yet, fetch it + if (collection.isValid() && collection.contentMimeTypes().isEmpty()) { + CollectionFetchJob *job = new CollectionFetchJob(collection, CollectionFetchJob::Base, this); + connect(job, SIGNAL(result(KJob*)), this, SLOT(collectionFetchResult(KJob*))); + return; + } + + beginResetModel(); + d->monitor->setCollectionMonitored(d->collection, false); + + d->collection = collection; + + d->monitor->setCollectionMonitored(d->collection, true); + + // the query changed, thus everything we have already is invalid + qDeleteAll(d->items); + d->items.clear(); + + // stop all running jobs + d->session->clear(); + endResetModel(); + + // start listing job + if (d->collectionIsCompatible()) { + ItemFetchJob *job = new ItemFetchJob(collection, session()); + job->setFetchScope(d->monitor->itemFetchScope()); + connect(job, SIGNAL(itemsReceived(Akonadi::Item::List)), + SLOT(itemsAdded(Akonadi::Item::List))); + connect(job, SIGNAL(result(KJob*)), SLOT(listingDone(KJob*))); + } + + emit collectionChanged(collection); +} + +void ItemModel::setFetchScope(const ItemFetchScope &fetchScope) +{ + d->monitor->setItemFetchScope(fetchScope); +} + +ItemFetchScope &ItemModel::fetchScope() +{ + return d->monitor->itemFetchScope(); +} + +Item ItemModel::itemForIndex(const QModelIndex &index) const +{ + if (!index.isValid()) { + return Akonadi::Item(); + } + + if (index.row() >= d->items.count()) { + return Akonadi::Item(); + } + + Item item = d->items.at(index.row())->item; + if (item.isValid()) { + return item; + } else { + return Akonadi::Item(); + } +} + +Qt::ItemFlags ItemModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractTableModel::flags(index); + + if (index.isValid()) { + return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + } else { + return Qt::ItemIsDropEnabled | defaultFlags; + } +} + +QStringList ItemModel::mimeTypes() const +{ + return QStringList() << QStringLiteral("text/uri-list"); +} + +Session *ItemModel::session() const +{ + return d->session; +} + +QMimeData *ItemModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *data = new QMimeData(); + // Add item uri to the mimedata for dropping in external applications + QList urls; + foreach (const QModelIndex &index, indexes) { + if (index.column() != 0) { + continue; + } + + urls << itemForIndex(index).url(Item::UrlWithMimeType); + } + data->setUrls(urls); + + return data; +} + +QModelIndex ItemModel::indexForItem(const Akonadi::Item &item, const int column) const +{ + return index(d->rowForItem(item), column); +} + +bool ItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(row); + Q_UNUSED(column); + Q_UNUSED(parent); + KJob *job = PasteHelper::paste(data, d->collection, action != Qt::MoveAction); + // TODO: error handling + return job; +} + +Collection ItemModel::collection() const +{ + return d->collection; +} + +Qt::DropActions ItemModel::supportedDropActions() const +{ + return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; +} + +#include "moc_itemmodel.cpp" diff --git a/src/core/models/itemmodel.h b/src/core/models/itemmodel.h new file mode 100644 index 0000000..c73f8e7 --- /dev/null +++ b/src/core/models/itemmodel.h @@ -0,0 +1,196 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_ITEMMODEL_H +#define AKONADI_ITEMMODEL_H + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +#include + +namespace Akonadi +{ + +class Collection; +class ItemFetchScope; +class Job; +class Session; + +/** + * @short A table model for items. + * + * A self-updating table model that shows all items of + * a collection. + * + * @code + * + * QTableView *view = new QTableView( this ); + * + * Akonadi::ItemModel *model = new Akonadi::ItemModel(); + * view->setModel( model ); + * + * model->setCollection( Akonadi::Collection::root() ); + * + * @endcode + * + * @author Volker Krause + * @deprecated Use Akonadi::EntityTreeModel instead + */ +class AKONADICORE_DEPRECATED_EXPORT ItemModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + /** + * Describes the types of the columns in the model. + */ + enum Column { + Id = 0, ///< The unique id. + RemoteId, ///< The remote identifier. + MimeType ///< The item's mime type. + }; + + /** + * Describes the roles of the model. + */ + enum Roles { + IdRole = Qt::UserRole + 1, ///< The id of the item. + ItemRole, ///< The item object. + MimeTypeRole, ///< The mime type of the item. + UserRole = Qt::UserRole + 42 ///< Role for user extensions. + }; + + /** + * Creates a new item model. + * + * @param parent The parent object. + */ + explicit ItemModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the item model. + */ + virtual ~ItemModel(); + + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + + QMimeData *mimeData(const QModelIndexList &indexes) const Q_DECL_OVERRIDE; + + QStringList mimeTypes() const Q_DECL_OVERRIDE; + + Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched from the + * server, e.g. whether to fetch the full item payload or only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(const ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope. + * + * @see setFetchScope() for replacing the current item fetch scope. + */ + ItemFetchScope &fetchScope(); + + /** + * Returns the item at the given @p index. + */ + Item itemForIndex(const QModelIndex &index) const; + + /** + * Returns the model index for the given item, with the given column. + * + * @param item The item to find. + * @param column The column for the returned index. + */ + QModelIndex indexForItem(const Akonadi::Item &item, const int column) const; + + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) Q_DECL_OVERRIDE; + + /** + * Returns the collection being displayed in the model. + */ + Collection collection() const; + +public Q_SLOTS: + /** + * Sets the collection the model should display. If the collection has + * changed, the model is reset and a new message listing is requested + * from the Akonadi storage. + * + * @param collection The collection. + */ + void setCollection(const Akonadi::Collection &collection); + +Q_SIGNALS: + /** + * This signal is emitted whenever setCollection is called. + * + * @param collection The new collection. + */ + void collectionChanged(const Akonadi::Collection &collection); + +protected: + /** + * Returns the Session object used for all operations by this model. + */ + Session *session() const; + +private: + //@cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void listingDone(KJob *)) + Q_PRIVATE_SLOT(d, void collectionFetchResult(KJob *)) + Q_PRIVATE_SLOT(d, void itemChanged(const Akonadi::Item &, const QSet &)) + Q_PRIVATE_SLOT(d, void itemMoved(const Akonadi::Item &, const Akonadi::Collection &, const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d, void itemAdded(const Akonadi::Item &)) + Q_PRIVATE_SLOT(d, void itemsAdded(const Akonadi::Item::List &)) + Q_PRIVATE_SLOT(d, void itemRemoved(const Akonadi::Item &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/models/quotacolorproxymodel.cpp b/src/core/models/quotacolorproxymodel.cpp new file mode 100644 index 0000000..994692e --- /dev/null +++ b/src/core/models/quotacolorproxymodel.cpp @@ -0,0 +1,109 @@ +/* + Copyright (c) 2009 Kevin Ottens + + 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 "quotacolorproxymodel.h" + +#include +#include + +#include + +using namespace Akonadi; + +static const int qmlForegroundRole = 1984; + +/** + * @internal + */ +class Q_DECL_HIDDEN QuotaColorProxyModel::Private +{ +public: + Private(QuotaColorProxyModel *parent) + : mParent(parent), mThreshold(100.0), mColor(Qt::red) + { + } + + QuotaColorProxyModel *mParent; + + qreal mThreshold; + QColor mColor; +}; + +QuotaColorProxyModel::QuotaColorProxyModel(QObject *parent) + : QIdentityProxyModel(parent), + d(new Private(this)) +{ +} + +QuotaColorProxyModel::~QuotaColorProxyModel() +{ + delete d; +} + +void QuotaColorProxyModel::setWarningThreshold(qreal threshold) +{ + d->mThreshold = threshold; +} + +qreal QuotaColorProxyModel::warningThreshold() const +{ + return d->mThreshold; +} + +void QuotaColorProxyModel::setWarningColor(const QColor &color) +{ + d->mColor = color; +} + +QColor QuotaColorProxyModel::warningColor() const +{ + return d->mColor; +} + +QVariant QuotaColorProxyModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::ForegroundRole || role == qmlForegroundRole) { + const QModelIndex sourceIndex = mapToSource(index); + const QModelIndex rowIndex = sourceIndex.sibling(sourceIndex.row(), 0); + const Akonadi::Collection collection = sourceModel()->data(rowIndex, Akonadi::EntityTreeModel::CollectionRole).value(); + + if (collection.isValid() && collection.hasAttribute()) { + const Akonadi::CollectionQuotaAttribute *quota = collection.attribute(); + + if (quota->currentValue() > -1 && quota->maximumValue() > 0) { + const qreal percentage = (100.0 * quota->currentValue()) / quota->maximumValue(); + + if (percentage >= d->mThreshold) { + return (role == Qt::ForegroundRole ? d->mColor : d->mColor.name()); + } + } + } + } + + return QIdentityProxyModel::data(index, role); +} + +QHash QuotaColorProxyModel::roleNames() const +{ + QHash names = QIdentityProxyModel::roleNames(); + names.insert(qmlForegroundRole, "foreground"); + return names; +} + + diff --git a/src/core/models/quotacolorproxymodel.h b/src/core/models/quotacolorproxymodel.h new file mode 100644 index 0000000..e8aa178 --- /dev/null +++ b/src/core/models/quotacolorproxymodel.h @@ -0,0 +1,84 @@ +/* + Copyright (c) 2009 Kevin Ottens + + 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_QUOTACOLORPROXYMODEL_H +#define AKONADI_QUOTACOLORPROXYMODEL_H + +#include + +#include "akonadicore_export.h" + +namespace Akonadi +{ + +/** + * @short A proxy model that colors collection text if they're above a given quota + * threshold. + * + * @code + * + * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel( ... ); + * + * Akonadi::QuotaColorProxyModel *proxy = new Akonadi::QuotaColorProxyModel(); + * proxy->setSourceModel( model ); + * + * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @author Kevin Ottens + * @since 4.4 + */ +class AKONADICORE_EXPORT QuotaColorProxyModel : public QIdentityProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new quota color proxy model. + * + * @param parent The parent object. + */ + explicit QuotaColorProxyModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the statistics tooltip proxy model. + */ + virtual ~QuotaColorProxyModel(); + + void setWarningThreshold(qreal threshold); + qreal warningThreshold() const; + + void setWarningColor(const QColor &color); + QColor warningColor() const; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + QHash roleNames() const Q_DECL_OVERRIDE; +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/models/recursivecollectionfilterproxymodel.cpp b/src/core/models/recursivecollectionfilterproxymodel.cpp new file mode 100644 index 0000000..1df60b4 --- /dev/null +++ b/src/core/models/recursivecollectionfilterproxymodel.cpp @@ -0,0 +1,146 @@ +/* + Copyright (c) 2009 Stephen Kelly + Copyright (c) 2012 Laurent Montel + + 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 "recursivecollectionfilterproxymodel.h" + +#include "entitytreemodel.h" +#include "mimetypechecker.h" + +#include + +using namespace Akonadi; + +namespace Akonadi +{ + +class RecursiveCollectionFilterProxyModelPrivate +{ + Q_DECLARE_PUBLIC(RecursiveCollectionFilterProxyModel) + RecursiveCollectionFilterProxyModel *q_ptr; +public: + RecursiveCollectionFilterProxyModelPrivate(RecursiveCollectionFilterProxyModel *model) + : q_ptr(model) + , checkOnlyChecked(false) + { + + } + + QSet includedMimeTypes; + Akonadi::MimeTypeChecker checker; + QString pattern; + bool checkOnlyChecked; +}; + +} + +RecursiveCollectionFilterProxyModel::RecursiveCollectionFilterProxyModel(QObject *parent) + : KRecursiveFilterProxyModel(parent) + , d_ptr(new RecursiveCollectionFilterProxyModelPrivate(this)) +{ + +} + +RecursiveCollectionFilterProxyModel::~RecursiveCollectionFilterProxyModel() +{ + delete d_ptr; +} + +bool RecursiveCollectionFilterProxyModel::acceptRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_D(const RecursiveCollectionFilterProxyModel); + + const QModelIndex rowIndex = sourceModel()->index(sourceRow, 0, sourceParent); + const Akonadi::Collection collection = rowIndex.data(Akonadi::EntityTreeModel::CollectionRole).value(); + if (!collection.isValid()) { + return false; + } + const bool checked = (rowIndex.data(Qt::CheckStateRole).toInt() == Qt::Checked); + const bool isCheckable = sourceModel()->flags(rowIndex) & Qt::ItemIsUserCheckable; + if (isCheckable && (d->checkOnlyChecked && !checked)) { + return false; + } + + const bool collectionWanted = d->checker.isWantedCollection(collection); + if (collectionWanted) { + if (!d->pattern.isEmpty()) { + const QString text = rowIndex.data(Qt::DisplayRole).toString(); + return text.contains(d->pattern, Qt::CaseInsensitive); + } + } + return collectionWanted; +} + +void RecursiveCollectionFilterProxyModel::addContentMimeTypeInclusionFilter(const QString &mimeType) +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->includedMimeTypes << mimeType; + d->checker.setWantedMimeTypes(d->includedMimeTypes.toList()); + invalidateFilter(); +} + +void RecursiveCollectionFilterProxyModel::addContentMimeTypeInclusionFilters(const QStringList &mimeTypes) +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->includedMimeTypes.unite(mimeTypes.toSet()); + d->checker.setWantedMimeTypes(d->includedMimeTypes.toList()); + invalidateFilter(); +} + +void RecursiveCollectionFilterProxyModel::clearFilters() +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->includedMimeTypes.clear(); + d->checker.setWantedMimeTypes(QStringList()); + invalidateFilter(); +} + +void RecursiveCollectionFilterProxyModel::setContentMimeTypeInclusionFilters(const QStringList &mimeTypes) +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->includedMimeTypes = mimeTypes.toSet(); + d->checker.setWantedMimeTypes(d->includedMimeTypes.toList()); + invalidateFilter(); +} + +QStringList RecursiveCollectionFilterProxyModel::contentMimeTypeInclusionFilters() const +{ + Q_D(const RecursiveCollectionFilterProxyModel); + return d->includedMimeTypes.toList(); +} + +int Akonadi::RecursiveCollectionFilterProxyModel::columnCount(const QModelIndex &index) const +{ + // Optimization: we know that we're not changing the number of columns, so skip QSortFilterProxyModel + return sourceModel()->columnCount(mapToSource(index)); +} + +void Akonadi::RecursiveCollectionFilterProxyModel::setSearchPattern(const QString &pattern) +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->pattern = pattern; + invalidate(); +} + +void Akonadi::RecursiveCollectionFilterProxyModel::setIncludeCheckedOnly(bool checked) +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->checkOnlyChecked = checked; + invalidate(); +} diff --git a/src/core/models/recursivecollectionfilterproxymodel.h b/src/core/models/recursivecollectionfilterproxymodel.h new file mode 100644 index 0000000..69d9774 --- /dev/null +++ b/src/core/models/recursivecollectionfilterproxymodel.h @@ -0,0 +1,112 @@ +/* + Copyright (c) 2009 Stephen Kelly + Copyright (c) 2012 Laurent Montel + + 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_RECURSIVECOLLECTIONFILTERPROXYMODEL_H +#define AKONADI_RECURSIVECOLLECTIONFILTERPROXYMODEL_H + +#include + +#include "akonadicore_export.h" + +namespace Akonadi +{ + +class RecursiveCollectionFilterProxyModelPrivate; + +/** + * @short A model to filter out collections of non-matching content types. + * + * @author Stephen Kelly + * @since 4.6 + */ +class AKONADICORE_EXPORT RecursiveCollectionFilterProxyModel : public KRecursiveFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new recursive collection filter proxy model. + * + * @param parent The parent object. + */ + RecursiveCollectionFilterProxyModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the recursive collection filter proxy model. + */ + virtual ~RecursiveCollectionFilterProxyModel(); + + /** + * Add content mime type to be shown by the filter. + * + * @param mimeType A mime type to be shown. + */ + void addContentMimeTypeInclusionFilter(const QString &mimeType); + + /** + * Add content mime types to be shown by the filter. + * + * @param mimeTypes A list of content mime types to be included. + */ + void addContentMimeTypeInclusionFilters(const QStringList &mimeTypes); + + /** + * Clears the current filters. + */ + void clearFilters(); + + /** + * Replace the content mime types to be shown by the filter. + * + * @param mimeTypes A list of content mime types to be included. + */ + void setContentMimeTypeInclusionFilters(const QStringList &mimeTypes); + + /** + * Returns the currently included mimetypes in the filter. + */ + QStringList contentMimeTypeInclusionFilters() const; + + /** + * Add search pattern + * @param pattern the search pattern to add + * @since 4.8.1 + */ + void setSearchPattern(const QString &pattern); + + /** + * Show only checked item + * @param checked only shows checked item if set as @c true + * @since 4.9 + */ + void setIncludeCheckedOnly(bool checked); + +protected: + bool acceptRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex &index) const Q_DECL_OVERRIDE; + +protected: + RecursiveCollectionFilterProxyModelPrivate *const d_ptr; + Q_DECLARE_PRIVATE(RecursiveCollectionFilterProxyModel) +}; + +} + +#endif diff --git a/src/core/models/selectionproxymodel.cpp b/src/core/models/selectionproxymodel.cpp new file mode 100644 index 0000000..6a67b0f --- /dev/null +++ b/src/core/models/selectionproxymodel.cpp @@ -0,0 +1,87 @@ +/* + 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 "selectionproxymodel.h" + +#include "entitytreemodel.h" + +using namespace Akonadi; + +namespace Akonadi +{ + +class SelectionProxyModelPrivate +{ +public: + SelectionProxyModelPrivate(SelectionProxyModel *selectionProxyModel) + : q_ptr(selectionProxyModel) + { + Q_Q(SelectionProxyModel); + foreach (const QModelIndex &rootIndex, q->sourceRootIndexes()) { + rootIndexAdded(rootIndex); + } + } + ~SelectionProxyModelPrivate() + { + Q_Q(SelectionProxyModel); + foreach (const QModelIndex &idx, q->sourceRootIndexes()) { + rootIndexAboutToBeRemoved(idx); + } + } + + /** + Increases the refcount of the Collection in @p newRootIndex + */ + void rootIndexAdded(const QModelIndex &newRootIndex) + { + Q_Q(SelectionProxyModel); + // newRootIndex is already in the sourceModel. + q->sourceModel()->setData(newRootIndex, QVariant(), EntityTreeModel::CollectionRefRole); + q->sourceModel()->fetchMore(newRootIndex); + } + + /** + Decreases the refcount of the Collection in @p removedRootIndex + */ + void rootIndexAboutToBeRemoved(const QModelIndex &removedRootIndex) + { + Q_Q(SelectionProxyModel); + q->sourceModel()->setData(removedRootIndex, QVariant(), EntityTreeModel::CollectionDerefRole); + } + + Q_DECLARE_PUBLIC(SelectionProxyModel) + SelectionProxyModel *q_ptr; +}; + +} + +SelectionProxyModel::SelectionProxyModel(QItemSelectionModel *selectionModel, QObject *parent) + : KSelectionProxyModel(selectionModel, parent) + , d_ptr(new SelectionProxyModelPrivate(this)) +{ + connect(this, SIGNAL(rootIndexAdded(QModelIndex)), SLOT(rootIndexAdded(QModelIndex))); + connect(this, SIGNAL(rootIndexAboutToBeRemoved(QModelIndex)), SLOT(rootIndexAboutToBeRemoved(QModelIndex))); +} + +SelectionProxyModel::~SelectionProxyModel() +{ + delete d_ptr; +} + +#include "moc_selectionproxymodel.cpp" diff --git a/src/core/models/selectionproxymodel.h b/src/core/models/selectionproxymodel.h new file mode 100644 index 0000000..375fb2d --- /dev/null +++ b/src/core/models/selectionproxymodel.h @@ -0,0 +1,125 @@ +/* + 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_SELECTIONPROXYMODEL_H +#define AKONADI_SELECTIONPROXYMODEL_H + +#include + +#include "akonadicore_export.h" + +namespace Akonadi +{ + +class SelectionProxyModelPrivate; + +/** + * @short A proxy model used to reference count selected Akonadi::Collection in a view + * + * Only selected Collections will be populated and monitored for changes. Unselected + * Collections will be ignored. + * + * This model extends KSelectionProxyModel to implement reference counting on the Collections + * in an EntityTreeModel. The EntityTreeModel must use LazyPopulation to enable + * SelectionProxyModel to work. + * + * By selecting a Collection, its reference count will be increased. A Collection in the + * EntityTreeModel which has a reference count of zero will ignore all signals from Monitor + * about items changed, inserted, removed etc, which can be expensive operations. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * // itemView + * // ^ + * // | + * // itemModel + * // | + * // flatModel + * // | + * // collectionView --> selectionModel + * // ^ ^ + * // | | + * // collectionFilter | + * // \______________model + * + * EntityTreeModel *model = new EntityTreeModel( ... ); + * + * // setup collection model + * EntityMimeTypeFilterModel *collectionFilter = new EntityMimeTypeFilterModel( this ); + * collectionFilter->setSourceModel( model ); + * collectionFilter->addMimeTypeInclusionFilter( Collection::mimeType() ); + * collectionFilter->setHeaderGroup( EntityTreeModel::CollectionTreeHeaders ); + * + * // setup collection view + * EntityTreeView *collectionView = new EntityTreeView( this ); + * collectionView->setModel( collectionFilter ); + * + * // setup selection model + * SelectionProxyModel *selectionModel = new SelectionProxyModel( collectionView->selectionModel(), this ); + * selectionModel->setSourceModel( model ); + * + * // setup item model + * KDescendantsProxyModel *flatModel = new KDescendantsProxyModel( this ); + * flatModel->setSourceModel( selectionModel ); + * + * EntityMimeTypeFilterModel *itemModel = new EntityMimeTypeFilterModel( this ); + * itemModel->setSourceModel( flatModel ); + * itemModel->setHeaderGroup( EntityTreeModel::ItemListHeaders ); + * itemModel->addMimeTypeExclusionFilter( Collection::mimeType() ); + * + * EntityListView *itemView = new EntityListView( this ); + * itemView->setModel( itemModel ); + * @endcode + * + * See \ref libakonadi_integration "Integration in your Application" for further guidance on the use of this class. + + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADICORE_EXPORT SelectionProxyModel : public KSelectionProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new selection proxy model. + * + * @param selectionModel The selection model of the source view. + * @param parent The parent object. + */ + explicit SelectionProxyModel(QItemSelectionModel *selectionModel, QObject *parent = Q_NULLPTR); + ~SelectionProxyModel(); + +private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(SelectionProxyModel) + SelectionProxyModelPrivate *const d_ptr; + + Q_PRIVATE_SLOT(d_func(), void rootIndexAdded(const QModelIndex &)) + Q_PRIVATE_SLOT(d_func(), void rootIndexAboutToBeRemoved(const QModelIndex &)) + //@endcond +}; + +} + +#endif diff --git a/src/core/models/statisticsproxymodel.cpp b/src/core/models/statisticsproxymodel.cpp new file mode 100644 index 0000000..435e490 --- /dev/null +++ b/src/core/models/statisticsproxymodel.cpp @@ -0,0 +1,553 @@ +/* + Copyright (c) 2009 Kevin Ottens + + 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 "statisticsproxymodel.h" +#include "akonadicore_debug.h" + +#include "entitytreemodel.h" +#include "collectionutils.h" +#include "collectionquotaattribute.h" +#include "collectionstatistics.h" +#include "entitydisplayattribute.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN StatisticsProxyModel::Private +{ +public: + Private(StatisticsProxyModel *parent) + : mParent(parent), mToolTipEnabled(false), mExtraColumnsEnabled(true) + { + } + + void getCountRecursive(const QModelIndex &index, qint64 &totalSize) const + { + Collection collection = qvariant_cast(index.data(EntityTreeModel::CollectionRole)); + // Do not assert on invalid collections, since a collection may be deleted + // in the meantime and deleted collections are invalid. + if (collection.isValid()) { + CollectionStatistics statistics = collection.statistics(); + totalSize += qMax(0LL, statistics.size()); + if (index.model()->hasChildren(index)) { + const int rowCount = index.model()->rowCount(index); + for (int row = 0; row < rowCount; row++) { + static const int column = 0; + getCountRecursive(index.model()->index(row, column, index), totalSize); + } + } + } + } + + int sourceColumnCount() const + { + return mParent->sourceModel()->columnCount(); + } + + QModelIndex sourceIndexAtFirstColumn(const QModelIndex &proxyIndex) const; + + QString toolTipForCollection(const QModelIndex &index, const Collection &collection) + { + QString bckColor = QApplication::palette().color(QPalette::ToolTipBase).name(); + QString txtColor = QApplication::palette().color(QPalette::ToolTipText).name(); + + QString tip = QStringLiteral( + "\n" + ); + const QString textDirection = (QApplication::layoutDirection() == Qt::LeftToRight) ? QStringLiteral("left") : QStringLiteral("right"); + tip += QStringLiteral( + " \n" + " \n" + " \n" + ).arg(txtColor, bckColor, index.data(Qt::DisplayRole).toString(), textDirection); + + tip += QStringLiteral( + " \n" + " \n" + ).arg(iconPath).arg(icon_size_found) ; + + if (QApplication::layoutDirection() == Qt::LeftToRight) { + tip += tipInfo + QStringLiteral("" \ + "
\n" + "
\n" + " %3\n" + "
\n" + "
\n" + ).arg(textDirection); + + QString tipInfo; + tipInfo += QStringLiteral( + " %1: %2
\n" + " %3: %4

\n" + ).arg(i18n("Total Messages")).arg(collection.statistics().count()) + .arg(i18n("Unread Messages")).arg(collection.statistics().unreadCount()); + + if (collection.hasAttribute()) { + CollectionQuotaAttribute *quota = collection.attribute(); + if (quota->currentValue() > -1 && quota->maximumValue() > 0) { + qreal percentage = (100.0 * quota->currentValue()) / quota->maximumValue(); + + if (qAbs(percentage) >= 0.01) { + QString percentStr = QString::number(percentage, 'f', 2); + tipInfo += QStringLiteral( + " %1: %2%
\n" + ).arg(i18n("Quota"), percentStr); + } + } + } + + KFormat formatter; + qint64 currentFolderSize(collection.statistics().size()); + tipInfo += QStringLiteral( + " %1: %2
\n" + ).arg(i18n("Storage Size"), formatter.formatByteSize(currentFolderSize)); + + qint64 totalSize = 0; + getCountRecursive(index, totalSize); + totalSize -= currentFolderSize; + if (totalSize > 0) { + tipInfo += QStringLiteral( + "%1: %2
" + ).arg(i18n("Subfolder Storage Size"), formatter.formatByteSize(totalSize)); + } + + QString iconName = CollectionUtils::defaultIconName(collection); + if (collection.hasAttribute() && + !collection.attribute()->iconName().isEmpty()) { + if (!collection.attribute()->activeIconName().isEmpty() && collection.statistics().unreadCount() > 0) { + iconName = collection.attribute()->activeIconName(); + } else { + iconName = collection.attribute()->iconName(); + } + } + + int iconSizes[] = { 32, 22 }; + int icon_size_found = 32; + + QString iconPath; + + for (int i = 0; i < 2; ++i) { + iconPath = KIconLoader::global()->iconPath(iconName, -iconSizes[ i ], true); + if (!iconPath.isEmpty()) { + icon_size_found = iconSizes[ i ]; + break; + } + } + + if (iconPath.isEmpty()) { + iconPath = KIconLoader::global()->iconPath(QStringLiteral("folder"), -32, false); + } + + QString tipIcon = QStringLiteral( + "
\n" + " \n" + "
\n" + "
").arg(textDirection) + tipIcon; + } else { + tip += tipIcon + QStringLiteral("").arg(textDirection) + tipInfo; + } + + tip += QLatin1String( + "
" + ); + + return tip; + } + + void proxyDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + + void sourceLayoutAboutToBeChanged(); + void sourceLayoutChanged(); + + QVector m_proxyIndexes; + QVector m_persistentSourceFirstColumn; + + StatisticsProxyModel *mParent; + + bool mToolTipEnabled; + bool mExtraColumnsEnabled; +}; + +void StatisticsProxyModel::Private::proxyDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (mExtraColumnsEnabled) { + // Ugly hack. + // The proper solution is a KExtraColumnsProxyModel, but this will do for now. + QModelIndex parent = topLeft.parent(); + int parentColumnCount = mParent->columnCount(parent); + QModelIndex extraTopLeft = mParent->index(topLeft.row(), parentColumnCount - 1 - 3, parent); + QModelIndex extraBottomRight = mParent->index(bottomRight.row(), parentColumnCount - 1, parent); + mParent->disconnect(mParent, SIGNAL(dataChanged(QModelIndex,QModelIndex)), mParent, SLOT(proxyDataChanged(QModelIndex,QModelIndex))); + emit mParent->dataChanged(extraTopLeft, extraBottomRight); + + // We get this signal when the statistics of a row changes. + // However, we need to emit data changed for the statistics of all ancestor rows too + // so that recursive totals can be updated. + while (parent.isValid()) { + const QModelIndex left = mParent->index(parent.row(), parentColumnCount - 1 - 3, parent.parent()); + const QModelIndex right = mParent->index(parent.row(), parentColumnCount - 1, parent.parent()); + emit mParent->dataChanged(left, right); + parent = parent.parent(); + parentColumnCount = mParent->columnCount(parent); + } + mParent->connect(mParent, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + SLOT(proxyDataChanged(QModelIndex,QModelIndex))); + } +} + +void StatisticsProxyModel::Private::sourceLayoutAboutToBeChanged() +{ + // QIdentityProxyModel took care of the first columnCount() columns + // We have to take care of the extra columns (by storing persistent indexes in column 0, + // waiting for the source to update them, and then looking at where they ended up) + QModelIndexList persistent = mParent->persistentIndexList(); + const int columnCount = mParent->sourceModel()->columnCount(); + foreach (const QModelIndex &proxyPersistentIndex, persistent) { + if (proxyPersistentIndex.column() >= columnCount) { + m_proxyIndexes << proxyPersistentIndex; + m_persistentSourceFirstColumn << QPersistentModelIndex(sourceIndexAtFirstColumn(proxyPersistentIndex)); + } + } +} + +void StatisticsProxyModel::Private::sourceLayoutChanged() +{ + QModelIndexList oldList; + QModelIndexList newList; + + for (int i = 0; i < m_proxyIndexes.size(); ++i) { + const QModelIndex oldProxyIndex = m_proxyIndexes.at(i); + const QModelIndex proxyIndexFirstCol = mParent->mapFromSource(m_persistentSourceFirstColumn.at(i)); + const QModelIndex newProxyIndex = proxyIndexFirstCol.sibling(proxyIndexFirstCol.row(), oldProxyIndex.column()); + if (newProxyIndex != oldProxyIndex) { + oldList.append(oldProxyIndex); + newList.append(newProxyIndex); + } + } + mParent->changePersistentIndexList(oldList, newList); + m_persistentSourceFirstColumn.clear(); + m_proxyIndexes.clear(); +} + +void StatisticsProxyModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + // Order is important here. sourceLayoutChanged must be called *before* any downstreams react + // to the layoutChanged so that it can have the QPersistentModelIndexes uptodate in time. + disconnect(this, SIGNAL(layoutChanged()), this, SLOT(sourceLayoutChanged())); + connect(this, SIGNAL(layoutChanged()), SLOT(sourceLayoutChanged())); + QIdentityProxyModel::setSourceModel(sourceModel); + // This one should come *after* any downstream handlers of layoutAboutToBeChanged. + // The connectNotify stuff below ensures that it remains the last one. + disconnect(this, SIGNAL(layoutAboutToBeChanged()), this, SLOT(sourceLayoutAboutToBeChanged())); + connect(this, SIGNAL(layoutAboutToBeChanged()), SLOT(sourceLayoutAboutToBeChanged())); +} + +void StatisticsProxyModel::connectNotify(const QMetaMethod &signal) +{ + static bool ignore = false; + if (ignore || signal == QMetaMethod::fromSignal(&StatisticsProxyModel::layoutAboutToBeChanged)) { + return QIdentityProxyModel::connectNotify(signal); + } + ignore = true; + disconnect(this, SIGNAL(layoutAboutToBeChanged()), this, SLOT(sourceLayoutAboutToBeChanged())); + connect(this, SIGNAL(layoutAboutToBeChanged()), SLOT(sourceLayoutAboutToBeChanged())); + ignore = false; + QIdentityProxyModel::connectNotify(signal); +} + +StatisticsProxyModel::StatisticsProxyModel(QObject *parent) + : QIdentityProxyModel(parent), + d(new Private(this)) +{ + connect(this, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + SLOT(proxyDataChanged(QModelIndex,QModelIndex))); +} + +StatisticsProxyModel::~StatisticsProxyModel() +{ + delete d; +} + +void StatisticsProxyModel::setToolTipEnabled(bool enable) +{ + d->mToolTipEnabled = enable; +} + +bool StatisticsProxyModel::isToolTipEnabled() const +{ + return d->mToolTipEnabled; +} + +void StatisticsProxyModel::setExtraColumnsEnabled(bool enable) +{ + d->mExtraColumnsEnabled = enable; +} + +bool StatisticsProxyModel::isExtraColumnsEnabled() const +{ + return d->mExtraColumnsEnabled; +} + +QModelIndex StatisticsProxyModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) { + return QModelIndex(); + } + + int sourceColumn = column; + if (column >= d->sourceColumnCount()) { + sourceColumn = 0; + } + + QModelIndex i = QIdentityProxyModel::index(row, sourceColumn, parent); + return createIndex(i.row(), column, i.internalPointer()); +} + +struct SourceModelIndex { + SourceModelIndex(int _r, int _c, void *_p, QAbstractItemModel *_m) + : r(_r), c(_c), p(_p), m(_m) {} + + operator QModelIndex() + { + return reinterpret_cast(*this); + } + + int r, c; + void *p; + const QAbstractItemModel *m; +}; + +QModelIndex StatisticsProxyModel::Private::sourceIndexAtFirstColumn(const QModelIndex &proxyIndex) const +{ + // We rely on the fact that the internal pointer is the same for column 0 and for the extra columns + return SourceModelIndex(proxyIndex.row(), 0, proxyIndex.internalPointer(), mParent->sourceModel()); +} + +QModelIndex StatisticsProxyModel::parent(const QModelIndex &child) const +{ + if (!sourceModel()) { + return QModelIndex(); + } + + Q_ASSERT(child.isValid() ? child.model() == this : true); + if (child.column() >= d->sourceColumnCount()) { + // We need to get hold of the source index at column 0. But we can't do that + // via the proxy index at column 0, because sibling() or index() needs the + // parent index, and that's *exactly* what we're trying to determine here. + // So the only way is to create a source index ourselves. + const QModelIndex sourceIndex = d->sourceIndexAtFirstColumn(child); + const QModelIndex sourceParent = sourceIndex.parent(); + //qCDebug(AKONADICORE_LOG) << "parent of" << child.data() << "is" << sourceParent.data(); + return mapFromSource(sourceParent); + } else { + return QIdentityProxyModel::parent(child); + } +} + +QVariant StatisticsProxyModel::data(const QModelIndex &index, int role) const +{ + if (!sourceModel()) { + return QVariant(); + } + + const int sourceColumnCount = d->sourceColumnCount(); + + if (role == Qt::DisplayRole && index.column() >= sourceColumnCount) { + const QModelIndex sourceIndex = d->sourceIndexAtFirstColumn(index); + Collection collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value(); + + if (collection.isValid() && collection.statistics().count() >= 0) { + if (index.column() == sourceColumnCount + 2) { + KFormat formatter; + return formatter.formatByteSize(collection.statistics().size()); + } else if (index.column() == sourceColumnCount + 1) { + return collection.statistics().count(); + } else if (index.column() == sourceColumnCount) { + if (collection.statistics().unreadCount() > 0) { + return collection.statistics().unreadCount(); + } else { + return QString(); + } + } else { + qCWarning(AKONADICORE_LOG) << "We shouldn't get there for a column which is not total, unread or size."; + return QVariant(); + } + } + + } else if (role == Qt::TextAlignmentRole && index.column() >= sourceColumnCount) { + return Qt::AlignRight; + + } else if (role == Qt::ToolTipRole && d->mToolTipEnabled) { + const QModelIndex sourceIndex = d->sourceIndexAtFirstColumn(index); + Collection collection + = sourceModel()->data(sourceIndex, + EntityTreeModel::CollectionRole).value(); + + if (collection.isValid()) { + const QModelIndex sourceIndex = d->sourceIndexAtFirstColumn(index); + return d->toolTipForCollection(sourceIndex, collection); + } + + } else if (role == Qt::DecorationRole && index.column() == 0) { + const QModelIndex sourceIndex = mapToSource(index); + return sourceModel()->data(sourceIndex, Qt::DecorationRole); + } + + if (index.column() >= sourceColumnCount) { + return QVariant(); + } + + return QAbstractProxyModel::data(index, role); +} + +QVariant StatisticsProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + if (section == d->sourceColumnCount() + 2) { + return i18nc("collection size", "Size"); + } else if (section == d->sourceColumnCount() + 1) { + return i18nc("number of entities in the collection", "Total"); + } else if (section == d->sourceColumnCount()) { + return i18nc("number of unread entities in the collection", "Unread"); + } + } + + if (orientation == Qt::Horizontal && section >= d->sourceColumnCount()) { + return QVariant(); + } + + return QIdentityProxyModel::headerData(section, orientation, role); +} + +Qt::ItemFlags StatisticsProxyModel::flags(const QModelIndex &index_) const +{ + if (index_.column() >= d->sourceColumnCount()) { + return QIdentityProxyModel::flags(index(index_.row(), 0, index_.parent())) + & (Qt::ItemIsSelectable | Qt::ItemIsDragEnabled // Allowed flags + | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled); + } + + return QIdentityProxyModel::flags(index_); +} + +int StatisticsProxyModel::columnCount(const QModelIndex & /*parent*/) const +{ + if (sourceModel() == 0) { + return 0; + } else { + return d->sourceColumnCount() + + (d->mExtraColumnsEnabled ? 3 : 0); + } +} + +QModelIndexList StatisticsProxyModel::match(const QModelIndex &start, int role, const QVariant &value, + int hits, Qt::MatchFlags flags) const +{ + if (role < Qt::UserRole) { + return QIdentityProxyModel::match(start, role, value, hits, flags); + } + + QModelIndexList list; + QModelIndex proxyIndex; + foreach (const QModelIndex &idx, sourceModel()->match(mapToSource(start), role, value, hits, flags)) { + proxyIndex = mapFromSource(idx); + if (proxyIndex.isValid()) { + list << proxyIndex; + } + } + + return list; +} + +QModelIndex StatisticsProxyModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + if (!sourceIndex.isValid()) { + return QModelIndex(); + } + Q_ASSERT(sourceIndex.model() == sourceModel()); + Q_ASSERT(sourceIndex.column() < d->sourceColumnCount()); + return QIdentityProxyModel::mapFromSource(sourceIndex); +} + +QModelIndex StatisticsProxyModel::mapToSource(const QModelIndex &index) const +{ + if (!index.isValid()) { + return QModelIndex(); + } + Q_ASSERT(index.model() == this); + if (index.column() >= d->sourceColumnCount()) { + return QModelIndex(); + } + return QIdentityProxyModel::mapToSource(index); +} + +QModelIndex StatisticsProxyModel::buddy(const QModelIndex &index) const +{ + Q_UNUSED(index); + return QModelIndex(); +} + +QItemSelection StatisticsProxyModel::mapSelectionToSource(const QItemSelection &selection) const +{ + QItemSelection sourceSelection; + + if (!sourceModel()) { + return sourceSelection; + } + + // mapToSource will give invalid index for our additional columns, so truncate the selection + // to the columns known by the source model + const int sourceColumnCount = d->sourceColumnCount(); + QItemSelection::const_iterator it = selection.constBegin(); + const QItemSelection::const_iterator end = selection.constEnd(); + for (; it != end; ++it) { + Q_ASSERT(it->model() == this); + QModelIndex topLeft = it->topLeft(); + Q_ASSERT(topLeft.isValid()); + Q_ASSERT(topLeft.model() == this); + topLeft = topLeft.sibling(topLeft.row(), 0); + QModelIndex bottomRight = it->bottomRight(); + Q_ASSERT(bottomRight.isValid()); + Q_ASSERT(bottomRight.model() == this); + if (bottomRight.column() >= sourceColumnCount) { + bottomRight = bottomRight.sibling(bottomRight.row(), sourceColumnCount - 1); + } + // This can lead to duplicate source indexes, so use merge(). + const QItemSelectionRange range(mapToSource(topLeft), mapToSource(bottomRight)); + QItemSelection newSelection; newSelection << range; + sourceSelection.merge(newSelection, QItemSelectionModel::Select); + } + + return sourceSelection; +} + +#include "moc_statisticsproxymodel.cpp" + diff --git a/src/core/models/statisticsproxymodel.h b/src/core/models/statisticsproxymodel.h new file mode 100644 index 0000000..e189d9c --- /dev/null +++ b/src/core/models/statisticsproxymodel.h @@ -0,0 +1,120 @@ +/* + Copyright (c) 2009 Kevin Ottens + + 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_STATISTICSPROXYMODEL_H +#define AKONADI_STATISTICSPROXYMODEL_H + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ + +/** + * @short A proxy model that exposes collection statistics through extra columns. + * + * This class can be used on top of an EntityTreeModel to display extra columns + * summarizing statistics of collections. + * + * @code + * + * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel( ... ); + * + * Akonadi::StatisticsProxyModel *proxy = new Akonadi::StatisticsProxyModel(); + * proxy->setSourceModel( model ); + * + * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @author Kevin Ottens + * @since 4.4 + */ +class AKONADICORE_EXPORT StatisticsProxyModel : public QIdentityProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new statistics proxy model. + * + * @param parent The parent object. + */ + explicit StatisticsProxyModel(QObject *parent = Q_NULLPTR); + + /** + * Destroys the statistics proxy model. + */ + virtual ~StatisticsProxyModel(); + + /** + * @param enable Display tooltips + */ + void setToolTipEnabled(bool enable); + + /** + * Return true if we display tooltips, otherwise false + */ + bool isToolTipEnabled() const; + + /** + * @param enable Display extra statistics columns + */ + void setExtraColumnsEnabled(bool enable); + + /** + * Return true if we display extra statistics columns, otherwise false + */ + bool isExtraColumnsEnabled() const; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QModelIndex parent(const QModelIndex &child) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + + virtual QModelIndexList match(const QModelIndex &start, int role, const QVariant &value, int hits = 1, + Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const Q_DECL_OVERRIDE; + + void setSourceModel(QAbstractItemModel *sourceModel) Q_DECL_OVERRIDE; + void connectNotify(const QMetaMethod &signal) Q_DECL_OVERRIDE; + + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const Q_DECL_OVERRIDE; + QModelIndex mapToSource(const QModelIndex &sourceIndex) const Q_DECL_OVERRIDE; + QModelIndex buddy(const QModelIndex &index) const Q_DECL_OVERRIDE; + + QItemSelection mapSelectionToSource(const QItemSelection &selection) const Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void proxyDataChanged(QModelIndex, QModelIndex)) + Q_PRIVATE_SLOT(d, void sourceLayoutAboutToBeChanged()) + Q_PRIVATE_SLOT(d, void sourceLayoutChanged()) + //@endcond +}; + +} + +#endif diff --git a/src/core/models/subscriptionmodel.cpp b/src/core/models/subscriptionmodel.cpp new file mode 100644 index 0000000..45f9677 --- /dev/null +++ b/src/core/models/subscriptionmodel.cpp @@ -0,0 +1,198 @@ +/* + 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 "subscriptionmodel_p.h" +#include "collectionfetchjob.h" +#include "collectionutils.h" +#include "specialcollectionattribute.h" + +#include "entityhiddenattribute.h" + +#include "akonadicore_debug.h" + +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class SubscriptionModel::Private +{ +public: + Private(SubscriptionModel *parent) : q(parent), showHiddenCollection(false) {} + SubscriptionModel *q; + QHash subscriptions; + QSet changes; + bool showHiddenCollection; + + Collection::List changedSubscriptions(bool subscribed) + { + Collection::List list; + foreach (Collection::Id id, changes) { + if (subscriptions.value(id) == subscribed) { + list << Collection(id); + } + } + return list; + } + + void listResult(KJob *job) + { + if (job->error()) { + // TODO + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; + } + q->beginResetModel(); + Collection::List cols = static_cast(job)->collections(); + foreach (const Collection &col, cols) { + if (!CollectionUtils::isStructural(col)) { + subscriptions[ col.id() ] = true; + } + } + q->endResetModel(); + emit q->loaded(); + } + + bool isSubscribable(Collection::Id id) + { + Collection col = q->collectionForId(id); + if (CollectionUtils::isStructural(col) || col.isVirtual()) { + return false; + } + if (col.hasAttribute()) { + return false; + } + if (col.contentMimeTypes().isEmpty()) { + return false; + } + return true; + } +}; + +SubscriptionModel::SubscriptionModel(QObject *parent) : + CollectionModel(parent), + d(new Private(this)) +{ + includeUnsubscribed(); + CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + connect(job, SIGNAL(result(KJob*)), this, SLOT(listResult(KJob*))); +} + +SubscriptionModel::~ SubscriptionModel() +{ + delete d; +} + +QVariant SubscriptionModel::data(const QModelIndex &index, int role) const +{ + switch (role) { + case Qt::CheckStateRole: { + const Collection::Id col = index.data(CollectionIdRole).toLongLong(); + if (!d->isSubscribable(col)) { + return QVariant(); + } + if (d->subscriptions.value(col)) { + return Qt::Checked; + } + return Qt::Unchecked; + } + case SubscriptionChangedRole: { + const Collection::Id col = index.data(CollectionIdRole).toLongLong(); + if (d->changes.contains(col)) { + return true; + } + return false; + } + case Qt::FontRole: { + const Collection::Id col = index.data(CollectionIdRole).toLongLong(); + + QFont font = CollectionModel::data(index, role).value(); + font.setBold(d->changes.contains(col)); + + return font; + } + } + + if (role == CollectionIdRole) { + return CollectionModel::data(index, CollectionIdRole); + } else { + const Collection::Id collectionId = index.data(CollectionIdRole).toLongLong(); + const Collection collection = collectionForId(collectionId); + if (collection.hasAttribute()) { + if (d->showHiddenCollection) { + return CollectionModel::data(index, role); + } else { + return QVariant(); + } + } else { + return CollectionModel::data(index, role); + } + } +} + +Qt::ItemFlags SubscriptionModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = CollectionModel::flags(index); + if (d->isSubscribable(index.data(CollectionIdRole).toLongLong())) { + return flags | Qt::ItemIsUserCheckable; + } + return flags; +} + +bool SubscriptionModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == Qt::CheckStateRole) { + const Collection::Id col = index.data(CollectionIdRole).toLongLong(); + if (!d->isSubscribable(col)) { + return true; //No change + } + if (d->subscriptions.contains(col) && d->subscriptions.value(col) == (value == Qt::Checked)) { + return true; // no change + } + d->subscriptions[ col ] = value == Qt::Checked; + if (d->changes.contains(col)) { + d->changes.remove(col); + } else { + d->changes.insert(col); + } + emit dataChanged(index, index); + return true; + } + return CollectionModel::setData(index, value, role); +} + +Akonadi::Collection::List SubscriptionModel::subscribed() const +{ + return d->changedSubscriptions(true); +} + +Akonadi::Collection::List SubscriptionModel::unsubscribed() const +{ + return d->changedSubscriptions(false); +} + +void SubscriptionModel::showHiddenCollection(bool showHidden) +{ + d->showHiddenCollection = showHidden; +} + +#include "moc_subscriptionmodel_p.cpp" diff --git a/src/core/models/subscriptionmodel_p.h b/src/core/models/subscriptionmodel_p.h new file mode 100644 index 0000000..ea55928 --- /dev/null +++ b/src/core/models/subscriptionmodel_p.h @@ -0,0 +1,83 @@ +/* + 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_SUBSCRIPTIONMODEL_P_H +#define AKONADI_SUBSCRIPTIONMODEL_P_H + +#include "akonadicore_export.h" +#include "collection.h" +#include "collectionmodel.h" + +namespace Akonadi +{ + +/** + * @internal + * @deprecated This should be replaced by something based on EntityTreeModel + * + * An extended collection model used for the subscription dialog. + */ +class AKONADICORE_EXPORT SubscriptionModel : public CollectionModel +{ + Q_OBJECT +public: + /** Additional roles. */ + enum Roles { + SubscriptionChangedRole = CollectionModel::UserRole + 1 ///< Indicate the subscription status has been changed. + }; + + /** + Create a new subscription model. + @param parent The parent object. + */ + explicit SubscriptionModel(QObject *parent = 0); + + /** + Destructor. + */ + ~SubscriptionModel(); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE; + + Collection::List subscribed() const; + Collection::List unsubscribed() const; + + /** + * @param showHidden shows hidden collection if set as @c true + * @since: 4.9 + */ + void showHiddenCollection(bool showHidden); + +Q_SIGNALS: + /** + Emitted when the collection model is fully loaded. + */ + void loaded(); + +private: + class Private; + Private *const d; + Q_PRIVATE_SLOT(d, void listResult(KJob *)) +}; + +} + +#endif diff --git a/src/core/models/tagmodel.cpp b/src/core/models/tagmodel.cpp new file mode 100644 index 0000000..14028a1 --- /dev/null +++ b/src/core/models/tagmodel.cpp @@ -0,0 +1,181 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "tagmodel.h" +#include "tagmodel_p.h" +#include "tagattribute.h" + +#include +#include + +using namespace Akonadi; + +TagModel::TagModel(Monitor *recorder, QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(new TagModelPrivate(this)) +{ + Q_D(TagModel); + d->init(recorder); +} + +TagModel::TagModel(Monitor *recorder, TagModelPrivate *dd, QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(dd) +{ + Q_D(TagModel); + d->init(recorder); +} + +TagModel::~TagModel() +{ + delete d_ptr; +} + +int TagModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid() && parent.column() != 0) { + return 0; + } + + return 1; +} + +int TagModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const TagModel); + + Tag::Id parentTagId = -1; + if (parent.isValid()) { + parentTagId = d->mChildTags[parent.internalId()].at(parent.row()).id(); + } + + return d->mChildTags[parentTagId].count(); +} + +QVariant TagModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return i18n("Tag"); + } + } + + return QAbstractItemModel::headerData(section, orientation, role); +} + +QVariant TagModel::data(const QModelIndex &index, int role) const +{ + Q_D(const TagModel); + + if (!index.isValid()) { + return QVariant(); + } + const Tag tag = d->tagForIndex(index); + if (!tag.isValid()) { + return QVariant(); + } + + switch (role) { + case Qt::DisplayRole: // fall-through + case NameRole: + return tag.name(); + case IdRole: + return tag.id(); + case GIDRole: + return tag.gid(); + case ParentRole: + return QVariant::fromValue(tag.parent()); + case TagRole: + return QVariant::fromValue(tag); + case Qt::DecorationRole: { + TagAttribute *attr = tag.attribute(); + if (attr) { + return QIcon::fromTheme(attr->iconName()); + } else { + return QVariant(); + } + } + } + + return QVariant(); +} + +QModelIndex TagModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_D(const TagModel); + + qint64 parentId = -1; + if (parent.isValid()) { + const Tag parentTag = d->tagForIndex(parent); + parentId = parentTag.id(); + } + + const Tag::List &children = d->mChildTags.value(parentId); + if (row >= children.count()) { + return QModelIndex(); + } + + return createIndex(row, column, (int) parentId); +} + +QModelIndex TagModel::parent(const QModelIndex &child) const +{ + Q_D(const TagModel); + + if (!child.isValid()) { + return QModelIndex(); + } + + const qint64 parentId = child.internalId(); + return d->indexForTag(parentId); +} + +Qt::ItemFlags TagModel::flags(const QModelIndex &index) const +{ + Q_UNUSED(index); + + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable; +} + +bool TagModel::insertColumns(int, int, const QModelIndex &) +{ + return false; +} + +bool TagModel::insertRows(int, int, const QModelIndex &) +{ + return false; +} + +bool TagModel::removeColumns(int, int, const QModelIndex &) +{ + return false; +} + +bool TagModel::removeRows(int, int, const QModelIndex &) +{ + return false; +} + +#include "moc_tagmodel.cpp" diff --git a/src/core/models/tagmodel.h b/src/core/models/tagmodel.h new file mode 100644 index 0000000..6e34b83 --- /dev/null +++ b/src/core/models/tagmodel.h @@ -0,0 +1,97 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_TAGMODEL_H +#define AKONADI_TAGMODEL_H + +#include + +#include "akonadicore_export.h" +#include "tag.h" + +class KJob; + +namespace Akonadi +{ + +class Monitor; +class TagModelPrivate; + +class AKONADICORE_EXPORT TagModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + enum Roles { + IdRole = Qt::UserRole + 1, + NameRole, + TypeRole, + GIDRole, + ParentRole, + TagRole, + + UserRole = Qt::UserRole + 500, + TerminalUserRole = 2000, + EndRole = 65535 + }; + + explicit TagModel(Monitor *recorder, QObject *parent); + virtual ~TagModel(); + + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + + QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + /* + 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 ); + */ + + QModelIndex parent(const QModelIndex &child) const Q_DECL_OVERRIDE; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + +protected: + Q_DECLARE_PRIVATE(TagModel) + TagModelPrivate *d_ptr; + + TagModel(Monitor *recorder, TagModelPrivate *dd, QObject *parent = Q_NULLPTR); + +Q_SIGNALS: + void populated(); + +private: + bool insertRows(int row, int count, const QModelIndex &index = QModelIndex()) Q_DECL_OVERRIDE; + bool insertColumns(int column, int count, const QModelIndex &index = QModelIndex()) Q_DECL_OVERRIDE; + bool removeColumns(int column, int count, const QModelIndex &index = QModelIndex()) Q_DECL_OVERRIDE; + bool removeRows(int row, int count, const QModelIndex &index = QModelIndex()) Q_DECL_OVERRIDE; + + Q_PRIVATE_SLOT(d_func(), void fillModel()) + Q_PRIVATE_SLOT(d_func(), void tagsFetched(const Akonadi::Tag::List &tags)) + Q_PRIVATE_SLOT(d_func(), void tagsFetchDone(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void monitoredTagAdded(const Akonadi::Tag &tag)) + Q_PRIVATE_SLOT(d_func(), void monitoredTagRemoved(const Akonadi::Tag &tag)) + Q_PRIVATE_SLOT(d_func(), void monitoredTagChanged(const Akonadi::Tag &tag)) +}; +} + +#endif // AKONADI_TAGMODEL_H diff --git a/src/core/models/tagmodel_p.cpp b/src/core/models/tagmodel_p.cpp new file mode 100644 index 0000000..2fa9ff3 --- /dev/null +++ b/src/core/models/tagmodel_p.cpp @@ -0,0 +1,247 @@ +/* + Copyright (c) 2014 Daniel Vr??til + + 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 "tagmodel_p.h" +#include "tagmodel.h" + +#include "monitor.h" +#include "session.h" +#include "tagfetchjob.h" + +#include "akonadicore_debug.h" + +#include + +using namespace Akonadi; + +TagModelPrivate::TagModelPrivate(TagModel *parent) + : mMonitor(0) + , mSession(0) + , q_ptr(parent) +{ + // Root tag + mTags.insert(-1, Tag()); +} + +TagModelPrivate::~TagModelPrivate() +{ +} + +void TagModelPrivate::init(Monitor *monitor) +{ + Q_Q(TagModel); + + mMonitor = monitor; + mSession = mMonitor->session(); + + q->connect(mMonitor, SIGNAL(tagAdded(Akonadi::Tag)), + q, SLOT(monitoredTagAdded(Akonadi::Tag))); + q->connect(mMonitor, SIGNAL(tagChanged(Akonadi::Tag)), + q, SLOT(monitoredTagChanged(Akonadi::Tag))); + q->connect(mMonitor, SIGNAL(tagRemoved(Akonadi::Tag)), + q, SLOT(monitoredTagRemoved(Akonadi::Tag))); + + // Delay starting the job to allow unit-tests to set up fake stuff + QTimer::singleShot(0, q, SLOT(fillModel())); +} + +void TagModelPrivate::fillModel() +{ + Q_Q(TagModel); + + TagFetchJob *fetchJob = new TagFetchJob(mSession); + fetchJob->setFetchScope(mMonitor->tagFetchScope()); + q->connect(fetchJob, SIGNAL(tagsReceived(Akonadi::Tag::List)), + q, SLOT(tagsFetched(Akonadi::Tag::List))); + q->connect(fetchJob, SIGNAL(finished(KJob*)), + q, SLOT(tagsFetchDone(KJob*))); +} + +QModelIndex TagModelPrivate::indexForTag(const qint64 tagId) const +{ + Q_Q(const TagModel); + + if (!mTags.contains(tagId)) { + return QModelIndex(); + } + + const Tag tag = mTags.value(tagId); + if (!tag.isValid()) { + return QModelIndex(); + } + + const Tag::Id parentId = tag.parent().id(); + const int row = mChildTags.value(parentId).indexOf(tag); + if (row != -1) { + return q->createIndex(row, 0, (int) parentId); + } + + return QModelIndex(); +} + +Tag TagModelPrivate::tagForIndex(const QModelIndex &index) const +{ + if (!index.isValid()) { + return Tag(); + } + + const Tag::Id parentId = index.internalId(); + const Tag::List &children = mChildTags.value(parentId); + return children.at(index.row()); +} + +void TagModelPrivate::monitoredTagAdded(const Tag &tag) +{ + Q_Q(TagModel); + + const qint64 parentId = tag.parent().id(); + + // Parent not yet in model, defer for later + if (!mTags.contains(parentId)) { + Tag::List &list = mPendingTags[parentId]; + list.append(tag); + return; + } + + Tag::List &children = mChildTags[parentId]; + + q->beginInsertRows(indexForTag(parentId), children.count(), children.count()); + mTags.insert(tag.id(), tag); + children.append(tag); + q->endInsertRows(); + + // If there are any child tags waiting for this parent, insert them + if (mPendingTags.contains(tag.id())) { + const Tag::List pendingChildren = mPendingTags.take(tag.id()); + Q_FOREACH (const Tag &pendingTag, pendingChildren) { + monitoredTagAdded(pendingTag); + } + } +} + +void TagModelPrivate::removeTagsRecursively(qint64 tagId) +{ + const Tag tag = mTags.value(tagId); + + // Remove all children first + const Tag::List childTags = mChildTags.take(tagId); + Q_FOREACH (const Tag &child, childTags) { + removeTagsRecursively(child.id()); + } + + // Remove the actual tag + Tag::List &siblings = mChildTags[tag.parent().id()]; + siblings.removeOne(tag); + mTags.remove(tag.id()); +} + +void TagModelPrivate::monitoredTagRemoved(const Tag &tag) +{ + Q_Q(TagModel); + + if (!tag.isValid()) { + qCWarning(AKONADICORE_LOG) << "Attempting to remove root tag?"; + return; + } + + // Better lookup parent in our cache + auto iter = mTags.constFind(tag.id()); + if (iter == mTags.cend()) { + qCWarning(AKONADICORE_LOG) << "Got removal notification for unknown tag" << tag.id(); + return; + } + + const qint64 parentId = iter->parent().id(); + + const Tag::List &siblings = mChildTags[parentId]; + const int pos = siblings.indexOf(tag); + Q_ASSERT(pos != -1); + + q->beginRemoveRows(indexForTag(parentId), pos, pos); + removeTagsRecursively(tag.id()); + q->endRemoveRows(); +} + +void TagModelPrivate::monitoredTagChanged(const Tag &tag) +{ + Q_Q(TagModel); + + if (!mTags.contains(tag.id())) { + qCWarning(AKONADICORE_LOG) << "Got change notifications for unknown tag" << tag.id(); + return; + } + + const Tag oldTag = mTags.value(tag.id()); + // Replace existing tag in cache + mTags.insert(tag.id(), tag); + + // Check whether the tag has been reparented + const qint64 oldParent = oldTag.parent().id(); + const qint64 newParent = tag.parent().id(); + if (oldParent != newParent) { + const QModelIndex sourceParent = indexForTag(oldParent); + const int sourcePos = mChildTags.value(oldParent).indexOf(oldTag); + const QModelIndex destParent = indexForTag(newParent); + const int destPos = mChildTags.value(newParent).count(); + + q->beginMoveRows(sourceParent, sourcePos, sourcePos, destParent, destPos); + Tag::List &oldSiblings = mChildTags[oldParent]; + oldSiblings.removeAt(sourcePos); + Tag::List &newSiblings = mChildTags[newParent]; + newSiblings.append(tag); + q->endMoveRows(); + } else { + Tag::List &children = mChildTags[oldParent]; + const int sourcePos = children.indexOf(oldTag); + if (sourcePos != -1) { + children[sourcePos] = tag; + } + + const QModelIndex index = indexForTag(tag.id()); + q->dataChanged(index, index); + } +} + +void TagModelPrivate::tagsFetched(const Tag::List &tags) +{ + Q_FOREACH (const Tag &tag, tags) { + monitoredTagAdded(tag); + } +} + +void TagModelPrivate::tagsFetchDone(KJob *job) +{ + Q_Q(TagModel); + + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; + } + + if (!mPendingTags.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Fetched all tags from server, but there are still" << mPendingTags.count() << "orphan tags:"; + for (auto it = mPendingTags.cbegin(), e = mPendingTags.cend(); it != e; ++it) { + qCWarning(AKONADICORE_LOG) << "tagId = " << it.key() << "; with list count =" << it.value().count(); + } + + return; + } + + emit q->populated(); +} diff --git a/src/core/models/tagmodel_p.h b/src/core/models/tagmodel_p.h new file mode 100644 index 0000000..bd5f3f0 --- /dev/null +++ b/src/core/models/tagmodel_p.h @@ -0,0 +1,70 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_TAGMODELPRIVATE_H +#define AKONADI_TAGMODELPRIVATE_H + +#include "tag.h" + +class QModelIndex; +class KJob; + +namespace Akonadi +{ + +class Monitor; +class TagModel; +class Session; + +class TagModelPrivate +{ +public: + explicit TagModelPrivate(TagModel *parent); + virtual ~TagModelPrivate(); + + void init(Monitor *recorder); + void fillModel(); + + void tagsFetchDone(KJob *job); + void tagsFetched(const Akonadi::Tag::List &tags); + void monitoredTagAdded(const Akonadi::Tag &tag); + void monitoredTagChanged(const Akonadi::Tag &tag); + void monitoredTagRemoved(const Akonadi::Tag &tag); + + QModelIndex indexForTag(qint64 tagId) const; + Tag tagForIndex(const QModelIndex &index) const; + + void removeTagsRecursively(qint64 parentTag); + + Monitor *mMonitor; + Session *mSession; + + QHash mChildTags; + QHash mTags; + + QHash mPendingTags; + +protected: + Q_DECLARE_PUBLIC(TagModel) + TagModel *q_ptr; + +}; +} + +#endif // AKONADI_TAGMODELPRIVATE_H diff --git a/src/core/models/trashfilterproxymodel.cpp b/src/core/models/trashfilterproxymodel.cpp new file mode 100644 index 0000000..bcf50ac --- /dev/null +++ b/src/core/models/trashfilterproxymodel.cpp @@ -0,0 +1,78 @@ +/* + Copyright (c) 2011 Christian Mollekopf + + 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 "trashfilterproxymodel.h" +#include "entitydeletedattribute.h" +#include "item.h" +#include "entitytreemodel.h" + +using namespace Akonadi; + +class TrashFilterProxyModel::TrashFilterProxyModelPrivate +{ +public: + TrashFilterProxyModelPrivate() + : mTrashIsShown(false) + { + }; + bool mTrashIsShown; +}; + +TrashFilterProxyModel::TrashFilterProxyModel(QObject *parent) + : KRecursiveFilterProxyModel(parent) + , d_ptr(new TrashFilterProxyModelPrivate()) +{ + +} + +TrashFilterProxyModel::~TrashFilterProxyModel() +{ + delete d_ptr; +} + +void TrashFilterProxyModel::showTrash(bool enable) +{ + Q_D(TrashFilterProxyModel); + d->mTrashIsShown = enable; + invalidateFilter(); +} + +bool TrashFilterProxyModel::trashIsShown() const +{ + Q_D(const TrashFilterProxyModel); + return d->mTrashIsShown; +} + +bool TrashFilterProxyModel::acceptRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_D(const TrashFilterProxyModel); + const QModelIndex &index = sourceModel()->index(sourceRow, 0, sourceParent); + const Item &item = index.data(EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + if (item.hasAttribute()) { + return d->mTrashIsShown; + } + } + const Collection &collection = index.data(EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + if (collection.hasAttribute()) { + return d->mTrashIsShown; + } + } + return !d->mTrashIsShown; +} diff --git a/src/core/models/trashfilterproxymodel.h b/src/core/models/trashfilterproxymodel.h new file mode 100644 index 0000000..d6f78d3 --- /dev/null +++ b/src/core/models/trashfilterproxymodel.h @@ -0,0 +1,83 @@ +/* + Copyright (c) 2011 Christian Mollekopf + + 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_TRASHFILTERPROXYMODEL_H +#define AKONADI_TRASHFILTERPROXYMODEL_H + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ + +/** + * @short Filter model which hides/shows entites marked as trash + * + * Filter model which either hides all entities marked as trash, or the ones not marked. + * Subentities of collections marked as trash are also shown in the trash. + * + * The Base model must be an EntityTreeModel and the EntityDeletedAttribute must be available. + * + * Example: + * + * @code + * + * ChangeRecorder *monitor = new Akonadi::ChangeRecorder( this ); + * monitor->itemFetchScope().fetchAttribute(true); + * + * Akonadi::EntityTreeModel *sourcemodel = new Akonadi::EntityTreeModel(monitor, this); + * + * TrashFilterProxyModel *model = new TrashFilterProxyModel(this); + * model->setDynamicSortFilter(true); + * model->setSourceModel(sourcemodel); + * + * @endcode + * + * @author Christian Mollekopf + * @since 4.8 + */ +class AKONADICORE_EXPORT TrashFilterProxyModel : public KRecursiveFilterProxyModel +{ + Q_OBJECT + +public: + explicit TrashFilterProxyModel(QObject *parent = Q_NULLPTR); + virtual ~TrashFilterProxyModel(); + + void showTrash(bool enable); + bool trashIsShown() const; + +protected: + /** + * Sort filter criterias, according to how expensive the operation is + */ + bool acceptRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class TrashFilterProxyModelPrivate; + TrashFilterProxyModelPrivate *const d_ptr; + Q_DECLARE_PRIVATE(TrashFilterProxyModel) + //@endcond +}; + +} + +#endif diff --git a/src/core/monitor.cpp b/src/core/monitor.cpp new file mode 100644 index 0000000..258cbef --- /dev/null +++ b/src/core/monitor.cpp @@ -0,0 +1,388 @@ +/* + 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 "monitor.h" +#include "monitor_p.h" + +#include "changemediator_p.h" +#include "collectionfetchscope.h" +#include "itemfetchjob.h" +#include "session.h" + +#include + +#include +#include + +#include +#include + +using namespace Akonadi; + +Monitor::Monitor(QObject *parent) + : QObject(parent) + , d_ptr(new MonitorPrivate(0, this)) +{ + d_ptr->init(); + d_ptr->connectToNotificationManager(); +} + +//@cond PRIVATE +Monitor::Monitor(MonitorPrivate *d, QObject *parent) + : QObject(parent) + , d_ptr(d) +{ + d_ptr->init(); + d_ptr->connectToNotificationManager(); + + ChangeMediator::registerMonitor(this); +} +//@endcond + +Monitor::~Monitor() +{ + ChangeMediator::unregisterMonitor(this); + + delete d_ptr; +} + +void Monitor::setCollectionMonitored(const Collection &collection, bool monitored) +{ + Q_D(Monitor); + if (!d->collections.contains(collection) && monitored) { + d->collections << collection; + if (d->notificationSource) { + d->notificationSource->setMonitoredCollection(collection.id(), true); + } + } else if (!monitored) { + if (d->collections.removeAll(collection)) { + d->cleanOldNotifications(); + if (d->notificationSource) { + d->notificationSource->setMonitoredCollection(collection.id(), false); + } + } + } + + emit collectionMonitored(collection, monitored); +} + +void Monitor::setItemMonitored(const Item &item, bool monitored) +{ + Q_D(Monitor); + if (!d->items.contains(item.id()) && monitored) { + d->items.insert(item.id()); + if (d->notificationSource) { + d->notificationSource->setMonitoredItem(item.id(), true); + } + } else if (!monitored) { + if (d->items.remove(item.id())) { + d->cleanOldNotifications(); + if (d->notificationSource) { + d->notificationSource->setMonitoredItem(item.id(), false); + } + } + } + + emit itemMonitored(item, monitored); +} + +void Monitor::setResourceMonitored(const QByteArray &resource, bool monitored) +{ + Q_D(Monitor); + if (!d->resources.contains(resource) && monitored) { + d->resources.insert(resource); + if (d->notificationSource) { + d->notificationSource->setMonitoredResource(resource, true); + } + } else if (!monitored) { + if (d->resources.remove(resource)) { + d->cleanOldNotifications(); + if (d->notificationSource) { + d->notificationSource->setMonitoredResource(resource, false); + } + } + } + + emit resourceMonitored(resource, monitored); +} + +void Monitor::setMimeTypeMonitored(const QString &mimetype, bool monitored) +{ + Q_D(Monitor); + if (!d->mimetypes.contains(mimetype) && monitored) { + d->mimetypes.insert(mimetype); + if (d->notificationSource) { + d->notificationSource->setMonitoredMimeType(mimetype, true); + } + } else if (!monitored) { + if (d->mimetypes.remove(mimetype)) { + d->cleanOldNotifications(); + if (d->notificationSource) { + d->notificationSource->setMonitoredMimeType(mimetype, false); + } + } + } + + emit mimeTypeMonitored(mimetype, monitored); +} + +void Monitor::setTagMonitored(const Akonadi::Tag &tag, bool monitored) +{ + Q_D(Monitor); + if (!d->tags.contains(tag.id()) && monitored) { + d->tags.insert(tag.id()); + if (d->notificationSource) { + d->notificationSource->setMonitoredTag(tag.id(), true); + } + } else if (!monitored) { + if (d->tags.remove(tag.id())) { + d->cleanOldNotifications(); + if (d->notificationSource) { + d->notificationSource->setMonitoredTag(tag.id(), false); + } + } + } + + emit tagMonitored(tag, monitored); +} + +void Monitor::setTypeMonitored(Monitor::Type type, bool monitored) +{ + Q_D(Monitor); + if (!d->types.contains(type) && monitored) { + d->types.insert(type); + if (d->notificationSource) { + d->notificationSource->setMonitoredType(static_cast(type), true); + } + } else if (!monitored) { + if (d->types.remove(type)) { + d->cleanOldNotifications(); + if (d->notificationSource) { + d->notificationSource->setMonitoredType(static_cast(type), false); + } + } + } + + emit typeMonitored(type, monitored); +} + +void Akonadi::Monitor::setAllMonitored(bool monitored) +{ + Q_D(Monitor); + if (d->monitorAll == monitored) { + return; + } + + d->monitorAll = monitored; + + if (!monitored) { + d->cleanOldNotifications(); + } + + if (d->notificationSource) { + d->notificationSource->setAllMonitored(monitored); + } + + emit allMonitored(monitored); +} + +void Monitor::setExclusive(bool exclusive) +{ + Q_D(Monitor); + d->exclusive = exclusive; + if (d->notificationSource) { + d->notificationSource->setExclusive(exclusive); + } +} + +bool Monitor::exclusive() const +{ + Q_D(const Monitor); + return d->exclusive; +} + +void Monitor::ignoreSession(Session *session) +{ + Q_D(Monitor); + + if (!d->sessions.contains(session->sessionId())) { + d->sessions << session->sessionId(); + connect(session, SIGNAL(destroyed(QObject*)), this, SLOT(slotSessionDestroyed(QObject*))); + if (d->notificationSource) { + d->notificationSource->setIgnoredSession(session->sessionId(), true); + } + } +} + +void Monitor::fetchCollection(bool enable) +{ + Q_D(Monitor); + d->fetchCollection = enable; +} + +void Monitor::fetchCollectionStatistics(bool enable) +{ + Q_D(Monitor); + d->fetchCollectionStatistics = enable; +} + +void Monitor::setItemFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(Monitor); + d->mItemFetchScope = fetchScope; +} + +ItemFetchScope &Monitor::itemFetchScope() +{ + Q_D(Monitor); + return d->mItemFetchScope; +} + +void Monitor::fetchChangedOnly(bool enable) +{ + Q_D(Monitor); + d->mFetchChangedOnly = enable; +} + +void Monitor::setCollectionFetchScope(const CollectionFetchScope &fetchScope) +{ + Q_D(Monitor); + d->mCollectionFetchScope = fetchScope; +} + +CollectionFetchScope &Monitor::collectionFetchScope() +{ + Q_D(Monitor); + return d->mCollectionFetchScope; +} + +void Monitor::setTagFetchScope(const TagFetchScope &fetchScope) +{ + Q_D(Monitor); + d->mTagFetchScope = fetchScope; +} + +TagFetchScope &Monitor::tagFetchScope() +{ + Q_D(Monitor); + return d->mTagFetchScope; +} + +Akonadi::Collection::List Monitor::collectionsMonitored() const +{ + Q_D(const Monitor); + return d->collections; +} + +QVector Monitor::itemsMonitoredEx() const +{ + Q_D(const Monitor); + QVector result; + result.reserve(d->items.size()); + qCopy(d->items.begin(), d->items.end(), std::back_inserter(result)); + return result; +} + +int Monitor::numItemsMonitored() const +{ + Q_D(const Monitor); + return d->items.size(); +} + +QVector Monitor::tagsMonitored() const +{ + Q_D(const Monitor); + QVector result; + result.reserve(d->tags.size()); + qCopy(d->tags.begin(), d->tags.end(), std::back_inserter(result)); + return result; +} + +QVector Monitor::typesMonitored() const +{ + Q_D(const Monitor); + QVector result; + result.reserve(d->types.size()); + qCopy(d->types.begin(), d->types.end(), std::back_inserter(result)); + return result; +} + +QStringList Monitor::mimeTypesMonitored() const +{ + Q_D(const Monitor); + return d->mimetypes.toList(); +} + +int Monitor::numMimeTypesMonitored() const +{ + Q_D(const Monitor); + return d->mimetypes.count(); +} + +QList Monitor::resourcesMonitored() const +{ + Q_D(const Monitor); + return d->resources.toList(); +} + +int Monitor::numResourcesMonitored() const +{ + Q_D(const Monitor); + return d->resources.count(); +} + +bool Monitor::isAllMonitored() const +{ + Q_D(const Monitor); + return d->monitorAll; +} + +void Monitor::setSession(Akonadi::Session *session) +{ + Q_D(Monitor); + if (session == d->session) { + return; + } + + if (!session) { + d->session = Session::defaultSession(); + } else { + d->session = session; + } + + d->itemCache->setSession(d->session); + d->collectionCache->setSession(d->session); + if (d->notificationSource) { + d->notificationSource->setSession(d->session->sessionId()); + } +} + +Session *Monitor::session() const +{ + Q_D(const Monitor); + return d->session; +} + +void Monitor::setCollectionMoveTranslationEnabled(bool enabled) +{ + Q_D(Monitor); + d->collectionMoveTranslationEnabled = enabled; +} + +#include "moc_monitor.cpp" diff --git a/src/core/monitor.h b/src/core/monitor.h new file mode 100644 index 0000000..a15ec84 --- /dev/null +++ b/src/core/monitor.h @@ -0,0 +1,764 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_MONITOR_H +#define AKONADI_MONITOR_H + +#include "akonadicore_export.h" +#include "tag.h" +#include "collection.h" +#include "item.h" +#include "relation.h" + +#include + +namespace Akonadi +{ + +class CollectionFetchScope; +class CollectionStatistics; +class Item; +class ItemFetchScope; +class MonitorPrivate; +class Session; +class TagFetchScope; + +namespace Protocol +{ +class ChangeNotification; +} + +/** + * @short Monitors an item or collection for changes. + * + * The Monitor emits signals if some of these objects are changed or + * removed or new ones are added to the Akonadi storage. + * + * There are various ways to filter these notifications. There are three types of filter + * evaluation: + * - (-) removal-only filter, ie. if the filter matches the notification is dropped, + * if not filter evaluation continues with the next one + * - (+) pass-exit filter, ie. if the filter matches the notification is delivered, + * if not evaluation is continued + * - (f) final filter, ie. evaluation ends here if the corresponding filter criteria is set, + * the notification is delievered depending on the result, evaluation is only continued + * if no filter criteria is defined + * + * The following filter are available, listed in evaluation order: + * (1) ignored sessions (-) + * (2) monitor everything (+) + * (3a) resource and mimetype filters (f) (items only) + * (3b) resource filters (f) (collections only) + * (4) item is monitored (+) + * (5) collection is monitored (+) + * + * Optionally, the changed objects can be fetched automatically from the server. + * To enable this, see itemFetchScope() and collectionFetchScope(). + * + * Note that as a consequence of rule 3a, it is not possible to monitor (more than zero resources + * OR more than zero mimetypes) AND more than zero collections. + * + * @todo Distinguish between monitoring collection properties and collection content. + * @todo Special case for collection content counts changed + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT Monitor : public QObject +{ + Q_OBJECT + +public: + enum Type { + /** + * @internal This must be kept in sync with Akonadi::NotificationMessageV2::Type + */ + Collections = 1, + Items, + Tags, + Relations + }; + + /** + * Creates a new monitor. + * + * @param parent The parent object. + */ + explicit Monitor(QObject *parent = Q_NULLPTR); + + /** + * Destroys the monitor. + */ + virtual ~Monitor(); + + /** + * Sets whether the specified collection shall be monitored for changes. If + * monitoring is turned on for the collection, all notifications for items + * in that collection will be emitted, and its child collections will also + * be monitored. Note that move notifications will be emitted if either one + * of the collections involved is being monitored. + * + * Note that if a session is being ignored, this takes precedence over + * setCollectionMonitored() on that session. + * + * @param collection The collection to monitor. + * If this collection is Collection::root(), all collections + * in the Akonadi storage will be monitored. + * @param monitored Whether to monitor the collection. + */ + void setCollectionMonitored(const Collection &collection, bool monitored = true); + + /** + * Sets whether the specified item shall be monitored for changes. + * + * Note that if a session is being ignored, this takes precedence over + * setItemMonitored() on that session. + * + * @param item The item to monitor. + * @param monitored Whether to monitor the item. + */ + void setItemMonitored(const Item &item, bool monitored = true); + + /** + * Sets whether the specified resource shall be monitored for changes. If + * monitoring is turned on for the resource, all notifications for + * collections and items in that resource will be emitted. + * + * Note that if a session is being ignored, this takes precedence over + * setResourceMonitored() on that session. + * + * @param resource The resource identifier. + * @param monitored Whether to monitor the resource. + */ + void setResourceMonitored(const QByteArray &resource, bool monitored = true); + + /** + * Sets whether items of the specified mime type shall be monitored for changes. + * If monitoring is turned on for the mime type, all notifications for items + * matching that mime type will be emitted, but notifications for collections + * matching that mime type will only be emitted if this is otherwise specified, + * for example by setCollectionMonitored(). + * + * Note that if a session is being ignored, this takes precedence over + * setMimeTypeMonitored() on that session. + * + * @param mimetype The mime type to monitor. + * @param monitored Whether to monitor the mime type. + */ + void setMimeTypeMonitored(const QString &mimetype, bool monitored = true); + + /** + * Sets whether the specified tag shall be monitored for changes. + * + * Same rules as for item monitoring apply. + * + * @param tag Tag to monitor. + * @param monitored Whether to monitor the tag. + * @since 4.13 + */ + void setTagMonitored(const Tag &tag, bool monitored = true); + + /** + * Sets whether given type (Collection, Item, Tag should be monitored). + * + * By default all types are monitored, but once you change one, you have + * to explicitly enable all other types you want to monitor. + * + * @param type Type to monitor. + * @param monitored Whether to monitor the type + * @since 4.13 + */ + void setTypeMonitored(Type type, bool monitored = true); + + /** + * Sets whether all items shall be monitored. + * @param monitored sets all items as monitored if set as @c true + * Note that if a session is being ignored, this takes precedence over + * setAllMonitored() on that session. + */ + void setAllMonitored(bool monitored = true); + + void setExclusive(bool exclusive = true); + bool exclusive() const; + + /** + * Ignores all change notifications caused by the given session. This + * overrides all other settings on this session. + * + * @param session The session you want to ignore. + */ + void ignoreSession(Session *session); + + /** + * Enables automatic fetching of changed collections from the Akonadi storage. + * + * @param enable @c true enables automatic fetching, @c false disables automatic fetching. + */ + void fetchCollection(bool enable); + + /** + * Enables automatic fetching of changed collection statistics information from + * the Akonadi storage. + * + * @param enable @c true to enables automatic fetching, @c false disables automatic fetching. + */ + void fetchCollectionStatistics(bool enable); + + /** + * Sets the item fetch scope. + * + * Controls how much of an item's data is fetched from the server, e.g. + * whether to fetch the full item payload or only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see itemFetchScope() + */ + void setItemFetchScope(const ItemFetchScope &fetchScope); + + /** + * Instructs the monitor to fetch only those parts that were changed and + * were requested in the fetch scope. + * + * This is taken in account only for item modifications. + * Example usage: + * @code + * monitor->itemFetchScope().fetchFullPayload( true ); + * monitor->fetchChangedOnly(true); + * @endcode + * + * In the example if an item was changed, but its payload was not, the full + * payload will not be retrieved. + * If the item's payload was changed, the monitor retrieves the changed + * payload as well. + * + * The default is to fetch everything requested. + * + * @since 4.8 + * + * @param enable @c true to enable the feature, @c false means everything + * that was requested will be fetched. + * @return void + */ + void fetchChangedOnly(bool enable); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setItemFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &itemFetchScope(); + + /** + * Sets the collection fetch scope. + * + * Controls which collections are monitored and how much of a collection's data + * is fetched from the server. + * + * @param fetchScope The new scope for collection fetch operations. + * + * @see collectionFetchScope() + * @since 4.4 + */ + void setCollectionFetchScope(const CollectionFetchScope &fetchScope); + + /** + * Returns the collection fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the CollectionFetchScope documentation + * for an example. + * + * @return a reference to the current collection fetch scope + * + * @see setCollectionFetchScope() for replacing the current collection fetch scope + * @since 4.4 + */ + CollectionFetchScope &collectionFetchScope(); + + /** + * Sets the tag fetch scope. + * + * Controls how much of an tag's data is fetched from the server. + * + * @param fetchScope The new scope for tag fetch operations. + * + * @see tagFetchScope() + */ + void setTagFetchScope(const TagFetchScope &fetchScope); + + /** + * Returns the tag fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. + * + * @return a reference to the current tag fetch scope + * + * @see setTagFetchScope() for replacing the current tag fetch scope + */ + TagFetchScope &tagFetchScope(); + + /** + * Returns the list of collections being monitored. + * + * @since 4.3 + */ + Collection::List collectionsMonitored() const; + + /** + * Returns the set of items being monitored. + * + * Faster version (at least on 32-bit systems) of itemsMonitored(). + * + * @since 4.6 + */ + QVector itemsMonitoredEx() const; + + /** + * Returns the number of items being monitored. + * Optimization. + * @since 4.14.3 + */ + int numItemsMonitored() const; + + /** + * Returns the set of mimetypes being monitored. + * + * @since 4.3 + */ + QStringList mimeTypesMonitored() const; + + /** + * Returns the number of mimetypes being monitored. + * Optimization. + * @since 4.14.3 + */ + int numMimeTypesMonitored() const; + + /** + * Returns the set of tags being monitored. + * + * @since 4.13 + */ + QVector tagsMonitored() const; + + /** + * Returns the set of types being monitored. + * + * @since 4.13 + */ + QVector typesMonitored() const; + + /** + * Returns the set of identifiers for resources being monitored. + * + * @since 4.3 + */ + QList resourcesMonitored() const; + + /** + * Returns the number of resources being monitored. + * Optimization. + * @since 4.14.3 + */ + int numResourcesMonitored() const; + + /** + * Returns true if everything is being monitored. + * + * @since 4.3 + */ + bool isAllMonitored() const; + + /** + * Sets the session used by the Monitor to communicate with the %Akonadi server. + * If not set, the Akonadi::Session::defaultSession is used. + * @param session the session to be set + * @since 4.4 + */ + void setSession(Akonadi::Session *session); + + /** + * Returns the Session used by the monitor to communicate with Akonadi. + * + * @since 4.4 + */ + Session *session() const; + + /** + * Allows to enable/disable collection move translation. If enabled (the default), move + * notifications are automatically translated into add/remove notifications if the source/destination + * is outside of the monitored collection hierarchy. + * @param enabled enables collection move translation if set as @c true + * @since 4.9 + */ + void setCollectionMoveTranslationEnabled(bool enabled); + +Q_SIGNALS: + /** + * This signal is emitted if a monitored item has changed, e.g. item parts have been modified. + * + * @param item The changed item. + * @param partIdentifiers The identifiers of the item parts that has been changed. + */ + void itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers); + + /** + * This signal is emitted if flags of monitored items have changed. + * + * @param items Items that were changed + * @param addedFlags Flags that have been added to each item in @p items + * @param removedFlags Flags that have been removed from each item in @p items + * @since 4.11 + */ + void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, + const QSet &removedFlags); + + /** + * This signal is emitted if tags of monitored items have changed. + * + * @param items Items that were changed + * @param addedTags Tags that have been added to each item in @p items. + * @param removedTags Tags that have been removed from each item in @p items + * @since 4.13 + */ + void itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, + const QSet &removedTags); + + /** + * This signal is emitted if relations of monitored items have changed. + * + * @param items Items that were changed + * @param addedRelations Relations that have been added to each item in @p items. + * @param removedRelations Relations that have been removed from each item in @p items + * @since 4.15 + */ + void itemsRelationsChanged(const Akonadi::Item::List &items, const Akonadi::Relation::List &addedRelations, + const Akonadi::Relation::List &removedRelations); + + /** + * This signal is emitted if a monitored item has been moved between two collections + * + * @param item The moved item. + * @param collectionSource The collection the item has been moved from. + * @param collectionDestination The collection the item has been moved to. + */ + void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource, + const Akonadi::Collection &collectionDestination); + + /** + * This is signal is emitted when multiple monitored items have been moved between two collections + * + * @param items Moved items + * @param collectionSource The collection the items have been moved from. + * @param collectionDestination The collection the items have been moved to. + * + * @since 4.11 + */ + void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, + const Akonadi::Collection &collectionDestination); + + /** + * This signal is emitted if an item has been added to a monitored collection in the Akonadi storage. + * + * @param item The new item. + * @param collection The collection the item has been added to. + */ + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * This signal is emitted if + * - a monitored item has been removed from the Akonadi storage + * or + * - a item has been removed from a monitored collection. + * + * @param item The removed item. + */ + void itemRemoved(const Akonadi::Item &item); + + /** + * This signal is emitted if monitored items have been removed from Akonadi + * storage of items have been removed from a monitored collection. + * + * @param items Removed items + * + * @since 4.11 + */ + void itemsRemoved(const Akonadi::Item::List &items); + + /** + * This signal is emitted if a reference to an item is added to a virtual collection. + * @param item The linked item. + * @param collection The collection the item is linked to. + * + * @since 4.2 + */ + void itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * This signal is emitted if a reference to multiple items is added to a virtual collection + * + * @param items The linked items + * @param collection The collections the items are linked to + * + * @since 4.11 + */ + void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + + /** + * This signal is emitted if a reference to an item is removed from a virtual collection. + * @param item The unlinked item. + * @param collection The collection the item is unlinked from. + * + * @since 4.2 + */ + void itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * This signal is emitted if a refernece to items is removed from a virtual collection + * + * @param items The unlinked items + * @param collection The collections the items are unlinked from + * + * @since 4.11 + */ + void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + + /** + * This signal is emitted if a new collection has been added to a monitored collection in the Akonadi storage. + * + * @param collection The new collection. + * @param parent The parent collection. + */ + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + + /** + * This signal is emitted if a monitored collection has been changed (properties or content). + * + * @param collection The changed collection. + */ + void collectionChanged(const Akonadi::Collection &collection); + + /** + * This signal is emitted if a monitored collection has been changed (properties or attributes). + * + * @param collection The changed collection. + * @param attributeNames The names of the collection attributes that have been changed. + * + * @since 4.4 + */ + void collectionChanged(const Akonadi::Collection &collection, const QSet &attributeNames); + + /** + * This signals is emitted if a monitored collection has been moved. + * + * @param collection The moved collection. + * @param source The previous parent collection. + * @param destination The new parent collection. + * + * @since 4.4 + */ + void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination); + + /** + * This signal is emitted if a monitored collection has been removed from the Akonadi storage. + * + * @param collection The removed collection. + */ + void collectionRemoved(const Akonadi::Collection &collection); + + /** + * This signal is emitted if a collection has been subscribed to by the user. + * It will be emitted even for unmonitored collections as the check for whether to + * monitor it has not been applied yet. + * + * @param collection The subscribed collection + * @param parent The parent collection of the subscribed collection. + * + * @since 4.6 + */ + void collectionSubscribed(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + + /** + * This signal is emitted if a user unsubscribes from a collection. + * + * @param collection The unsubscribed collection + * + * @since 4.6 + */ + void collectionUnsubscribed(const Akonadi::Collection &collection); + + /** + * This signal is emitted if the statistics information of a monitored collection + * has changed. + * + * @param id The collection identifier of the changed collection. + * @param statistics The updated collection statistics, invalid if automatic + * fetching of statistics changes is disabled. + */ + void collectionStatisticsChanged(Akonadi::Collection::Id id, + const Akonadi::CollectionStatistics &statistics); + + /** + * This signal is emitted if a tag has been added to Akonadi storage. + * + * @param tag The added tag + * @since 4.13 + */ + void tagAdded(const Akonadi::Tag &tag); + + /** + * This signal is emitted if a monitored tag is changed on the server. + * + * @param tag The changed tag. + * @since 4.13 + */ + void tagChanged(const Akonadi::Tag &tag); + + /** + * This signal is emitted if a monitored tag is removed from the server storage. + * + * The monitor will also emit itemTagsChanged() signal for all monitored items + * (if any) that were tagged by @p tag. + * + * @param tag The removed tag. + * @since 4.13 + */ + void tagRemoved(const Akonadi::Tag &tag); + + /** + * This signal is emitted if a relation has been added to Akonadi storage. + * + * The monitor will also emit itemRelationsChanged() signal for all monitored items + * hat are affected by @p relation. + * + * @param relation The added relation + * @since 4.13 + */ + void relationAdded(const Akonadi::Relation &relation); + + /** + * This signal is emitted if a monitored relation is removed from the server storage. + * + * The monitor will also emit itemRelationsChanged() signal for all monitored items + * that were affected by @p relation. + * + * @param relation The removed relation. + * @since 4.13 + */ + void relationRemoved(const Akonadi::Relation &relation); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p collection explicitly. + * @param collection The collection + * @param monitored Whether the collection is now being monitored or not. + * + * @since 4.3 + */ + void collectionMonitored(const Akonadi::Collection &collection, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p item explicitly. + * @param item The item + * @param monitored Whether the item is now being monitored or not. + * + * @since 4.3 + */ + void itemMonitored(const Akonadi::Item &item, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring the resource with the identifier @p identifier explicitly. + * @param identifier The identifier of the resource. + * @param monitored Whether the resource is now being monitored or not. + * + * @since 4.3 + */ + void resourceMonitored(const QByteArray &identifier, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p mimeType explicitly. + * @param mimeType The mimeType. + * @param monitored Whether the mimeType is now being monitored or not. + * + * @since 4.3 + */ + void mimeTypeMonitored(const QString &mimeType, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring everything. + * @param monitored Whether everything is now being monitored or not. + * + * @since 4.3 + */ + void allMonitored(bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p tag explicitly. + * @param tag The tag. + * @param monitored Whether the tag is now being monitored or not. + * @since 4.13 + */ + void tagMonitored(const Akonadi::Tag &tag, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p type explicitly + * @param type The type. + * @param monitored Whether the type is now being monitored or not. + * @since 4.13 + */ + void typeMonitored(const Akonadi::Monitor::Type type, bool monitored); + +protected: + //@cond PRIVATE + friend class EntityTreeModel; + friend class EntityTreeModelPrivate; + MonitorPrivate *d_ptr; + explicit Monitor(MonitorPrivate *d, QObject *parent = Q_NULLPTR); + //@endcond + +private: + Q_DECLARE_PRIVATE(Monitor) + + //@cond PRIVATE + Q_PRIVATE_SLOT(d_ptr, void slotSessionDestroyed(QObject *)) + Q_PRIVATE_SLOT(d_ptr, void slotStatisticsChangedFinished(KJob *)) + Q_PRIVATE_SLOT(d_ptr, void slotFlushRecentlyChangedCollections()) + Q_PRIVATE_SLOT(d_ptr, void slotNotify(const Akonadi::Protocol::ChangeNotification &)) + Q_PRIVATE_SLOT(d_ptr, void dataAvailable()) + Q_PRIVATE_SLOT(d_ptr, void serverStateChanged(Akonadi::ServerManager::State)) + Q_PRIVATE_SLOT(d_ptr, void invalidateCollectionCache(qint64)) + Q_PRIVATE_SLOT(d_ptr, void invalidateItemCache(qint64)) + Q_PRIVATE_SLOT(d_ptr, void invalidateTagCache(qint64)) + + friend class ResourceBasePrivate; + //@endcond +}; + +} + +#endif diff --git a/src/core/monitor_p.cpp b/src/core/monitor_p.cpp new file mode 100644 index 0000000..b20d8d4 --- /dev/null +++ b/src/core/monitor_p.cpp @@ -0,0 +1,1297 @@ +/* + Copyright (c) 2007 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. +*/ + +// @cond PRIVATE + +#include "monitor_p.h" + +#include "collectionfetchjob.h" +#include "collectionstatistics.h" +#include "KDBusConnectionPool" +#include "itemfetchjob.h" +#include "notificationmanagerinterface.h" +#include "session.h" +#include "changemediator_p.h" +#include "vectorhelper.h" +#include "akonadicore_debug.h" + +using namespace Akonadi; + +static const int PipelineSize = 5; + +MonitorPrivate::MonitorPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, Monitor *parent) + : q_ptr(parent) + , dependenciesFactory(dependenciesFactory_ ? dependenciesFactory_ : new ChangeNotificationDependenciesFactory) + , notificationSource(0) + , notificationBus(0) + , monitorAll(false) + , exclusive(false) + , mFetchChangedOnly(false) + , session(Session::defaultSession()) + , collectionCache(0) + , itemCache(0) + , tagCache(0) + , fetchCollection(false) + , fetchCollectionStatistics(false) + , collectionMoveTranslationEnabled(true) + , useRefCounting(false) +{ + qRegisterMetaType(); + qDBusRegisterMetaType(); +} + +MonitorPrivate::~MonitorPrivate() +{ + delete dependenciesFactory; + delete collectionCache; + delete itemCache; + delete tagCache; +} + +void MonitorPrivate::init() +{ + // needs to be at least 3x pipeline size for the collection move case + collectionCache = dependenciesFactory->createCollectionCache(3 * PipelineSize, session); + // needs to be at least 1x pipeline size + itemCache = dependenciesFactory->createItemListCache(PipelineSize, session); + // 20 tags looks like a reasonable amount to keep around + tagCache = dependenciesFactory->createTagListCache(20, session); + + QObject::connect(collectionCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable())); + QObject::connect(itemCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable())); + QObject::connect(tagCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable())); + QObject::connect(ServerManager::self(), SIGNAL(stateChanged(Akonadi::ServerManager::State)), + q_ptr, SLOT(serverStateChanged(Akonadi::ServerManager::State))); + + statisticsCompressionTimer.setSingleShot(true); + statisticsCompressionTimer.setInterval(500); + QObject::connect(&statisticsCompressionTimer, SIGNAL(timeout()), q_ptr, SLOT(slotFlushRecentlyChangedCollections())); +} + +bool MonitorPrivate::connectToNotificationManager() +{ + delete notificationSource; + + notificationSource = dependenciesFactory->createNotificationSource(q_ptr); + if (!notificationSource) { + return false; + } + + notificationSource->setSession(session->sessionId()); + + if (notificationBus) { + // HACK: Implementation detail: notificationBus is SessionPrivate subclass, + // so we cannot delete it directly, but we need to delete the owning + // Session instead, otherwise it will dereference a deleted d_ptr. + delete notificationBus->parent(); + } + notificationBus = dependenciesFactory->createNotificationBus(q_ptr, notificationSource); + if (!notificationBus) { + delete notificationSource; + notificationSource = 0; + return false; + } + QObject::connect(notificationBus, SIGNAL(notify(Akonadi::Protocol::ChangeNotification)), + q_ptr, SLOT(slotNotify(Akonadi::Protocol::ChangeNotification))); + + return true; +} + +void MonitorPrivate::serverStateChanged(ServerManager::State state) +{ + if (state == ServerManager::Running) { + if (connectToNotificationManager()) { + notificationSource->setAllMonitored(monitorAll); + notificationSource->setSession(session->sessionId()); + Q_FOREACH (const Collection &col, collections) { + notificationSource->setMonitoredCollection(col.id(), true); + } + Q_FOREACH (const Item::Id id, items) { + notificationSource->setMonitoredItem(id, true); + } + Q_FOREACH (const QByteArray &resource, resources) { + notificationSource->setMonitoredResource(resource, true); + } + Q_FOREACH (const QByteArray &session, sessions) { + notificationSource->setIgnoredSession(session, true); + } + Q_FOREACH (const QString &mimeType, mimetypes) { + notificationSource->setMonitoredMimeType(mimeType, true); + } + Q_FOREACH (Tag::Id tagId, tags) { + notificationSource->setMonitoredTag(tagId, true); + } + Q_FOREACH (Monitor::Type type, types) { + notificationSource->setMonitoredType( + static_cast(type), true); + } + } + } +} + +void MonitorPrivate::invalidateCollectionCache(qint64 id) +{ + collectionCache->update(id, mCollectionFetchScope); +} + +void MonitorPrivate::invalidateItemCache(qint64 id) +{ + itemCache->update(QList() << id, mItemFetchScope); +} + +void MonitorPrivate::invalidateTagCache(qint64 id) +{ + tagCache->update(QList() << id, mTagFetchScope); +} + +int MonitorPrivate::pipelineSize() const +{ + return PipelineSize; +} + +bool MonitorPrivate::isLazilyIgnored(const Protocol::ChangeNotification &msg, bool allowModifyFlagsConversion) const +{ + Protocol::ChangeNotification::Operation op = msg.operation(); + + if (msg.type() == Protocol::ChangeNotification::Tags + && ((op == Protocol::ChangeNotification::Add && q_ptr->receivers(SIGNAL(tagAdded(Akonadi::Tag))) == 0) + || (op == Protocol::ChangeNotification::Modify && q_ptr->receivers(SIGNAL(tagChanged(Akonadi::Tag))) == 0) + || (op == Protocol::ChangeNotification::Remove && q_ptr->receivers(SIGNAL(tagRemoved(Akonadi::Tag))) == 0))) { + return true; + } + + if (!fetchCollectionStatistics + && (msg.type() == Protocol::ChangeNotification::Items) + && ((op == Protocol::ChangeNotification::Add && q_ptr->receivers(SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))) == 0) + || (op == Protocol::ChangeNotification::Remove && q_ptr->receivers(SIGNAL(itemRemoved(Akonadi::Item))) == 0 + && q_ptr->receivers(SIGNAL(itemsRemoved(Akonadi::Item::List))) == 0) + || (op == Protocol::ChangeNotification::Modify && q_ptr->receivers(SIGNAL(itemChanged(Akonadi::Item,QSet))) == 0) + || (op == Protocol::ChangeNotification::ModifyFlags + && (q_ptr->receivers(SIGNAL(itemsFlagsChanged(Akonadi::Item::List,QSet,QSet))) == 0 + // Newly delivered ModifyFlags notifications will be converted to + // itemChanged(item, "FLAGS") for legacy clients. + && (!allowModifyFlagsConversion || q_ptr->receivers(SIGNAL(itemChanged(Akonadi::Item,QSet))) == 0))) + || (op == Protocol::ChangeNotification::ModifyTags && q_ptr->receivers(SIGNAL(itemsTagsChanged(Akonadi::Item::List,QSet,QSet))) == 0) + || (op == Protocol::ChangeNotification::Move && q_ptr->receivers(SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))) == 0 + && q_ptr->receivers(SIGNAL(itemsMoved(Akonadi::Item::List,Akonadi::Collection,Akonadi::Collection))) == 0) + || (op == Protocol::ChangeNotification::Link && q_ptr->receivers(SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection))) == 0 + && q_ptr->receivers(SIGNAL(itemsLinked(Akonadi::Item::List,Akonadi::Collection))) == 0) + || (op == Protocol::ChangeNotification::Unlink && q_ptr->receivers(SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection))) == 0 + && q_ptr->receivers(SIGNAL(itemsUnlinked(Akonadi::Item::List,Akonadi::Collection))) == 0))) { + return true; + } + + if (!useRefCounting) { + return false; + } + + if (msg.type() == Protocol::ChangeNotification::Collections) { + // Lazy fetching can only affects items. + return false; + } + + Collection::Id parentCollectionId = msg.parentCollection(); + + if ((op == Protocol::ChangeNotification::Add) + || (op == Protocol::ChangeNotification::Remove) + || (op == Protocol::ChangeNotification::Modify) + || (op == Protocol::ChangeNotification::ModifyFlags) + || (op == Protocol::ChangeNotification::ModifyTags) + || (op == Protocol::ChangeNotification::Link) + || (op == Protocol::ChangeNotification::Unlink)) { + if (isMonitored(parentCollectionId)) { + return false; + } + } + + if (op == Protocol::ChangeNotification::Move) { + if (!isMonitored(parentCollectionId) && !isMonitored(msg.parentDestCollection())) { + return true; + } + // We can't ignore the move. It must be transformed later into a removal or insertion. + return false; + } + return true; +} + +void MonitorPrivate::checkBatchSupport(const Protocol::ChangeNotification &msg, bool &needsSplit, bool &batchSupported) const +{ + const bool isBatch = (msg.entities().count() > 1); + + if (msg.type() == Protocol::ChangeNotification::Items) { + switch (msg.operation()) { + case Protocol::ChangeNotification::Add: + needsSplit = isBatch; + batchSupported = false; + return; + case Protocol::ChangeNotification::Modify: + needsSplit = isBatch; + batchSupported = false; + return; + case Protocol::ChangeNotification::ModifyFlags: + batchSupported = q_ptr->receivers(SIGNAL(itemsFlagsChanged(Akonadi::Item::List,QSet,QSet))) > 0; + needsSplit = isBatch && !batchSupported && q_ptr->receivers(SIGNAL(itemChanged(Akonadi::Item,QSet))) > 0; + return; + case Protocol::ChangeNotification::ModifyTags: + // Tags were added after batch notifications, so they are always supported + batchSupported = true; + needsSplit = false; + return; + case Protocol::ChangeNotification::ModifyRelations: + // Relations were added after batch notifications, so they are always supported + batchSupported = true; + needsSplit = false; + return; + case Protocol::ChangeNotification::Move: + needsSplit = isBatch && q_ptr->receivers(SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))) > 0; + batchSupported = q_ptr->receivers(SIGNAL(itemsMoved(Akonadi::Item::List,Akonadi::Collection,Akonadi::Collection))) > 0; + return; + case Protocol::ChangeNotification::Remove: + needsSplit = isBatch && q_ptr->receivers(SIGNAL(itemRemoved(Akonadi::Item))) > 0; + batchSupported = q_ptr->receivers(SIGNAL(itemsRemoved(Akonadi::Item::List))) > 0; + return; + case Protocol::ChangeNotification::Link: + needsSplit = isBatch && q_ptr->receivers(SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection))) > 0; + batchSupported = q_ptr->receivers(SIGNAL(itemsLinked(Akonadi::Item::List,Akonadi::Collection))) > 0; + return; + case Protocol::ChangeNotification::Unlink: + needsSplit = isBatch && q_ptr->receivers(SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection))) > 0; + batchSupported = q_ptr->receivers(SIGNAL(itemsUnlinked(Akonadi::Item::List,Akonadi::Collection))) > 0; + return; + default: + needsSplit = isBatch; + batchSupported = false; + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in item change notification"; + return; + } + } else if (msg.type() == Protocol::ChangeNotification::Collections) { + needsSplit = isBatch; + batchSupported = false; + } else if (msg.type() == Protocol::ChangeNotification::Tags) { + needsSplit = isBatch; + batchSupported = false; + } else if (msg.type() == Protocol::ChangeNotification::Relations) { + needsSplit = isBatch; + batchSupported = false; + } +} + +Protocol::ChangeNotification::List MonitorPrivate::splitMessage(const Protocol::ChangeNotification &msg, bool legacy) const +{ + Protocol::ChangeNotification::List list; + + Protocol::ChangeNotification baseMsg; + baseMsg.setSessionId(msg.sessionId()); + baseMsg.setType(msg.type()); + if (legacy && msg.operation() == Protocol::ChangeNotification::ModifyFlags) { + baseMsg.setOperation(Protocol::ChangeNotification::Modify); + baseMsg.setItemParts(QSet() << "FLAGS"); + } else { + baseMsg.setOperation(msg.operation()); + baseMsg.setItemParts(msg.itemParts()); + } + baseMsg.setParentCollection(msg.parentCollection()); + baseMsg.setParentDestCollection(msg.parentDestCollection()); + baseMsg.setResource(msg.resource()); + baseMsg.setDestinationResource(msg.destinationResource()); + baseMsg.setAddedFlags(msg.addedFlags()); + baseMsg.setRemovedFlags(msg.removedFlags()); + baseMsg.setAddedTags(msg.addedTags()); + baseMsg.setRemovedTags(msg.removedTags()); + + list.reserve(msg.entities().count()); + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + Protocol::ChangeNotification copy = baseMsg; + copy.addEntity(entity.id, entity.remoteId, entity.remoteRevision, entity.mimeType); + + list << copy; + } + + return list; +} + +bool MonitorPrivate::acceptNotification(const Akonadi::Protocol::ChangeNotification &msg) const +{ + // session is ignored + if (sessions.contains(msg.sessionId())) { + return false; + } + + if (msg.entities().count() == 0 && msg.type() != Protocol::ChangeNotification::Relations) { + return false; + } + + // user requested everything + if (monitorAll && msg.type() != Protocol::ChangeNotification::InvalidType) { + return true; + } + + // Types are monitored, but not this one + if (!types.isEmpty() && !types.contains(static_cast(msg.type()))) { + return false; + } + + switch (msg.type()) { + case Protocol::ChangeNotification::InvalidType: + qCWarning(AKONADICORE_LOG) << "Received invalid change notification!"; + return false; + + case Protocol::ChangeNotification::Items: + // we have a resource or mimetype filter + if (!resources.isEmpty() || !mimetypes.isEmpty()) { + if (resources.contains(msg.resource()) || isMoveDestinationResourceMonitored(msg)) { + return true; + } + + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + if (isMimeTypeMonitored(entity.mimeType)) { + return true; + } + } + return false; + } + + // we explicitly monitor that item or the collections it's in + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + if (items.contains(entity.id)) { + return true; + } + } + + return isCollectionMonitored(msg.parentCollection()) + || isCollectionMonitored(msg.parentDestCollection()); + + case Protocol::ChangeNotification::Collections: + // we have a resource filter + if (!resources.isEmpty()) { + const bool resourceMatches = resources.contains(msg.resource()) || isMoveDestinationResourceMonitored(msg); + // a bit hacky, but match the behaviour from the item case, + // if resource is the only thing we are filtering on, stop here, and if the resource filter matched, of course + if (mimetypes.isEmpty() || resourceMatches) { + return resourceMatches; + } + // else continue + } + + // we explicitly monitor that colleciton, or all of them + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + if (isCollectionMonitored(entity.id)) { + return true; + } + } + return isCollectionMonitored(msg.parentCollection()) + || isCollectionMonitored(msg.parentDestCollection()); + + case Protocol::ChangeNotification::Tags: + if (!tags.isEmpty()) { + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + if (tags.contains(entity.id)) { + return true; + } + } + return false; + } + return true; + case Protocol::ChangeNotification::Relations: + return true; + } + Q_ASSERT(false); + return false; +} + +void MonitorPrivate::cleanOldNotifications() +{ + bool erased = false; + for (QQueue::iterator it = pipeline.begin(); it != pipeline.end();) { + if (!acceptNotification(*it) || isLazilyIgnored(*it)) { + it = pipeline.erase(it); + erased = true; + } else { + ++it; + } + } + + for (QQueue::iterator it = pendingNotifications.begin(); it != pendingNotifications.end();) { + if (!acceptNotification(*it) || isLazilyIgnored(*it)) { + it = pendingNotifications.erase(it); + erased = true; + } else { + ++it; + } + } + if (erased) { + notificationsErased(); + } +} + +bool MonitorPrivate::fetchCollections() const +{ + return fetchCollection; +} + +bool MonitorPrivate::fetchItems() const +{ + return !mItemFetchScope.isEmpty(); +} + +bool MonitorPrivate::ensureDataAvailable(const Protocol::ChangeNotification &msg) +{ + if (msg.type() == Protocol::ChangeNotification::Tags) { + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + if (!tagCache->ensureCached(QList() << entity.id, mTagFetchScope)) { + return false; + } + } + return true; + } + if (msg.type() == Protocol::ChangeNotification::Relations) { + return true; + } + + if (msg.operation() == Protocol::ChangeNotification::Remove && msg.type() == Protocol::ChangeNotification::Collections) { + //For collection removals the collection is gone anyways, so we can't fetch it. Rid will be set later on instead. + return true; + } + + bool allCached = true; + if (fetchCollections()) { + if (!collectionCache->ensureCached(msg.parentCollection(), mCollectionFetchScope)) { + allCached = false; + } + if (msg.operation() == Protocol::ChangeNotification::Move && !collectionCache->ensureCached(msg.parentDestCollection(), mCollectionFetchScope)) { + allCached = false; + } + } + if (msg.operation() == Protocol::ChangeNotification::Remove) { + return allCached; // the actual object is gone already, nothing to fetch there + } + + if (msg.type() == Protocol::ChangeNotification::Items && fetchItems()) { + ItemFetchScope scope(mItemFetchScope); + if (mFetchChangedOnly && (msg.operation() == Protocol::ChangeNotification::Modify || msg.operation() == Protocol::ChangeNotification::ModifyFlags)) { + bool fullPayloadWasRequested = scope.fullPayload(); + scope.fetchFullPayload(false); + QSet requestedPayloadParts = scope.payloadParts(); + Q_FOREACH (const QByteArray &part, requestedPayloadParts) { + scope.fetchPayloadPart(part, false); + } + + bool allAttributesWereRequested = scope.allAttributes(); + QSet requestedAttrParts = scope.attributes(); + Q_FOREACH (const QByteArray &part, requestedAttrParts) { + scope.fetchAttribute(part, false); + } + + QSet changedParts = msg.itemParts(); + Q_FOREACH (const QByteArray &part, changedParts) { + if (part.startsWith("PLD:") && //krazy:exclude=strings since QByteArray + (fullPayloadWasRequested || requestedPayloadParts.contains(part))) { + scope.fetchPayloadPart(part.mid(4), true);; + } + if (part.startsWith("ATR:") && //krazy:exclude=strings since QByteArray + (allAttributesWereRequested || requestedAttrParts.contains(part))) { + scope.fetchAttribute(part.mid(4), true); + } + } + } + if (!itemCache->ensureCached(msg.uids(), scope)) { + allCached = false; + + } + + // Make sure all tags for ModifyTags operation are in cache too + if (msg.operation() == Protocol::ChangeNotification::ModifyTags) { + if (!tagCache->ensureCached((msg.addedTags() + msg.removedTags()).toList(), mTagFetchScope)) { + allCached = false; + } + } + + } else if (msg.type() == Protocol::ChangeNotification::Collections && fetchCollections()) { + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + if (!collectionCache->ensureCached(entity.id, mCollectionFetchScope)) { + allCached = false; + break; + } + } + } + return allCached; +} + +bool MonitorPrivate::emitNotification(const Protocol::ChangeNotification &msg) +{ + bool someoneWasListening = false; + bool shouldCleanOldNotifications = false; + if (msg.type() == Protocol::ChangeNotification::Tags) { + //In case of a Remove notification this will return a list of invalid entities (we'll deal later with them) + const Tag::List tags = tagCache->retrieve(msg.uids()); + someoneWasListening = emitTagsNotification(msg, tags); + shouldCleanOldNotifications = !someoneWasListening; + } else if (msg.type() == Protocol::ChangeNotification::Relations) { + Relation rel; + Q_FOREACH (const QByteArray &part, msg.itemParts()) { + QList splitPart = part.split(' '); + Q_ASSERT(splitPart.size() == 2); + if (splitPart.first() == "LEFT") { + rel.setLeft(Akonadi::Item(splitPart.at(1).toLongLong())); + } else if (splitPart.first() == "RIGHT") { + rel.setRight(Akonadi::Item(splitPart.at(1).toLongLong())); + } else if (splitPart.first() == "TYPE") { + rel.setType(splitPart.at(1)); + } else if (splitPart.first() == "RID") { + rel.setRemoteId(splitPart.at(1)); + } + } + someoneWasListening = emitRelationsNotification(msg, Relation::List() << rel); + shouldCleanOldNotifications = !someoneWasListening; + } else { + const Collection parent = collectionCache->retrieve(msg.parentCollection()); + Collection destParent; + if (msg.operation() == Protocol::ChangeNotification::Move) { + destParent = collectionCache->retrieve(msg.parentDestCollection()); + } + + if (msg.type() == Protocol::ChangeNotification::Collections) { + Collection col; + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + //For removals this will retrieve an invalid collection. We'll deal with that in emitCollectionNotification + col = collectionCache->retrieve(entity.id); + //It is possible that the retrieval fails also in the non-removal case (e.g. because the item was meanwhile removed while + //the changerecorder stored the notification or the notification was in the queue). In order to drop such invalid notifications we have to ignore them. + if (col.isValid() || msg.operation() == Protocol::ChangeNotification::Remove || !fetchCollections()) { + someoneWasListening = emitCollectionNotification(msg, col, parent, destParent); + shouldCleanOldNotifications = !someoneWasListening; + } + } + } else if (msg.type() == Protocol::ChangeNotification::Items) { + //For removals this will retrieve an empty set. We'll deal with that in emitItemNotification + const Item::List items = itemCache->retrieve(msg.uids()); + //It is possible that the retrieval fails also in the non-removal case (e.g. because the item was meanwhile removed while + //the changerecorder stored the notification or the notification was in the queue). In order to drop such invalid notifications we have to ignore them. + if (!items.isEmpty() || msg.operation() == Protocol::ChangeNotification::Remove || !fetchItems()) { + someoneWasListening = emitItemsNotification(msg, items, parent, destParent); + shouldCleanOldNotifications = !someoneWasListening; + } + } + } + + if (shouldCleanOldNotifications) { + cleanOldNotifications(); // probably someone disconnected a signal in the meantime, get rid of the no longer interesting stuff + } + + return someoneWasListening; +} + +void MonitorPrivate::updatePendingStatistics(const Protocol::ChangeNotification &msg) +{ + if (msg.type() == Protocol::ChangeNotification::Items) { + notifyCollectionStatisticsWatchers(msg.parentCollection(), msg.resource()); + // FIXME use the proper resource of the target collection, for cross resource moves + notifyCollectionStatisticsWatchers(msg.parentDestCollection(), msg.destinationResource()); + } else if (msg.type() == Protocol::ChangeNotification::Collections && msg.operation() == Protocol::ChangeNotification::Remove) { + // no need for statistics updates anymore + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, msg.entities()) { + recentlyChangedCollections.remove(entity.id); + } + } +} + +void MonitorPrivate::slotSessionDestroyed(QObject *object) +{ + Session *objectSession = qobject_cast(object); + if (objectSession) { + sessions.removeAll(objectSession->sessionId()); + if (notificationSource) { + notificationSource->setIgnoredSession(objectSession->sessionId(), false); + } + } +} + +void MonitorPrivate::slotStatisticsChangedFinished(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error on fetching collection statistics: " << job->errorText(); + } else { + CollectionStatisticsJob *statisticsJob = static_cast(job); + Q_ASSERT(statisticsJob->collection().isValid()); + emit q_ptr->collectionStatisticsChanged(statisticsJob->collection().id(), + statisticsJob->statistics()); + } +} + +void MonitorPrivate::slotFlushRecentlyChangedCollections() +{ + foreach (Collection::Id collection, recentlyChangedCollections) { + Q_ASSERT(collection >= 0); + if (fetchCollectionStatistics) { + fetchStatistics(collection); + } else { + static const CollectionStatistics dummyStatistics; + emit q_ptr->collectionStatisticsChanged(collection, dummyStatistics); + } + } + recentlyChangedCollections.clear(); +} + +int MonitorPrivate::translateAndCompress(QQueue< Protocol::ChangeNotification > ¬ificationQueue, const Protocol::ChangeNotification &msg) +{ + // We have to split moves into insert or remove if the source or destination + // is not monitored. + if (msg.operation() != Protocol::ChangeNotification::Move) { + notificationQueue.enqueue(msg); + return 1; + } + + // Always handle tags + if (msg.type() == Protocol::ChangeNotification::Tags) { + notificationQueue.enqueue(msg); + return 1; + } + + bool sourceWatched = false; + bool destWatched = false; + + if (useRefCounting && msg.type() == Protocol::ChangeNotification::Items) { + sourceWatched = isMonitored(msg.parentCollection()); + destWatched = isMonitored(msg.parentDestCollection()); + } else { + if (!resources.isEmpty()) { + sourceWatched = resources.contains(msg.resource()); + destWatched = isMoveDestinationResourceMonitored(msg); + } + if (!sourceWatched) { + sourceWatched = isCollectionMonitored(msg.parentCollection()); + } + if (!destWatched) { + destWatched = isCollectionMonitored(msg.parentDestCollection()); + } + } + + if (!sourceWatched && !destWatched) { + return 0; + } + + if ((sourceWatched && destWatched) || (!collectionMoveTranslationEnabled && msg.type() == Protocol::ChangeNotification::Collections)) { + notificationQueue.enqueue(msg); + return 1; + } + + if (sourceWatched) { + // Transform into a removal + Protocol::ChangeNotification removalMessage = msg; + removalMessage.setOperation(Protocol::ChangeNotification::Remove); + removalMessage.setParentDestCollection(-1); + notificationQueue.enqueue(removalMessage); + return 1; + } + + // Transform into an insertion + Protocol::ChangeNotification insertionMessage = msg; + insertionMessage.setOperation(Protocol::ChangeNotification::Add); + insertionMessage.setParentCollection(msg.parentDestCollection()); + insertionMessage.setParentDestCollection(-1); + // We don't support batch insertion, so we have to do it one by one + const Protocol::ChangeNotification::List split = splitMessage(insertionMessage, false); + Q_FOREACH (const Protocol::ChangeNotification &insertion, split) { + notificationQueue.enqueue(insertion); + } + return split.count(); +} + +/* + + server notification --> ?accepted --> pendingNotifications --> ?dataAvailable --> emit + | | + x --> discard x --> pipeline + + fetchJobDone --> pipeline ?dataAvailable --> emit + */ + +void MonitorPrivate::slotNotify(const Protocol::ChangeNotification &msg) +{ + int appendedMessages = 0; + int modifiedMessages = 0; + int erasedMessages = 0; + + invalidateCaches(msg); + updatePendingStatistics(msg); + bool needsSplit = true; + bool supportsBatch = false; + + if (isLazilyIgnored(msg, true)) { + return; + } + + checkBatchSupport(msg, needsSplit, supportsBatch); + + if (supportsBatch + || (!needsSplit && !supportsBatch && msg.operation() != Protocol::ChangeNotification::ModifyFlags) + || msg.type() == Protocol::ChangeNotification::Collections) { + // Make sure the batch msg is always queued before the split notifications + const int oldSize = pendingNotifications.size(); + const int appended = translateAndCompress(pendingNotifications, msg); + if (appended > 0) { + appendedMessages += appended; + } else { + ++modifiedMessages; + } + // translateAndCompress can remove an existing "modify" when msg is a "delete". + // Or it can merge two ModifyFlags and return false. + // We need to detect such removals, for ChangeRecorder. + if (pendingNotifications.count() != oldSize + appended) { + ++erasedMessages; // this count isn't exact, but it doesn't matter + } + } else if (needsSplit) { + // If it's not queued at least make sure we fetch all the items from split + // notifications in one go. + itemCache->ensureCached(msg.uids(), mItemFetchScope); + } + + // if the message contains more items, but we need to emit single-item notification, + // split the message into one message per item and queue them + // if the message contains only one item, but batches are not supported + // (and thus neither is flagsModified), splitMessage() will convert the + // notification to regular Modify with "FLAGS" part changed + if (needsSplit || (!needsSplit && !supportsBatch && msg.operation() == Akonadi::Protocol::ChangeNotification::ModifyFlags)) { + // Make sure inter-resource move notifications are translated into + // Add/Remove notifications + if (msg.operation() == Protocol::ChangeNotification::Move && msg.resource() != msg.destinationResource()) { + if (needsSplit) { + const Protocol::ChangeNotification::List split = splitMessage(msg, !supportsBatch); + Q_FOREACH (const auto &splitMsg, split) { + appendedMessages += translateAndCompress(pendingNotifications, splitMsg); + } + } else { + appendedMessages += translateAndCompress(pendingNotifications, msg); + } + } else { + const Protocol::ChangeNotification::List split = splitMessage(msg, !supportsBatch); + pendingNotifications << split.toList(); + appendedMessages += split.count(); + } + } + + // tell ChangeRecorder (even if 0 appended, the compression could have made changes to existing messages) + if (appendedMessages > 0 || modifiedMessages > 0 || erasedMessages > 0) { + if (erasedMessages > 0) { + notificationsErased(); + } else { + notificationsEnqueued(appendedMessages); + } + } + + dispatchNotifications(); +} + +void MonitorPrivate::flushPipeline() +{ + while (!pipeline.isEmpty()) { + const Protocol::ChangeNotification msg = pipeline.head(); + if (ensureDataAvailable(msg)) { + // dequeue should be before emit, otherwise stuff might happen (like dataAvailable + // being called again) and we end up dequeuing an empty pipeline + pipeline.dequeue(); + emitNotification(msg); + } else { + break; + } + } +} + +void MonitorPrivate::dataAvailable() +{ + flushPipeline(); + dispatchNotifications(); +} + +void MonitorPrivate::dispatchNotifications() +{ + // Note that this code is not used in a ChangeRecorder (pipelineSize==0) + while (pipeline.size() < pipelineSize() && !pendingNotifications.isEmpty()) { + const Protocol::ChangeNotification msg = pendingNotifications.dequeue(); + if (ensureDataAvailable(msg) && pipeline.isEmpty()) { + emitNotification(msg); + } else { + pipeline.enqueue(msg); + } + } +} + +static Relation::List extractRelations(QSet &flags) +{ + Relation::List relations; + Q_FOREACH (const QByteArray &flag, flags) { + if (flag.startsWith("RELATION")) { + flags.remove(flag); + const QList parts = flag.split(' '); + Q_ASSERT(parts.size() == 4); + relations << Relation(parts[1], Item(parts[2].toLongLong()), Item(parts[3].toLongLong())); + } + } + return relations; +} + +bool MonitorPrivate::emitItemsNotification(const Protocol::ChangeNotification &msg_, const Item::List &items, const Collection &collection, const Collection &collectionDest) +{ + Protocol::ChangeNotification msg = msg_; + Q_ASSERT(msg.type() == Protocol::ChangeNotification::Items); + Collection col = collection; + Collection colDest = collectionDest; + if (!col.isValid()) { + col = Collection(msg.parentCollection()); + col.setResource(QString::fromUtf8(msg.resource())); + } + if (!colDest.isValid()) { + colDest = Collection(msg.parentDestCollection()); + // HACK: destination resource is delivered in the parts field... + if (!msg.itemParts().isEmpty()) { + colDest.setResource(QString::fromLatin1(*(msg.itemParts().cbegin()))); + } + } + + Relation::List addedRelations, removedRelations; + if (msg.operation() == Protocol::ChangeNotification::ModifyRelations) { + QSet addedFlags = msg.addedFlags(); + addedRelations = extractRelations(addedFlags); + msg.setAddedFlags(addedFlags); + + QSet removedFlags = msg.removedFlags(); + removedRelations = extractRelations(removedFlags); + msg.setRemovedFlags(removedFlags); + } + + Tag::List addedTags, removedTags; + if (msg.operation() == Protocol::ChangeNotification::ModifyTags) { + addedTags = tagCache->retrieve(msg.addedTags().toList()); + removedTags = tagCache->retrieve(msg.removedTags().toList()); + } + + QMap msgEntities = msg.entities(); + Item::List its = items; + QMutableVectorIterator iter(its); + while (iter.hasNext()) { + Item it = iter.next(); + if (it.isValid()) { + const Protocol::ChangeNotification::Entity msgEntity = msgEntities[it.id()]; + if (msg.operation() == Protocol::ChangeNotification::Remove) { + it.setRemoteId(msgEntity.remoteId); + it.setRemoteRevision(msgEntity.remoteRevision); + it.setMimeType(msgEntity.mimeType); + } + + if (!it.parentCollection().isValid()) { + if (msg.operation() == Protocol::ChangeNotification::Move) { + it.setParentCollection(colDest); + } else { + it.setParentCollection(col); + } + } else { + // item has a valid parent collection, most likely due to retrieved ancestors + // still, collection might contain extra info, so inject that + if (it.parentCollection() == col) { + const Collection oldParent = it.parentCollection(); + if (oldParent.parentCollection().isValid() && !col.parentCollection().isValid()) { + col.setParentCollection(oldParent.parentCollection()); // preserve ancestor chain + } + it.setParentCollection(col); + } else { + // If one client does a modify followed by a move we have to make sure that the + // AgentBase::itemChanged() in another client always sees the parent collection + // of the item before it has been moved. + if (msg.operation() != Protocol::ChangeNotification::Move) { + it.setParentCollection(col); + } + } + } + iter.setValue(it); + msgEntities.remove(it.id()); + } else { + // remove the invalid item + iter.remove(); + } + } + + its.reserve(its.size() + msgEntities.size()); + // Now reconstruct any items there were left in msgItems + Q_FOREACH (const Protocol::ChangeNotification::Entity &msgItem, msgEntities) { + Item it(msgItem.id); + it.setRemoteId(msgItem.remoteId); + it.setRemoteRevision(msgItem.remoteRevision); + it.setMimeType(msgItem.mimeType); + if (msg.operation() == Protocol::ChangeNotification::Move) { + it.setParentCollection(colDest); + } else { + it.setParentCollection(col); + } + its << it; + } + + bool handled = false; + switch (msg.operation()) { + case Protocol::ChangeNotification::Add: + if (q_ptr->receivers(SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection))) > 0) { + Q_ASSERT(its.count() == 1); + emit q_ptr->itemAdded(its.first(), col); + return true; + } + return false; + case Protocol::ChangeNotification::Modify: + if (q_ptr->receivers(SIGNAL(itemChanged(Akonadi::Item,QSet))) > 0) { + Q_ASSERT(its.count() == 1); + emit q_ptr->itemChanged(its.first(), msg.itemParts()); + return true; + } + return false; + case Protocol::ChangeNotification::ModifyFlags: + if (q_ptr->receivers(SIGNAL(itemsFlagsChanged(Akonadi::Item::List,QSet,QSet))) > 0) { + emit q_ptr->itemsFlagsChanged(its, msg.addedFlags(), msg.removedFlags()); + handled = true; + } + return handled; + case Protocol::ChangeNotification::Move: + if (q_ptr->receivers(SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))) > 0) { + Q_ASSERT(its.count() == 1); + emit q_ptr->itemMoved(its.first(), col, colDest); + handled = true; + } + if (q_ptr->receivers(SIGNAL(itemsMoved(Akonadi::Item::List,Akonadi::Collection,Akonadi::Collection))) > 0) { + emit q_ptr->itemsMoved(its, col, colDest); + handled = true; + } + return handled; + case Protocol::ChangeNotification::Remove: + if (q_ptr->receivers(SIGNAL(itemRemoved(Akonadi::Item))) > 0) { + Q_ASSERT(its.count() == 1); + emit q_ptr->itemRemoved(its.first()); + handled = true; + } + if (q_ptr->receivers(SIGNAL(itemsRemoved(Akonadi::Item::List))) > 0) { + emit q_ptr->itemsRemoved(its); + handled = true; + } + return handled; + case Protocol::ChangeNotification::Link: + if (q_ptr->receivers(SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection))) > 0) { + Q_ASSERT(its.count() == 1); + emit q_ptr->itemLinked(its.first(), col); + handled = true; + } + if (q_ptr->receivers(SIGNAL(itemsLinked(Akonadi::Item::List,Akonadi::Collection))) > 0) { + emit q_ptr->itemsLinked(its, col); + handled = true; + } + return handled; + case Protocol::ChangeNotification::Unlink: + if (q_ptr->receivers(SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection))) > 0) { + Q_ASSERT(its.count() == 1); + emit q_ptr->itemUnlinked(its.first(), col); + handled = true; + } + if (q_ptr->receivers(SIGNAL(itemsUnlinked(Akonadi::Item::List,Akonadi::Collection))) > 0) { + emit q_ptr->itemsUnlinked(its, col); + handled = true; + } + return handled; + case Protocol::ChangeNotification::ModifyTags: + if (q_ptr->receivers(SIGNAL(itemsTagsChanged(Akonadi::Item::List,QSet,QSet))) > 0) { + emit q_ptr->itemsTagsChanged(its, Akonadi::vectorToSet(addedTags), Akonadi::vectorToSet(removedTags)); + return true; + } + return false; + case Protocol::ChangeNotification::ModifyRelations: + if (q_ptr->receivers(SIGNAL(itemsRelationsChanged(Akonadi::Item::List,Akonadi::Relation::List,Akonadi::Relation::List))) > 0) { + emit q_ptr->itemsRelationsChanged(its, addedRelations, removedRelations); + return true; + } + return false; + default: + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in item change notification"; + } + + return false; +} + +bool MonitorPrivate::emitCollectionNotification(const Protocol::ChangeNotification &msg, const Collection &col, const Collection &par, const Collection &dest) +{ + Q_ASSERT(msg.type() == Protocol::ChangeNotification::Collections); + Collection parent = par; + if (!parent.isValid()) { + parent = Collection(msg.parentCollection()); + } + Collection destination = dest; + if (!destination.isValid()) { + destination = Collection(msg.parentDestCollection()); + } + + Collection collection = col; + Protocol::ChangeNotification::Entity msgEntities = msg.entities().cbegin().value(); + if (!collection.isValid() || msg.operation() == Protocol::ChangeNotification::Remove) { + collection = Collection(msgEntities.id); + collection.setResource(QString::fromUtf8(msg.resource())); + collection.setRemoteId(msgEntities.remoteId); + } + + if (!collection.parentCollection().isValid()) { + if (msg.operation() == Protocol::ChangeNotification::Move) { + collection.setParentCollection(destination); + } else { + collection.setParentCollection(parent); + } + } + + switch (msg.operation()) { + case Protocol::ChangeNotification::Add: + if (q_ptr->receivers(SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection))) == 0) { + return false; + } + emit q_ptr->collectionAdded(collection, parent); + return true; + case Protocol::ChangeNotification::Modify: + if (q_ptr->receivers(SIGNAL(collectionChanged(Akonadi::Collection))) == 0 + && q_ptr->receivers(SIGNAL(collectionChanged(Akonadi::Collection,QSet))) == 0) { + return false; + } + emit q_ptr->collectionChanged(collection); + emit q_ptr->collectionChanged(collection, msg.itemParts()); + return true; + case Protocol::ChangeNotification::Move: + if (q_ptr->receivers(SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))) == 0) { + return false; + } + emit q_ptr->collectionMoved(collection, parent, destination); + return true; + case Protocol::ChangeNotification::Remove: + if (q_ptr->receivers(SIGNAL(collectionRemoved(Akonadi::Collection))) == 0) { + return false; + } + emit q_ptr->collectionRemoved(collection); + return true; + case Protocol::ChangeNotification::Subscribe: + if (q_ptr->receivers(SIGNAL(collectionSubscribed(Akonadi::Collection,Akonadi::Collection))) == 0) { + return false; + } + if (!monitorAll) { // ### why?? + emit q_ptr->collectionSubscribed(collection, parent); + } + return true; + case Protocol::ChangeNotification::Unsubscribe: + if (q_ptr->receivers(SIGNAL(collectionUnsubscribed(Akonadi::Collection))) == 0) { + return false; + } + if (!monitorAll) { // ### why?? + emit q_ptr->collectionUnsubscribed(collection); + } + return true; + default: + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in collection change notification"; + } + + return false; +} + +bool MonitorPrivate::emitTagsNotification(const Protocol::ChangeNotification &msg, const Tag::List &tags) +{ + Q_ASSERT(msg.type() == Protocol::ChangeNotification::Tags); + + Tag::List validTags; + if (msg.operation() == Protocol::ChangeNotification::Remove) { + //In case of a removed signal the cache entry was already invalidated, and we therefore received an empty list of tags + validTags.reserve(msg.entities().count()); + Q_FOREACH (const Akonadi::Protocol::ChangeNotification::Entity &entity, msg.entities()) { + Tag tag(entity.id); + tag.setRemoteId(entity.remoteId.toLatin1()); + validTags << tag; + } + } else { + validTags = tags; + } + + if (validTags.isEmpty()) { + return false; + } + + switch (msg.operation()) { + case Protocol::ChangeNotification::Add: + if (q_ptr->receivers(SIGNAL(tagAdded(Akonadi::Tag))) == 0) { + return false; + } + Q_FOREACH (const Tag &tag, validTags) { + Q_EMIT q_ptr->tagAdded(tag); + } + return true; + case Protocol::ChangeNotification::Modify: + if (q_ptr->receivers(SIGNAL(tagChanged(Akonadi::Tag))) == 0) { + return false; + } + Q_FOREACH (const Tag &tag, validTags) { + Q_EMIT q_ptr->tagChanged(tag); + } + return true; + case Protocol::ChangeNotification::Remove: + if (q_ptr->receivers(SIGNAL(tagRemoved(Akonadi::Tag))) == 0) { + return false; + } + Q_FOREACH (const Tag &tag, validTags) { + Q_EMIT q_ptr->tagRemoved(tag); + } + return true; + default: + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in tag change notification"; + } + + return false; +} + +bool MonitorPrivate::emitRelationsNotification(const Protocol::ChangeNotification &msg, const Relation::List &relations) +{ + Q_ASSERT(msg.type() == Protocol::ChangeNotification::Relations); + + if (relations.isEmpty()) { + return false; + } + + switch (msg.operation()) { + case Protocol::ChangeNotification::Add: + if (q_ptr->receivers(SIGNAL(relationAdded(Akonadi::Relation))) == 0) { + return false; + } + Q_FOREACH (const Relation &relation, relations) { + Q_EMIT q_ptr->relationAdded(relation); + } + return true; + case Protocol::ChangeNotification::Remove: + if (q_ptr->receivers(SIGNAL(relationRemoved(Akonadi::Relation))) == 0) { + return false; + } + Q_FOREACH (const Relation &relation, relations) { + Q_EMIT q_ptr->relationRemoved(relation); + } + return true; + default: + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in tag change notification"; + } + + return false; +} + +void MonitorPrivate::invalidateCaches(const Protocol::ChangeNotification &msg) +{ + // remove invalidates + if (msg.operation() == Protocol::ChangeNotification::Remove) { + if (msg.type() == Protocol::ChangeNotification::Collections) { + Q_FOREACH (qint64 uid, msg.uids()) { + collectionCache->invalidate(uid); + } + } else if (msg.type() == Protocol::ChangeNotification::Items) { + itemCache->invalidate(msg.uids()); + } else if (msg.type() == Protocol::ChangeNotification::Tags) { + tagCache->invalidate(msg.uids()); + } + } + + // modify removes the cache entry, as we need to re-fetch + // And subscription modify the visibility of the collection by the collectionFetchScope. + if (msg.operation() == Protocol::ChangeNotification::Modify + || msg.operation() == Protocol::ChangeNotification::ModifyFlags + || msg.operation() == Protocol::ChangeNotification::ModifyTags + || msg.operation() == Protocol::ChangeNotification::Move + || msg.operation() == Protocol::ChangeNotification::Subscribe) { + if (msg.type() == Protocol::ChangeNotification::Collections) { + Q_FOREACH (quint64 uid, msg.uids()) { + collectionCache->update(uid, mCollectionFetchScope); + } + } else if (msg.type() == Protocol::ChangeNotification::Items) { + itemCache->update(msg.uids(), mItemFetchScope); + } else if (msg.type() == Protocol::ChangeNotification::Tags) { + tagCache->update(msg.uids(), mTagFetchScope); + } + } +} + +void MonitorPrivate::invalidateCache(const Collection &col) +{ + collectionCache->update(col.id(), mCollectionFetchScope); +} + +void MonitorPrivate::ref(Collection::Id id) +{ + if (!refCountMap.contains(id)) { + refCountMap.insert(id, 0); + } + ++refCountMap[id]; + + if (m_buffer.isBuffered(id)) { + m_buffer.purge(id); + } +} + +Akonadi::Collection::Id MonitorPrivate::deref(Collection::Id id) +{ + Q_ASSERT(refCountMap.contains(id)); + if (--refCountMap[id] == 0) { + refCountMap.remove(id); + return m_buffer.buffer(id); + } + return -1; +} + +void MonitorPrivate::PurgeBuffer::purge(Collection::Id id) +{ + m_buffer.removeOne(id); +} + +Akonadi::Collection::Id MonitorPrivate::PurgeBuffer::buffer(Collection::Id id) +{ + // Ensure that we don't put a duplicate @p id into the buffer. + purge(id); + + Collection::Id bumpedId = -1; + if (m_buffer.size() == MAXBUFFERSIZE) { + bumpedId = m_buffer.dequeue(); + purge(bumpedId); + } + + m_buffer.enqueue(id); + + return bumpedId; +} + +int MonitorPrivate::PurgeBuffer::buffersize() +{ + return MAXBUFFERSIZE; +} + +bool MonitorPrivate::isMonitored(Collection::Id colId) const +{ + if (!useRefCounting) { + return true; + } + return refCountMap.contains(colId) || m_buffer.isBuffered(colId); +} + +void MonitorPrivate::notifyCollectionStatisticsWatchers(Collection::Id collection, const QByteArray &resource) +{ + if (collection > 0 && (monitorAll || isCollectionMonitored(collection) || resources.contains(resource))) { + recentlyChangedCollections.insert(collection); + if (!statisticsCompressionTimer.isActive()) { + statisticsCompressionTimer.start(); + } + } +} + +// @endcond diff --git a/src/core/monitor_p.h b/src/core/monitor_p.h new file mode 100644 index 0000000..a94d0f8 --- /dev/null +++ b/src/core/monitor_p.h @@ -0,0 +1,315 @@ +/* + Copyright (c) 2007 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_MONITOR_P_H +#define AKONADI_MONITOR_P_H + +#include "akonadicore_export.h" +#include "monitor.h" +#include "collection.h" +#include "collectionstatisticsjob.h" +#include "collectionfetchscope.h" +#include "item.h" +#include "itemfetchscope.h" +#include "tagfetchscope.h" +#include "job.h" +#include "entitycache_p.h" +#include "servermanager.h" +#include "changenotificationdependenciesfactory_p.h" +#include "notificationsource_p.h" + +#include "private/protocol_p.h" + +#include +#include + +#include +#include + +namespace Akonadi +{ + +class Monitor; + +/** + * @internal + */ +class AKONADICORE_EXPORT MonitorPrivate +{ +public: + MonitorPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, Monitor *parent); + virtual ~MonitorPrivate(); + void init(); + + Monitor *q_ptr; + Q_DECLARE_PUBLIC(Monitor) + ChangeNotificationDependenciesFactory *dependenciesFactory; + NotificationSource *notificationSource; + QObject *notificationBus; + Collection::List collections; + QSet resources; + QSet items; + QSet tags; + QSet types; + QSet mimetypes; + bool monitorAll; + bool exclusive; + QList sessions; + ItemFetchScope mItemFetchScope; + TagFetchScope mTagFetchScope; + CollectionFetchScope mCollectionFetchScope; + bool mFetchChangedOnly; + Session *session; + CollectionCache *collectionCache; + ItemListCache *itemCache; + TagListCache *tagCache; + QMimeDatabase mimeDatabase; + + // The waiting list + QQueue pendingNotifications; + // The messages for which data is currently being fetched + QQueue pipeline; + // In a pure Monitor, the pipeline contains items that were dequeued from pendingNotifications. + // The ordering [pipeline] [pendingNotifications] is kept at all times. + // [] [A B C] -> [A B] [C] -> [B] [C] -> [B C] [] -> [C] [] -> [] + // In a ChangeRecorder, the pipeline contains one item only, and not dequeued yet. + // [] [A B C] -> [A] [A B C] -> [] [A B C] -> (changeProcessed) [] [B C] -> [B] [B C] etc... + + bool fetchCollection; + bool fetchCollectionStatistics; + bool collectionMoveTranslationEnabled; + + // Virtual methods for ChangeRecorder + virtual void notificationsEnqueued(int) + { + } + virtual void notificationsErased() + { + } + + // Virtual so it can be overridden in FakeMonitor. + virtual bool connectToNotificationManager(); + bool acceptNotification(const Protocol::ChangeNotification &msg) const; + void dispatchNotifications(); + void flushPipeline(); + + // Called when the monitored item/collection changes, checks if the queued messages + // are still accepted, if not they are removed + void cleanOldNotifications(); + + bool ensureDataAvailable(const Protocol::ChangeNotification &msg); + /** + * Sends out the change notification @p msg. + * @param msg the change notification to send + * @return @c true if the notification was actually send to someone, @c false if no one was listening. + */ + virtual bool emitNotification(const Protocol::ChangeNotification &msg); + void updatePendingStatistics(const Protocol::ChangeNotification &msg); + void invalidateCaches(const Protocol::ChangeNotification &msg); + + /** Used by ResourceBase to inform us about collection changes before the notifications are emitted, + needed to avoid the missing RID race on change replay. + */ + void invalidateCache(const Collection &col); + + /// Virtual so that ChangeRecorder can set it to 0 and handle the pipeline itself + virtual int pipelineSize() const; + + // private Q_SLOTS + void dataAvailable(); + void slotSessionDestroyed(QObject *object); + void slotStatisticsChangedFinished(KJob *job); + void slotFlushRecentlyChangedCollections(); + + /** + Returns whether a message was appended to @p notificationQueue + */ + int translateAndCompress(QQueue ¬ificationQueue, const Protocol::ChangeNotification &msg); + + virtual void slotNotify(const Protocol::ChangeNotification &msg); + + /** + * Sends out a change notification for an item. + * @return @c true if the notification was actually send to someone, @c false if no one was listening. + */ + bool emitItemsNotification(const Protocol::ChangeNotification &msg, const Item::List &items = Item::List(), + const Collection &collection = Collection(), const Collection &collectionDest = Collection()); + /** + * Sends out a change notification for a collection. + * @return @c true if the notification was actually send to someone, @c false if no one was listening. + */ + bool emitCollectionNotification(const Protocol::ChangeNotification &msg, const Collection &col = Collection(), + const Collection &par = Collection(), const Collection &dest = Collection()); + + bool emitTagsNotification(const Protocol::ChangeNotification &msg, const Tag::List &tags); + + bool emitRelationsNotification(const Protocol::ChangeNotification &msg, const Relation::List &relations); + + void serverStateChanged(Akonadi::ServerManager::State state); + + /** + * This method is called by the ChangeMediator to enforce an invalidation of the passed collection. + */ + void invalidateCollectionCache(qint64 collectionId); + + /** + * This method is called by the ChangeMediator to enforce an invalidation of the passed item. + */ + void invalidateItemCache(qint64 itemId); + + /** + * This method is called by the ChangeMediator to enforce an invalidation of the passed tag. + */ + void invalidateTagCache(qint64 tagId); + + /** + @brief Class used to determine when to purge items in a Collection + + The buffer method can be used to buffer a Collection. This may cause another Collection + to be purged if it is removed from the buffer. + + The purge method is used to purge a Collection from the buffer, but not the model. + This is used for example, to not buffer Collections anymore if they get referenced, + and to ensure that one Collection does not appear twice in the buffer. + + Check whether a Collection is buffered using the isBuffered method. + */ + class AKONADI_TESTS_EXPORT PurgeBuffer + { + // Buffer the most recent 10 unreferenced Collections + static const int MAXBUFFERSIZE = 10; + public: + explicit PurgeBuffer() + { + } + + /** + Adds @p id to the Collections to be buffered + + @returns The collection id which was removed form the buffer or -1 if none. + */ + Collection::Id buffer(Collection::Id id); + + /** + Removes @p id from the Collections being buffered + */ + void purge(Collection::Id id); + + bool isBuffered(Collection::Id id) const + { + return m_buffer.contains(id); + } + + static int buffersize(); + + private: + QQueue m_buffer; + } m_buffer; + + QHash refCountMap; + bool useRefCounting; + void ref(Collection::Id id); + Collection::Id deref(Collection::Id id); + + /** + * Returns true if the collection is monitored by monitor. + * + * A collection is always monitored if useRefCounting is false. + * If ref counting is used, the collection is only monitored, + * if the collection is either in refCountMap or m_buffer. + * If ref counting is used and the collection is not in refCountMap or m_buffer, + * no updates for the contained items are emitted, because they are lazily ignored. + */ + bool isMonitored(Collection::Id colId) const; + +private: + // collections that need a statistics update + QSet recentlyChangedCollections; + QTimer statisticsCompressionTimer; + + /** + @returns True if @p msg should be ignored. Otherwise appropriate signals are emitted for it. + */ + bool isLazilyIgnored(const Protocol::ChangeNotification &msg, bool allowModifyFlagsConversion = false) const; + + /** + Sets @p needsSplit to True when @p msg contains more than one item and there's at least one + listener that does not support batch operations. Sets @p batchSupported to True when + there's at least one listener that supports batch operations. + */ + void checkBatchSupport(const Protocol::ChangeNotification &msg, bool &needsSplit, bool &batchSupported) const; + + Protocol::ChangeNotification::List splitMessage(const Protocol::ChangeNotification &msg, bool legacy) const; + + bool isCollectionMonitored(Collection::Id collection) const + { + if (collection < 0) { + return false; + } + if (collections.contains(Collection(collection))) { + return true; + } + if (collections.contains(Collection::root())) { + return true; + } + return false; + } + + bool isMimeTypeMonitored(const QString &mimetype) const + { + if (mimetypes.contains(mimetype)) { + return true; + } + + const QMimeType mimeType = mimeDatabase.mimeTypeForName(mimetype); + if (!mimeType.isValid()) { + return false; + } + + foreach (const QString &mt, mimetypes) { + if (mimeType.inherits(mt)) { + return true; + } + } + + return false; + } + + bool isMoveDestinationResourceMonitored(const Protocol::ChangeNotification &msg) const + { + if (msg.operation() != Protocol::ChangeNotification::Move) { + return false; + } + return resources.contains(msg.destinationResource()); + } + + void fetchStatistics(Collection::Id colId) + { + CollectionStatisticsJob *job = new CollectionStatisticsJob(Collection(colId), session); + QObject::connect(job, SIGNAL(result(KJob *)), q_ptr, SLOT(slotStatisticsChangedFinished(KJob *))); + } + + void notifyCollectionStatisticsWatchers(Collection::Id collection, const QByteArray &resource); + bool fetchCollections() const; + bool fetchItems() const; +}; + +} + +#endif diff --git a/src/core/newmailnotifierattribute.cpp b/src/core/newmailnotifierattribute.cpp new file mode 100644 index 0000000..a83c3d8 --- /dev/null +++ b/src/core/newmailnotifierattribute.cpp @@ -0,0 +1,90 @@ +/* + Copyright (c) 2013, 2014 Montel Laurent + + 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 "newmailnotifierattribute.h" + +#include +#include +#include + +namespace Akonadi +{ + +class NewMailNotifierAttributePrivate +{ +public: + NewMailNotifierAttributePrivate() + : ignoreNewMail(false) + { + } + bool ignoreNewMail; +}; + +NewMailNotifierAttribute::NewMailNotifierAttribute() + : d(new NewMailNotifierAttributePrivate) +{ +} + +NewMailNotifierAttribute::~NewMailNotifierAttribute() +{ + delete d; +} + +NewMailNotifierAttribute *NewMailNotifierAttribute::clone() const +{ + NewMailNotifierAttribute *attr = new NewMailNotifierAttribute(); + attr->setIgnoreNewMail(ignoreNewMail()); + return attr; +} + +QByteArray NewMailNotifierAttribute::type() const +{ + static const QByteArray sType("newmailnotifierattribute"); + return sType; +} + +QByteArray NewMailNotifierAttribute::serialized() const +{ + QByteArray result; + QDataStream s(&result, QIODevice::WriteOnly); + s << ignoreNewMail(); + return result; +} + +void NewMailNotifierAttribute::deserialize(const QByteArray &data) +{ + QDataStream s(data); + s >> d->ignoreNewMail; +} + +bool NewMailNotifierAttribute::ignoreNewMail() const +{ + return d->ignoreNewMail; +} + +void NewMailNotifierAttribute::setIgnoreNewMail(bool b) +{ + d->ignoreNewMail = b; +} + +bool NewMailNotifierAttribute::operator==(const NewMailNotifierAttribute &other) const +{ + return d->ignoreNewMail == other.ignoreNewMail(); +} +} diff --git a/src/core/newmailnotifierattribute.h b/src/core/newmailnotifierattribute.h new file mode 100644 index 0000000..77fab72 --- /dev/null +++ b/src/core/newmailnotifierattribute.h @@ -0,0 +1,52 @@ +/* + Copyright (c) 2013, 2014 Montel Laurent + + 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 NEWMAILNOTIFIERATTRIBUTE_H +#define NEWMAILNOTIFIERATTRIBUTE_H + +#include "akonadicore_export.h" +#include + +namespace Akonadi +{ + +class NewMailNotifierAttributePrivate; +class AKONADICORE_EXPORT NewMailNotifierAttribute : public Akonadi::Attribute +{ +public: + NewMailNotifierAttribute(); + ~NewMailNotifierAttribute(); + + /* reimpl */ + NewMailNotifierAttribute *clone() const Q_DECL_OVERRIDE; + QByteArray type() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + + bool ignoreNewMail() const; + void setIgnoreNewMail(bool b); + bool operator==(const NewMailNotifierAttribute &other) const; + +private: + friend class NewMailNotifierAttributePrivate; + NewMailNotifierAttributePrivate *const d; +}; +} + +#endif // NEWMAILNOTIFIERATTRIBUTE_H diff --git a/src/core/notificationbus_p.cpp b/src/core/notificationbus_p.cpp new file mode 100644 index 0000000..8b50080 --- /dev/null +++ b/src/core/notificationbus_p.cpp @@ -0,0 +1,86 @@ +/* + Copyright (c) 2015 Daniel Vrátil + + 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 "notificationbus_p.h" +#include "session_p.h" +#include "connectionthread_p.h" +#include "akonadicore_debug.h" + +#include "private/protocol_p.h" + +#include + +using namespace Akonadi; + +NotificationBusPrivate::NotificationBusPrivate(Session *parent) + : QObject(parent) + , SessionPrivate(parent) +{ +} + +NotificationBusPrivate::~NotificationBusPrivate() +{ +} + +bool NotificationBusPrivate::handleCommand(qint64 tag, const Protocol::Command &cmd) +{ + Q_UNUSED(tag); + + if (cmd.type() == Protocol::Command::Hello) { + Protocol::HelloResponse hello(cmd); + if (hello.isError()) { + qCWarning(AKONADICORE_LOG) << "Error when establishing connection with Akonadi server:" << hello.errorMessage(); + connThread->disconnect(); + QTimer::singleShot(1000, mParent, SLOT(reconnect())); + return false; + } + + qCDebug(AKONADICORE_LOG) << "Connected to" << hello.serverName() << ", using protocol version" << hello.protocolVersion(); + qCDebug(AKONADICORE_LOG) << "Server says:" << hello.message(); + // Version mismatch is handled in SessionPrivate::startJob() so that + // we can report the error out via KJob API + protocolVersion = hello.protocolVersion(); + + Protocol::LoginCommand login(sessionId, Protocol::LoginCommand::NotificationBus); + sendCommand(nextTag(), login); + return true; + } + + if (cmd.type() == Protocol::Command::Login) { + Protocol::LoginResponse login(cmd); + if (login.isError()) { + qCWarning(AKONADICORE_LOG) << "Unable to login to Akonadi server:" << login.errorMessage(); + connThread->disconnect(); + QTimer::singleShot(1000, mParent, SLOT(reconnect())); + return false; + } + + connected = true; + startNext(); + return true; + } + + if (cmd.type() == Protocol::Command::ChangeNotification) { + Q_EMIT notify(cmd); + return true; + } + + qCWarning(AKONADICORE_LOG) << "Recieved invalid command on NotificationBus" << sessionId; + return false; +} diff --git a/src/core/notificationbus_p.h b/src/core/notificationbus_p.h new file mode 100644 index 0000000..9c568ca --- /dev/null +++ b/src/core/notificationbus_p.h @@ -0,0 +1,49 @@ +/* + Copyright (c) 2015 Daniel Vrátil + + 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_NOTIFICATIONBUS_P_H +#define AKONADI_NOTIFICATIONBUS_P_H + +#include "session_p.h" + +namespace Akonadi +{ + +namespace Protocol +{ +class ChangeNotification; +} + +class NotificationBusPrivate : public QObject, + public Akonadi::SessionPrivate +{ + Q_OBJECT + +public: + explicit NotificationBusPrivate(Session *parent = Q_NULLPTR); + ~NotificationBusPrivate(); + + bool handleCommand(qint64 tag, const Protocol::Command &cmd) Q_DECL_OVERRIDE; + +Q_SIGNALS: + void notify(const Akonadi::Protocol::ChangeNotification &ntf); +}; +} + +#endif // AKONADI_NOTIFICATIONBUS_P_H diff --git a/src/core/notificationsource_p.cpp b/src/core/notificationsource_p.cpp new file mode 100644 index 0000000..d30e2cd --- /dev/null +++ b/src/core/notificationsource_p.cpp @@ -0,0 +1,132 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "notificationsource_p.h" +#include "notificationsourceinterface.h" + +using namespace Akonadi; + +NotificationSource::NotificationSource(QObject *source) + : QObject(source) +{ + Q_ASSERT(source); +} + +NotificationSource::~NotificationSource() +{ +} + +QString NotificationSource::identifier() const +{ + org::freedesktop::Akonadi::NotificationSource *source = + qobject_cast(parent()); + return source->path(); +} + +void NotificationSource::setAllMonitored(bool allMonitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setAllMonitored", + Q_ARG(bool, allMonitored)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +void NotificationSource::setExclusive(bool exclusive) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setExclusive", + Q_ARG(bool, exclusive)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +void NotificationSource::setMonitoredCollection(Collection::Id id, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredCollection", + Q_ARG(qlonglong, id), + Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +void NotificationSource::setMonitoredItem(Item::Id id, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredItem", + Q_ARG(qlonglong, id), + Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +void NotificationSource::setMonitoredResource(const QByteArray &resource, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredResource", + Q_ARG(QByteArray, resource), + Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +void NotificationSource::setMonitoredMimeType(const QString &mimeType, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredMimeType", + Q_ARG(QString, mimeType), + Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +void NotificationSource::setIgnoredSession(const QByteArray &session, bool ignored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setIgnoredSession", + Q_ARG(QByteArray, session), + Q_ARG(bool, ignored)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +void NotificationSource::setMonitoredTag(Tag::Id id, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredTag", + Q_ARG(qlonglong, id), + Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +void NotificationSource::setMonitoredType(Protocol::ChangeNotification::Type type, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredType", + Q_ARG(Akonadi::Protocol::ChangeNotification::Type, type), + Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +void NotificationSource::setSession(const QByteArray &session) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setSession", + Q_ARG(QByteArray, session)); + Q_ASSERT(ok); + Q_UNUSED(ok); +} + +QObject *NotificationSource::source() const +{ + return parent(); +} diff --git a/src/core/notificationsource_p.h b/src/core/notificationsource_p.h new file mode 100644 index 0000000..037e05c --- /dev/null +++ b/src/core/notificationsource_p.h @@ -0,0 +1,62 @@ +/* + Copyright (c) 2013 Daniel Vrátil + + 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 NOTIFICATIONSOURCE_P_H +#define NOTIFICATIONSOURCE_P_H + +#include + +#include "akonaditests_export.h" + +#include "tag.h" +#include "collection.h" +#include "item.h" + +#include "private/protocol_p.h" + +namespace Akonadi +{ + +class AKONADI_TESTS_EXPORT NotificationSource : public QObject +{ + Q_OBJECT + +public: + NotificationSource(QObject *source); + ~NotificationSource(); + + QString identifier() const; + + void setAllMonitored(bool allMonitored); + void setExclusive(bool exclusive); + void setMonitoredCollection(Collection::Id id, bool monitored); + void setMonitoredItem(Item::Id id, bool monitored); + void setMonitoredResource(const QByteArray &resource, bool monitored); + void setMonitoredMimeType(const QString &mimeType, bool monitored); + void setMonitoredTag(Tag::Id id, bool monitored); + void setMonitoredType(Protocol::ChangeNotification::Type type, bool monitored); + void setIgnoredSession(const QByteArray &session, bool monitored); + void setSession(const QByteArray &session); + + QObject *source() const; +}; + +} + +#endif // NOTIFICATIONSOURCE_P_H diff --git a/src/core/partfetcher.cpp b/src/core/partfetcher.cpp new file mode 100644 index 0000000..adc52a4 --- /dev/null +++ b/src/core/partfetcher.cpp @@ -0,0 +1,184 @@ +/* + 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" +#include + +Q_DECLARE_METATYPE(QSet) + +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 *job); + + 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(i18n("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(i18n("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.apply(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)) +{ +} + +PartFetcher::~PartFetcher() +{ + delete d_ptr; +} + +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(i18n("Payload part '%1' is not available for this index", QString::fromLatin1(d->m_partName))); + emitResult(); + return; + } + + Akonadi::Session *session = qobject_cast(qvariant_cast(index.data(EntityTreeModel::SessionRole))); + + if (!session) { + setError(UserDefinedError); + setErrorText(i18n("No session available for this index")); + emitResult(); + return; + } + + const Akonadi::Item item = index.data(EntityTreeModel::ItemRole).value(); + + if (!item.isValid()) { + setError(UserDefinedError); + setErrorText(i18n("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 "moc_partfetcher.cpp" diff --git a/src/core/partfetcher.h b/src/core/partfetcher.h new file mode 100644 index 0000000..afbf8cc --- /dev/null +++ b/src/core/partfetcher.h @@ -0,0 +1,123 @@ +/* + 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_PARTFETCHER_H +#define AKONADI_PARTFETCHER_H + +#include + +#include "akonadicore_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 AKONADICORE_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. + */ + PartFetcher(const QModelIndex &index, const QByteArray &partName, QObject *parent = Q_NULLPTR); + + /** + * Destroys the part fetcher. + */ + virtual ~PartFetcher(); + + /** + * Starts the fetch operation. + */ + void start() Q_DECL_OVERRIDE; + + /** + * 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 *const d_ptr; + + Q_PRIVATE_SLOT(d_func(), void fetchJobDone(KJob *job)) + //@endcond +}; + +} + +#endif diff --git a/src/core/pastehelper.cpp b/src/core/pastehelper.cpp new file mode 100644 index 0000000..0230969 --- /dev/null +++ b/src/core/pastehelper.cpp @@ -0,0 +1,346 @@ +/* + 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 "pastehelper_p.h" + +#include "collectioncopyjob.h" +#include "collectionmovejob.h" +#include "collectionfetchjob.h" +#include "item.h" +#include "itemcreatejob.h" +#include "itemcopyjob.h" +#include "itemmodifyjob.h" +#include "itemmovejob.h" +#include "linkjob.h" +#include "transactionsequence.h" +#include "session.h" +#include "unlinkjob.h" + +#include "akonadicore_debug.h" + +#include +#include + +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +class PasteHelperJob : public Akonadi::TransactionSequence +{ + Q_OBJECT + +public: + explicit PasteHelperJob(Qt::DropAction action, const Akonadi::Item::List &items, + const Akonadi::Collection::List &collections, + const Akonadi::Collection &destination, + QObject *parent = 0); + virtual ~PasteHelperJob(); + +private Q_SLOTS: + void onDragSourceCollectionFetched(KJob *job); + +private: + void runActions(); + void runItemsActions(); + void runCollectionsActions(); + +private: + Qt::DropAction mAction; + Akonadi::Item::List mItems; + Akonadi::Collection::List mCollections; + Akonadi::Collection mDestCollection; +}; + +PasteHelperJob::PasteHelperJob(Qt::DropAction action, const Item::List &items, + const Collection::List &collections, + const Collection &destination, + QObject *parent) + : TransactionSequence(parent) + , mAction(action) + , mItems(items) + , mCollections(collections) + , mDestCollection(destination) +{ + //FIXME: The below code disables transactions in otder to avoid data loss due to nested + //transactions (copy and colcopy in the server doesn't see the items retrieved into the cache and copies empty payloads). + //Remove once this is fixed properly, see the other FIXME comments. + setProperty("transactionsDisabled", true); + + Collection dragSourceCollection; + using namespace std::placeholders; + if (!items.isEmpty() && items.first().parentCollection().isValid()) { + // Check if all items have the same parent collection ID + const Collection parent = items.first().parentCollection(); + if (std::find_if(items.constBegin(), items.constEnd(), + [parent](const Item &item) -> bool { + return item.parentCollection() != parent; + }) + == items.constEnd()) { + dragSourceCollection = parent; + } + + qCDebug(AKONADICORE_LOG) << items.first().parentCollection().id() << dragSourceCollection.id(); + } + + if (dragSourceCollection.isValid()) { + // Disable autocommitting, because starting a Link/Unlink/Copy/Move job + // after the transaction has ended leaves the job hanging + setAutomaticCommittingEnabled(false); + + CollectionFetchJob *fetch = new CollectionFetchJob(dragSourceCollection, + CollectionFetchJob::Base, + this); + QObject::connect(fetch, &KJob::finished, + this, &PasteHelperJob::onDragSourceCollectionFetched); + } else { + runActions(); + } +} + +PasteHelperJob::~PasteHelperJob() +{ +} + +void PasteHelperJob::onDragSourceCollectionFetched(KJob *job) +{ + CollectionFetchJob *fetch = qobject_cast(job); + qCDebug(AKONADICORE_LOG) << fetch->error() << fetch->collections().count(); + if (fetch->error() || fetch->collections().count() != 1) { + runActions(); + commit(); + return; + } + + // If the source collection is virtual, treat copy and move actions differently + const Collection sourceCollection = fetch->collections().at(0); + qCDebug(AKONADICORE_LOG) << "FROM: " << sourceCollection.id() << sourceCollection.name() << sourceCollection.isVirtual(); + qCDebug(AKONADICORE_LOG) << "DEST: " << mDestCollection.id() << mDestCollection.name() << mDestCollection.isVirtual(); + qCDebug(AKONADICORE_LOG) << "ACTN:" << mAction; + if (sourceCollection.isVirtual()) { + switch (mAction) { + case Qt::CopyAction: + if (mDestCollection.isVirtual()) { + new LinkJob(mDestCollection, mItems, this); + } else { + new ItemCopyJob(mItems, mDestCollection, this); + } + break; + case Qt::MoveAction: + new UnlinkJob(sourceCollection, mItems, this); + if (mDestCollection.isVirtual()) { + new LinkJob(mDestCollection, mItems, this); + } else { + new ItemCopyJob(mItems, mDestCollection, this); + } + break; + case Qt::LinkAction: + new LinkJob(mDestCollection, mItems, this); + break; + default: + Q_ASSERT(false); + } + runCollectionsActions(); + commit(); + } else { + runActions(); + } + + commit(); +} + +void PasteHelperJob::runActions() +{ + runItemsActions(); + runCollectionsActions(); +} + +void PasteHelperJob::runItemsActions() +{ + if (mItems.isEmpty()) { + return; + } + + switch (mAction) { + case Qt::CopyAction: + new ItemCopyJob(mItems, mDestCollection, this); + break; + case Qt::MoveAction: + new ItemMoveJob(mItems, mDestCollection, this); + break; + case Qt::LinkAction: + new LinkJob(mDestCollection, mItems, this); + break; + default: + Q_ASSERT(false); // WTF?! + } +} + +void PasteHelperJob::runCollectionsActions() +{ + if (mCollections.isEmpty()) { + return; + } + + switch (mAction) { + case Qt::CopyAction: + foreach (const Collection &col, mCollections) { // FIXME: remove once we have a batch job for collections as well + new CollectionCopyJob(col, mDestCollection, this); + } + break; + case Qt::MoveAction: + foreach (const Collection &col, mCollections) { // FIXME: remove once we have a batch job for collections as well + new CollectionMoveJob(col, mDestCollection, this); + } + break; + case Qt::LinkAction: + // Not supported for collections + break; + default: + Q_ASSERT(false); // WTF?! + } +} + +bool PasteHelper::canPaste(const QMimeData *mimeData, const Collection &collection) +{ + if (!mimeData || !collection.isValid()) { + return false; + } + + // check that the target collection has the rights to + // create the pasted items resp. collections + Collection::Rights neededRights = Collection::ReadOnly; + if (mimeData->hasUrls()) { + const QList urls = mimeData->urls(); + foreach (const QUrl &url, urls) { + const QUrlQuery query(url); + if (query.hasQueryItem(QStringLiteral("item"))) { + neededRights |= Collection::CanCreateItem; + } else if (query.hasQueryItem(QStringLiteral("collection"))) { + neededRights |= Collection::CanCreateCollection; + } + } + + if ((collection.rights() & neededRights) == 0) { + return false; + } + + // check that the target collection supports the mime types of the + // items/collections that shall be pasted + bool supportsMimeTypes = true; + foreach (const QUrl &url, urls) { + const QUrlQuery query(url); + // collections do not provide mimetype information, so ignore this check + if (query.hasQueryItem(QStringLiteral("collection"))) { + continue; + } + + const QString mimeType = query.queryItemValue(QStringLiteral("type")); + if (!collection.contentMimeTypes().contains(mimeType)) { + supportsMimeTypes = false; + break; + } + } + + if (!supportsMimeTypes) { + return false; + } + + return true; + } + + return false; +} + +KJob *PasteHelper::paste(const QMimeData *mimeData, const Collection &collection, bool copy, Session *session) +{ + if (!canPaste(mimeData, collection)) { + return 0; + } + + // we try to drop data not coming with the akonadi:// url + // find a type the target collection supports + foreach (const QString &type, mimeData->formats()) { + if (!collection.contentMimeTypes().contains(type)) { + continue; + } + + QByteArray item = mimeData->data(type); + // HACK for some unknown reason the data is sometimes 0-terminated... + if (!item.isEmpty() && item.at(item.size() - 1) == 0) { + item.resize(item.size() - 1); + } + + Item it; + it.setMimeType(type); + it.setPayloadFromData(item); + + ItemCreateJob *job = new ItemCreateJob(it, collection); + return job; + } + + if (!mimeData->hasUrls()) { + return 0; + } + + // data contains an url list + return pasteUriList(mimeData, collection, copy ? Qt::CopyAction : Qt::MoveAction, session); +} + +KJob *PasteHelper::pasteUriList(const QMimeData *mimeData, const Collection &destination, Qt::DropAction action, Session *session) +{ + if (!mimeData->hasUrls()) { + return 0; + } + + if (!canPaste(mimeData, destination)) { + return 0; + } + + const QList urls = mimeData->urls(); + Collection::List collections; + Item::List items; + foreach (const QUrl &url, urls) { + const QUrlQuery query(url); + const Collection collection = Collection::fromUrl(url); + if (collection.isValid()) { + collections.append(collection); + } + Item item = Item::fromUrl(url); + if (query.hasQueryItem(QStringLiteral("parent"))) { + item.setParentCollection(Collection(query.queryItemValue(QStringLiteral("parent")).toLongLong())); + } + if (item.isValid()) { + items.append(item); + } + // TODO: handle non Akonadi URLs? + } + + PasteHelperJob *job = new PasteHelperJob(action, items, + collections, destination, + session); + + return job; +} + +#include "pastehelper.moc" diff --git a/src/core/pastehelper_p.h b/src/core/pastehelper_p.h new file mode 100644 index 0000000..4f751ca --- /dev/null +++ b/src/core/pastehelper_p.h @@ -0,0 +1,73 @@ +/* + 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_PASTEHELPER_P_H +#define AKONADI_PASTEHELPER_P_H + +#include "akonadicore_export.h" +#include "collection.h" + +#include + +class KJob; +class QMimeData; + +namespace Akonadi +{ + +class Session; + +/** + @internal + + Helper methods for pasting/droping content into a collection. + + @todo Use in item/collection models as well for dnd +*/ +namespace PasteHelper +{ +/** + Check whether the given mime data can be pasted into the given collection. + @param mimeData The pasted/dropped data. + @param collection The collection to paste/drop into. +*/ +AKONADICORE_EXPORT bool canPaste(const QMimeData *mimeData, const Collection &collection); + +/** + Paste/drop the given mime data into the given collection. + @param mimeData The pasted/dropped data. + @param collection The target collection. + @param copy Indicate whether this is a copy or a move. + @returns The job performing the paste, 0 if there is nothing to paste. +*/ +AKONADICORE_EXPORT KJob *paste(const QMimeData *mimeData, const Collection &collection, bool copy = true, Session *session = 0); + +/** + URI list paste/drop. + @param mimeData The pasted/dropped data. + @param collection The target collection. + @param action The drop action (copy/move/link). + @returns The job performing the paste, 0 if there is nothing to paste. +*/ +AKONADICORE_EXPORT KJob *pasteUriList(const QMimeData *mimeData, const Collection &collection, Qt::DropAction action, Session *session = 0); +} + +} + +#endif diff --git a/src/core/persistentsearchattribute.cpp b/src/core/persistentsearchattribute.cpp new file mode 100644 index 0000000..6eefaa1 --- /dev/null +++ b/src/core/persistentsearchattribute.cpp @@ -0,0 +1,168 @@ +/* + Copyright (c) 2010 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 "persistentsearchattribute.h" +#include "collection.h" + +#include "private/imapparser_p.h" + +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN PersistentSearchAttribute::Private +{ +public: + Private() + : remote(false) + , recursive(false) + { + } + + QString queryString; + QList queryCollections; + bool remote; + bool recursive; +}; + +PersistentSearchAttribute::PersistentSearchAttribute() + : d(new Private) +{ +} + +PersistentSearchAttribute::~PersistentSearchAttribute() +{ + delete d; +} + +QString PersistentSearchAttribute::queryString() const +{ + return d->queryString; +} + +void PersistentSearchAttribute::setQueryString(const QString &query) +{ + d->queryString = query; +} + +QList PersistentSearchAttribute::queryCollections() const +{ + return d->queryCollections; +} + +void PersistentSearchAttribute::setQueryCollections(const QVector &collections) +{ + d->queryCollections.clear(); + Q_FOREACH (const Collection &collection, collections) { + d->queryCollections << collection.id(); + } +} + +void PersistentSearchAttribute::setQueryCollections(const QList &collectionsIds) +{ + d->queryCollections = collectionsIds; +} + +bool PersistentSearchAttribute::isRecursive() const +{ + return d->recursive; +} + +void PersistentSearchAttribute::setRecursive(bool recursive) +{ + d->recursive = recursive; +} + +bool PersistentSearchAttribute::isRemoteSearchEnabled() const +{ + return d->remote; +} + +void PersistentSearchAttribute::setRemoteSearchEnabled(bool enabled) +{ + d->remote = enabled; +} + +QByteArray PersistentSearchAttribute::type() const +{ + static const QByteArray sType("PERSISTENTSEARCH"); + return sType; +} + +Attribute *PersistentSearchAttribute::clone() const +{ + PersistentSearchAttribute *attr = new PersistentSearchAttribute; + attr->setQueryString(queryString()); + attr->setQueryCollections(queryCollections()); + attr->setRecursive(isRecursive()); + attr->setRemoteSearchEnabled(isRemoteSearchEnabled()); + return attr; +} + +QByteArray PersistentSearchAttribute::serialized() const +{ + QStringList cols; + cols.reserve(d->queryCollections.count()); + Q_FOREACH (qint64 colId, d->queryCollections) { + cols << QString::number(colId); + } + + QList l; + // ### eventually replace with the AKONADI_PARAM_PERSISTENTSEARCH_XXX constants + l.append("QUERYSTRING"); + l.append(ImapParser::quote(d->queryString.toUtf8())); + l.append("QUERYCOLLECTIONS"); + l.append("(" + cols.join(QStringLiteral(" ")).toLatin1() + ")"); + if (d->remote) { + l.append("REMOTE"); + } + if (d->recursive) { + l.append("RECURSIVE"); + } + return "(" + ImapParser::join(l, " ") + ')'; //krazy:exclude=doublequote_chars +} + +void PersistentSearchAttribute::deserialize(const QByteArray &data) +{ + QList l; + ImapParser::parseParenthesizedList(data, l); + for (int i = 0; i < l.size(); ++i) { + const QByteArray key = l.at(i); + if (key == "QUERYLANGUAGE") { + // Skip the value + ++i; + } else if (key == "QUERYSTRING") { + d->queryString = QString::fromUtf8(l.at(i + 1)); + ++i; + } else if (key == "QUERYCOLLECTIONS") { + QList ids; + ImapParser::parseParenthesizedList(l.at(i + 1), ids); + d->queryCollections.clear(); + Q_FOREACH (const QByteArray &id, ids) { + d->queryCollections << id.toLongLong(); + } + ++i; + } else if (key == "REMOTE") { + d->remote = true; + } else if (key == "RECURSIVE") { + d->recursive = true; + } + } +} diff --git a/src/core/persistentsearchattribute.h b/src/core/persistentsearchattribute.h new file mode 100644 index 0000000..6bfdbe0 --- /dev/null +++ b/src/core/persistentsearchattribute.h @@ -0,0 +1,180 @@ +/* + Copyright (c) 2010 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_PERSISTENTSEARCHATTRIBUTE_H +#define AKONADI_PERSISTENTSEARCHATTRIBUTE_H + +#include "akonadicore_export.h" +#include "attribute.h" + +namespace Akonadi +{ + +class Collection; + +/** + * @short An attribute to store query properties of persistent search collections. + * + * This attribute is attached to persistent search collections automatically when + * creating a new persistent search with SearchCreateJob. + * Later on the search query can be changed by modifying this attribute of the + * persistent search collection with an CollectionModifyJob. + * + * Example: + * + * @code + * + * const QString name = "My search folder"; + * const QString query = "..."; + * + * Akonadi::SearchCreateJob *job = new Akonadi::SearchCreateJob( name, query ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * const Collection searchCollection = job->createdCollection(); + * ... + * + * // now let's change the query + * if ( searchCollection.hasAttribute() ) { + * Akonadi::PersistentSearchAttribute *attribute = searchCollection.attribute(); + * attribute->setQueryString( "... another query string ..." ); + * + * Akonadi::CollectionModifyJob *modifyJob = new Akonadi::CollectionModifyJob( searchCollection ); + * connect( modifyJob, SIGNAL(result(KJob*)), SLOT(modifyFinished(KJob*)) ); + * } + * ... + * } + * + * @endcode + * + * @author Volker Krause + * @since 4.5 + */ +class AKONADICORE_EXPORT PersistentSearchAttribute : public Akonadi::Attribute +{ +public: + /** + * Creates a new persistent search attribute. + */ + PersistentSearchAttribute(); + + /** + * Destroys the persistent search attribute. + */ + ~PersistentSearchAttribute(); + + /** + * Returns the query string used for this search. + */ + QString queryString() const; + + /** + * Sets the query string to be used for this search. + * @param query The query string. + */ + void setQueryString(const QString &query); + + /** + * Returns IDs of collections that will be queried + * @since 4.13 + */ + QList queryCollections() const; + + /** + * Sets collections to be queried. + * @param collections List of collections to be queries + * @since 4.13 + */ + void setQueryCollections(const QVector &collections); + + /** + * Sets IDs of collections to be queries + * @param collectionsIDs IDs of collections to query + * @since 4.13 + */ + void setQueryCollections(const QList &collectionsIds); + + /** + * Sets whether resources should be queried too. + * + * When set to true, Akonadi will search local indexed items and will also + * query resources that support server-side search, to forward the query + * to remote storage (for example using SEARCH feature on IMAP servers) and + * merge their results with results from local index. + * + * This is useful especially when searching resources, that don't fetch full + * payload by default, for example the IMAP resource, which only fetches headers + * by default and the body is fetched on demand, which means that emails that + * were not yet fully fetched cannot be indexed in local index, and thus cannot + * be searched. With remote search, even those emails can be included in search + * results. + * + * @param enabled Whether remote search is enabled + * @since 4.13 + */ + void setRemoteSearchEnabled(bool enabled); + + /** + * Returns whether remote search is enabled. + * + * @since 4.13 + */ + bool isRemoteSearchEnabled() const; + + /** + * Sets whether the search should recurse into collections + * + * When set to true, all child collections of the specific collections will + * be search recursively. + * + * @param recursive Whether to search recursively + * @since 4.13 + */ + void setRecursive(bool recursive); + + /** + * Returns whether the search is recursive + * + * @since 4.13 + */ + bool isRecursive() const; + + //@cond PRIVATE + QByteArray type() const Q_DECL_OVERRIDE; + Attribute *clone() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + //@endcond + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/pluginloader.cpp b/src/core/pluginloader.cpp new file mode 100644 index 0000000..b6b6bac --- /dev/null +++ b/src/core/pluginloader.cpp @@ -0,0 +1,188 @@ +/* -*- c++ -*- + 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. +*/ + +#include "pluginloader_p.h" +#include "akonadicore_debug.h" +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +PluginMetaData::PluginMetaData() +{ +} + +PluginMetaData::PluginMetaData(const QString &lib, const QString &name, const QString &comment, const QString &cname) + : library(lib) + , nameLabel(name) + , descriptionLabel(comment) + , className(cname) + , loaded(false) +{ +} + +PluginLoader *PluginLoader::mSelf = Q_NULLPTR; + +PluginLoader::PluginLoader() +{ + scan(); +} + +PluginLoader::~PluginLoader() +{ + qDeleteAll(mPluginLoaders); + mPluginLoaders.clear(); +} + +PluginLoader *PluginLoader::self() +{ + if (!mSelf) { + mSelf = new PluginLoader(); + } + + return mSelf; +} + +QStringList PluginLoader::names() const +{ + return mPluginInfos.keys(); +} + +QObject *PluginLoader::createForName(const QString &name) +{ + if (!mPluginInfos.contains(name)) { + qCWarning(AKONADICORE_LOG) << "plugin name \"" << name << "\" is unknown to the plugin loader." << endl; + return 0; + } + + PluginMetaData &info = mPluginInfos[name]; + + //First try to load it staticly + foreach (QObject *plugin, QPluginLoader::staticInstances()) { + if (QLatin1String(plugin->metaObject()->className()) == info.className) { + info.loaded = true; + return plugin; + break; + } + } + + if (!info.loaded) { + QPluginLoader *loader = new QPluginLoader(info.library); + if (loader->fileName().isEmpty()) { + qCWarning(AKONADICORE_LOG) << loader->errorString(); + delete loader; + return 0; + } + + mPluginLoaders.insert(name, loader); + info.loaded = true; + } + + QPluginLoader *loader = mPluginLoaders.value(name); + Q_ASSERT(loader); + + QObject *object = loader->instance(); + if (!object) { + qCWarning(AKONADICORE_LOG) << "unable to load plugin" << info.library << "for plugin name" << name << "."; + qCWarning(AKONADICORE_LOG) << "Error was:\"" << loader->errorString() << "\"."; + return 0; + } + + return object; +} + +PluginMetaData PluginLoader::infoForName(const QString &name) const +{ + if (!mPluginInfos.contains(name)) { + return PluginMetaData(); + } + + return mPluginInfos.value(name); +} + +void PluginLoader::scan() +{ + const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("akonadi/plugins/serializer/"), QStandardPaths::LocateDirectory); + Q_FOREACH (const QString &dir, dirs) { + const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.desktop")); + Q_FOREACH (const QString &file, fileNames) { + const QString entry = dir + QLatin1Char('/') + file; + KConfig config(entry, KConfig::SimpleConfig); + if (config.hasGroup("Misc") && config.hasGroup("Plugin")) { + KConfigGroup group(&config, "Plugin"); + + const QString type = group.readEntry("Type").toLower(); + if (type.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Plugin]Type value in" << entry << "- skipping"; + continue; + } + + // read Class entry as a list so that types like QPair are + // properly escaped and don't end up being split into QPair. + const QStringList classes = group.readXdgListEntry("X-Akonadi-Class"); + if (classes.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Plugin]X-Akonadi-Class value in" << entry << "- skipping"; + continue; + } + + const QString library = group.readEntry("X-KDE-Library"); + if (library.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Plugin]X-KDE-Library value in" << entry << "- skipping"; + continue; + } + + KConfigGroup group2(&config, "Misc"); + + QString name = group2.readEntry("Name"); + if (name.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Misc]Name value in \"" << entry << "\" - inserting default name" << endl; + name = i18n("Unnamed plugin"); + } + + QString comment = group2.readEntry("Comment"); + if (comment.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Misc]Comment value in \"" << entry << "\" - inserting default name" << endl; + comment = i18n("No description available"); + } + + QString cname = group.readEntry("X-KDE-ClassName"); + if (cname.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty X-KDE-ClassName value in \"" << entry << "\"" << endl; + } + + const QStringList mimeTypes = type.split(QLatin1Char(','), QString::SkipEmptyParts); + + qCDebug(AKONADICORE_LOG) << "registering Desktop file" << entry << "for" << mimeTypes << '@' << classes; + Q_FOREACH (const QString &mimeType, mimeTypes) { + Q_FOREACH (const QString &classType, classes) { + mPluginInfos.insert(mimeType + QLatin1Char('@') + classType, PluginMetaData(library, name, comment, cname)); + } + } + + } else { + qCWarning(AKONADICORE_LOG) << "Desktop file \"" << entry << "\" doesn't seem to describe a plugin " << "(misses Misc and/or Plugin group)" << endl; + } + } + } +} diff --git a/src/core/pluginloader_p.h b/src/core/pluginloader_p.h new file mode 100644 index 0000000..43df72e --- /dev/null +++ b/src/core/pluginloader_p.h @@ -0,0 +1,73 @@ +/* -*- c++ -*- + 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_PLUGINLOADER_P_H +#define AKONADI_PLUGINLOADER_P_H + +#include "akonaditests_export.h" + +#include +#include +#include + +class QPluginLoader; + +namespace Akonadi +{ + +class AKONADI_TESTS_EXPORT PluginMetaData +{ +public: + PluginMetaData(); + PluginMetaData(const QString &lib, const QString &name, const QString &comment, const QString &cname); + + QString library; + QString nameLabel; + QString descriptionLabel; + QString className; + bool loaded; +}; + +class AKONADI_TESTS_EXPORT PluginLoader +{ +public: + ~PluginLoader(); + + static PluginLoader *self(); + + QStringList names() const; + + QObject *createForName(const QString &name); + + PluginMetaData infoForName(const QString &name) const; + + void scan(); + +private: + Q_DISABLE_COPY(PluginLoader) + PluginLoader(); + + static PluginLoader *mSelf; + QHash mPluginLoaders; + QHash mPluginInfos; +}; + +} + +#endif diff --git a/src/core/pop3resourceattribute.cpp b/src/core/pop3resourceattribute.cpp new file mode 100644 index 0000000..2ca1421 --- /dev/null +++ b/src/core/pop3resourceattribute.cpp @@ -0,0 +1,90 @@ +/* + Copyright (c) 2013, 2014 Montel Laurent + + 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 "pop3resourceattribute.h" + +#include +#include +#include +namespace Akonadi +{ + +class Pop3ResourceAttributePrivate +{ +public: + Pop3ResourceAttributePrivate() + { + } + QString accountName; +}; + +Pop3ResourceAttribute::Pop3ResourceAttribute() + : d(new Pop3ResourceAttributePrivate) +{ +} + +Pop3ResourceAttribute::~Pop3ResourceAttribute() +{ + delete d; +} + +Pop3ResourceAttribute *Pop3ResourceAttribute::clone() const +{ + Pop3ResourceAttribute *attr = new Pop3ResourceAttribute(); + attr->setPop3AccountName(pop3AccountName()); + return attr; +} + +QByteArray Pop3ResourceAttribute::type() const +{ + static const QByteArray sType("pop3resourceattribute"); + return sType; +} + +QByteArray Pop3ResourceAttribute::serialized() const +{ + QByteArray result; + QDataStream s(&result, QIODevice::WriteOnly); + s << pop3AccountName(); + return result; +} + +void Pop3ResourceAttribute::deserialize(const QByteArray &data) +{ + QDataStream s(data); + QString value; + s >> value; + d->accountName = value; +} + +QString Pop3ResourceAttribute::pop3AccountName() const +{ + return d->accountName; +} + +void Pop3ResourceAttribute::setPop3AccountName(const QString &accountName) +{ + d->accountName = accountName; +} + +bool Pop3ResourceAttribute::operator==(const Pop3ResourceAttribute &other) const +{ + return d->accountName == other.pop3AccountName(); +} +} diff --git a/src/core/pop3resourceattribute.h b/src/core/pop3resourceattribute.h new file mode 100644 index 0000000..3679ce0 --- /dev/null +++ b/src/core/pop3resourceattribute.h @@ -0,0 +1,51 @@ +/* + Copyright (c) 2013, 2014 Montel Laurent + + 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 AKONADICORE_POP3RESOURCEATTRIBUTE_H +#define AKONADICORE_POP3RESOURCEATTRIBUTE_H + +#include +#include "akonadicore_export.h" + +namespace Akonadi +{ + +class Pop3ResourceAttributePrivate; +class AKONADICORE_EXPORT Pop3ResourceAttribute : public Akonadi::Attribute +{ +public: + Pop3ResourceAttribute(); + ~Pop3ResourceAttribute(); + + /* reimpl */ + Pop3ResourceAttribute *clone() const Q_DECL_OVERRIDE; + QByteArray type() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + + QString pop3AccountName() const; + void setPop3AccountName(const QString &accountName); + + bool operator==(const Pop3ResourceAttribute &other) const; +private: + friend class Pop3ResourceAttributePrivate; + Pop3ResourceAttributePrivate *const d; +}; +} +#endif // AKONADICORE_POP3RESOURCEATTRIBUTE_H diff --git a/src/core/protocolhelper.cpp b/src/core/protocolhelper.cpp new file mode 100644 index 0000000..f873a50 --- /dev/null +++ b/src/core/protocolhelper.cpp @@ -0,0 +1,582 @@ +/* + 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 "akonadicore_debug.h" +#include "attributefactory.h" +#include "collectionstatistics.h" +#include "item_p.h" +#include "collection_p.h" +#include "exception.h" +#include "itemserializer_p.h" +#include "itemserializerplugin.h" +#include "servermanager.h" +#include "tagfetchscope.h" +#include "persistentsearchattribute.h" + +#include "private/protocol_p.h" +#include "private/xdgbasedirs_p.h" +#include "private/externalpartstorage_p.h" + +#include +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +CachePolicy ProtocolHelper::parseCachePolicy(const Protocol::CachePolicy &policy) +{ + CachePolicy cp; + cp.setCacheTimeout(policy.cacheTimeout()); + cp.setIntervalCheckTime(policy.checkInterval()); + cp.setInheritFromParent(policy.inherit()); + cp.setSyncOnDemand(policy.syncOnDemand()); + cp.setLocalParts(policy.localParts()); + return cp; +} + +Protocol::CachePolicy ProtocolHelper::cachePolicyToProtocol(const CachePolicy &policy) +{ + Protocol::CachePolicy proto; + proto.setCacheTimeout(policy.cacheTimeout()); + proto.setCheckInterval(policy.intervalCheckTime()); + proto.setInherit(policy.inheritFromParent()); + proto.setSyncOnDemand(policy.syncOnDemand()); + proto.setLocalParts(policy.localParts()); + return proto; +} + + +template +inline static void parseAttributesImpl(const Protocol::Attributes &attributes, T *entity) +{ + for (auto iter = attributes.cbegin(), end = attributes.cend(); iter != end; ++iter) { + Attribute *attribute = AttributeFactory::createAttribute(iter.key()); + if (!attribute) { + qCWarning(AKONADICORE_LOG) << "Warning: unknown attribute" << iter.key(); + continue; + } + attribute->deserialize(iter.value()); + entity->addAttribute(attribute); + } +} + +template +inline static void parseAncestorsCachedImpl(const QVector &ancestors, T *entity, + Collection::Id parentCollection, + ProtocolHelperValuePool *pool) +{ + if (!pool || parentCollection == -1) { + // if no pool or parent collection id is provided we can't cache anything, so continue as usual + ProtocolHelper::parseAncestors(ancestors, entity); + return; + } + + if (pool->ancestorCollections.contains(parentCollection)) { + // ancestor chain is cached already, so use the cached value + entity->setParentCollection(pool->ancestorCollections.value(parentCollection)); + } else { + // not cached yet, parse the chain + ProtocolHelper::parseAncestors(ancestors, entity); + pool->ancestorCollections.insert(parentCollection, entity->parentCollection()); + } +} + +template +inline static Protocol::Attributes attributesToProtocolImpl(const T &entity, bool ns) +{ + Protocol::Attributes attributes; + Q_FOREACH (const Attribute *attr, entity.attributes()) { + attributes.insert(ProtocolHelper::encodePartIdentifier(ns ? ProtocolHelper::PartAttribute : ProtocolHelper::PartGlobal, attr->type()), + attr->serialized()); + } + return attributes; +} + +void ProtocolHelper::parseAncestorsCached(const QVector &ancestors, + Item *item, Collection::Id parentCollection, + ProtocolHelperValuePool *pool) +{ + parseAncestorsCachedImpl(ancestors, item, parentCollection, pool); +} + +void ProtocolHelper::parseAncestorsCached(const QVector &ancestors, + Collection *collection, Collection::Id parentCollection, + ProtocolHelperValuePool *pool) +{ + parseAncestorsCachedImpl(ancestors, collection, parentCollection, pool); +} + +void ProtocolHelper::parseAncestors(const QVector &ancestors, Item *item) +{ + Collection fakeCollection; + parseAncestors(ancestors, &fakeCollection); + + item->setParentCollection(fakeCollection.parentCollection()); +} + +void ProtocolHelper::parseAncestors(const QVector &ancestors, Collection *collection) +{ + static const Collection::Id rootCollectionId = Collection::root().id(); + QList parentIds; + + Collection *current = collection; + Q_FOREACH (const Protocol::Ancestor &ancestor, ancestors) { + if (ancestor.id() == rootCollectionId) { + current->setParentCollection(Collection::root()); + break; + } + + Akonadi::Collection parentCollection(ancestor.id()); + parentCollection.setName(ancestor.name()); + parentCollection.setRemoteId(ancestor.remoteId()); + parseAttributesImpl(ancestor.attributes(), &parentCollection); + current->setParentCollection(parentCollection); + current = ¤t->parentCollection(); + } +} + +static Collection::ListPreference parsePreference(Tristate value) +{ + switch (value) { + case Tristate::True: + return Collection::ListEnabled; + case Tristate::False: + return Collection::ListDisabled; + case Tristate::Undefined: + return Collection::ListDefault; + } + + Q_ASSERT(false); + return Collection::ListDefault; +} + +CollectionStatistics ProtocolHelper::parseCollectionStatistics(const Protocol::FetchCollectionStatsResponse &stats) +{ + CollectionStatistics cs; + cs.setCount(stats.count()); + cs.setSize(stats.size()); + cs.setUnreadCount(stats.unseen()); + return cs; +} + +void ProtocolHelper::parseAttributes(const Protocol::Attributes &attributes, Item *item) +{ + parseAttributesImpl(attributes, item); +} + +void ProtocolHelper::parseAttributes(const Protocol::Attributes &attributes, Collection *collection) +{ + parseAttributesImpl(attributes, collection); +} + +void ProtocolHelper::parseAttributes(const Protocol::Attributes &attributes, Tag *tag) +{ + parseAttributesImpl(attributes, tag); +} + +Protocol::Attributes ProtocolHelper::attributesToProtocol(const Item &item, bool ns) +{ + return attributesToProtocolImpl(item, ns); +} + +Protocol::Attributes ProtocolHelper::attributesToProtocol(const Collection &collection, bool ns) +{ + return attributesToProtocolImpl(collection, ns); +} + +Protocol::Attributes ProtocolHelper::attributesToProtocol(const Tag &tag, bool ns) +{ + return attributesToProtocolImpl(tag, ns); +} + +Collection ProtocolHelper::parseCollection(const Protocol::FetchCollectionsResponse &data, bool requireParent) +{ + Collection collection(data.id()); + + if (requireParent) { + collection.setParentCollection(Collection(data.parentId())); + } + + collection.setName(data.name()); + collection.setRemoteId(data.remoteId()); + collection.setRemoteRevision(data.remoteRevision()); + collection.setResource(data.resource()); + collection.setContentMimeTypes(data.mimeTypes()); + collection.setVirtual(data.isVirtual()); + collection.setStatistics(parseCollectionStatistics(data.statistics())); + collection.setCachePolicy(parseCachePolicy(data.cachePolicy())); + parseAncestors(data.ancestors(), &collection); + collection.setEnabled(data.enabled()); + collection.setLocalListPreference(Collection::ListDisplay, parsePreference(data.displayPref())); + collection.setLocalListPreference(Collection::ListIndex, parsePreference(data.indexPref())); + collection.setLocalListPreference(Collection::ListSync, parsePreference(data.syncPref())); + collection.setReferenced(data.referenced()); + + if (!data.searchQuery().isEmpty()) { + auto attr = collection.attribute(Collection::AddIfMissing); + attr->setQueryString(data.searchQuery()); + + QVector cols; + cols.reserve(data.searchCollections().size()); + foreach (auto id, data.searchCollections()) { + cols.push_back(Collection(id)); + } + attr->setQueryCollections(cols); + } + + parseAttributes(data.attributes(), &collection); + + collection.d_ptr->resetChangeLog(); + return collection; +} + +QByteArray ProtocolHelper::encodePartIdentifier(PartNamespace ns, const QByteArray &label) +{ + switch (ns) { + case PartGlobal: + return label; + case PartPayload: + return "PLD:" + label; + case PartAttribute: + return "ATR:" + label; + 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; + } +} + +Protocol::ScopeContext ProtocolHelper::commandContextToProtocol(const Akonadi::Collection &collection, + const Akonadi::Tag &tag, + const Item::List &requestedItems) +{ + Protocol::ScopeContext ctx; + if (tag.isValid()) { + ctx.setContext(Protocol::ScopeContext::Tag, tag.id()); + } + + if (collection == Collection::root()) { + if (requestedItems.isEmpty() && !tag.isValid()) { // collection content listing + throw Exception("Cannot perform item operations on root collection."); + } + } else { + if (collection.isValid()) { + ctx.setContext(Protocol::ScopeContext::Collection, collection.id()); + } else if (!collection.remoteId().isEmpty()) { + ctx.setContext(Protocol::ScopeContext::Collection, collection.remoteId()); + } + } + + return ctx; +} + +Scope ProtocolHelper::hierarchicalRidToScope(const Collection &col) +{ + if (col == Collection::root()) { + return Scope({ Scope::HRID(0) }); + } + if (col.remoteId().isEmpty()) { + return Scope(); + } + + QVector chain; + Collection c = col; + while (!c.remoteId().isEmpty()) { + chain.append(Scope::HRID(c.id(), c.remoteId())); + c = c.parentCollection(); + } + return Scope(chain + QVector { Scope::HRID(0) }); +} + +Scope ProtocolHelper::hierarchicalRidToScope(const Item &item) +{ + return Scope(QVector({ Scope::HRID(item.id(), item.remoteId()) }) + hierarchicalRidToScope(item.parentCollection()).hridChain()); +} + +Protocol::FetchScope ProtocolHelper::itemFetchScopeToProtocol(const ItemFetchScope &fetchScope) +{ + Protocol::FetchScope fs; + QVector parts; + parts.reserve(fetchScope.payloadParts().size() + fetchScope.attributes().size()); + Q_FOREACH (const QByteArray &part, fetchScope.payloadParts()) { + parts << ProtocolHelper::encodePartIdentifier(ProtocolHelper::PartPayload, part); + } + Q_FOREACH (const QByteArray &part, fetchScope.attributes()) { + parts << ProtocolHelper::encodePartIdentifier(ProtocolHelper::PartAttribute, part); + } + fs.setRequestedParts(parts); + + // The default scope + fs.setFetch(Protocol::FetchScope::Flags | + Protocol::FetchScope::Size | + Protocol::FetchScope::RemoteID | + Protocol::FetchScope::RemoteRevision | + Protocol::FetchScope::MTime); + + fs.setFetch(Protocol::FetchScope::FullPayload, fetchScope.fullPayload()); + fs.setFetch(Protocol::FetchScope::AllAttributes, fetchScope.allAttributes()); + fs.setFetch(Protocol::FetchScope::CacheOnly, fetchScope.cacheOnly()); + fs.setFetch(Protocol::FetchScope::CheckCachedPayloadPartsOnly, fetchScope.checkForCachedPayloadPartsOnly()); + fs.setFetch(Protocol::FetchScope::IgnoreErrors, fetchScope.ignoreRetrievalErrors()); + if (fetchScope.ancestorRetrieval() != ItemFetchScope::None) { + switch (fetchScope.ancestorRetrieval()) { + case ItemFetchScope::Parent: + fs.setAncestorDepth(Protocol::Ancestor::ParentAncestor); + break; + case ItemFetchScope::All: + fs.setAncestorDepth(Protocol::Ancestor::AllAncestors); + break; + default: + Q_ASSERT(false); + } + } else { + fs.setAncestorDepth(Protocol::Ancestor::NoAncestor); + } + + if (fetchScope.fetchChangedSince().isValid()) { + fs.setChangedSince(fetchScope.fetchChangedSince()); + } + + fs.setFetch(Protocol::FetchScope::RemoteID, fetchScope.fetchRemoteIdentification()); + fs.setFetch(Protocol::FetchScope::RemoteRevision, fetchScope.fetchRemoteIdentification()); + fs.setFetch(Protocol::FetchScope::GID, fetchScope.fetchGid()); + if (fetchScope.fetchTags()) { + fs.setFetch(Protocol::FetchScope::Tags); + if (!fetchScope.tagFetchScope().fetchIdOnly()) { + if (fetchScope.tagFetchScope().attributes().isEmpty()) { + fs.setTagFetchScope({ "ALL" }); + } else { + fs.setTagFetchScope(fetchScope.tagFetchScope().attributes()); + } + } + } + + fs.setFetch(Protocol::FetchScope::VirtReferences, fetchScope.fetchVirtualReferences()); + fs.setFetch(Protocol::FetchScope::MTime, fetchScope.fetchModificationTime()); + fs.setFetch(Protocol::FetchScope::Relations, fetchScope.fetchRelations()); + + return fs; +} + +static Item::Flags convertFlags(const QVector &flags, ProtocolHelperValuePool *valuePool) +{ +#if __cplusplus >= 201103L || defined(__GNUC__) || defined(__clang__) + // When the compiler supports thread-safe static initialization (mandated by the C++11 memory model) + // then use it to share the common case of a single-item set only containing the \SEEN flag. + // NOTE: GCC and clang has threadsafe static initialization for some time now, even without C++11. + if (flags.size() == 1 && flags.first() == "\\SEEN") { + static const Item::Flags sharedSeen = Item::Flags() << QByteArray("\\SEEN"); + return sharedSeen; + } +#endif + + Item::Flags convertedFlags; + convertedFlags.reserve(flags.size()); + foreach (const QByteArray &flag, flags) { + if (valuePool) { + convertedFlags.insert(valuePool->flagPool.sharedValue(flag)); + } else { + convertedFlags.insert(flag); + } + } + return convertedFlags; +} + +Item ProtocolHelper::parseItemFetchResult(const Protocol::FetchItemsResponse &data, ProtocolHelperValuePool *valuePool) +{ + Item item; + item.setId(data.id()); + item.setRevision(data.revision()); + item.setRemoteId(data.remoteId()); + item.setRemoteRevision(data.remoteRevision()); + item.setGid(data.gid()); + item.setStorageCollectionId(data.parentId()); + + if (valuePool) { + item.setMimeType(valuePool->mimeTypePool.sharedValue(data.mimeType())); + } else { + item.setMimeType(data.mimeType()); + } + + if (!item.isValid()) { + return Item(); + } + + item.setFlags(convertFlags(data.flags(), valuePool)); + + if (!data.tags().isEmpty()) { + Tag::List tags; + tags.reserve(data.tags().size()); + Q_FOREACH (const Protocol::FetchTagsResponse &tag, data.tags()) { + tags.append(parseTagFetchResult(tag)); + } + item.setTags(tags); + } + + if (!data.relations().isEmpty()) { + Relation::List relations; + relations.reserve(data.relations().size()); + Q_FOREACH (const Protocol::FetchRelationsResponse &rel, data.relations()) { + relations.append(parseRelationFetchResult(rel)); + } + item.d_ptr->mRelations = relations; + } + + if (!data.virtualReferences().isEmpty()) { + Collection::List virtRefs; + virtRefs.reserve(data.virtualReferences().size()); + Q_FOREACH (qint64 colId, data.virtualReferences()) { + virtRefs.append(Collection(colId)); + } + item.setVirtualReferences(virtRefs); + } + + if (!data.cachedParts().isEmpty()) { + QSet cp; + cp.reserve(data.cachedParts().size()); + Q_FOREACH (const QByteArray &ba, data.cachedParts()) { + cp.insert(ba); + } + item.setCachedPayloadParts(cp); + } + + item.setSize(data.size()); + item.setModificationTime(data.MTime()); + parseAncestorsCached(data.ancestors(), &item, data.parentId(), valuePool); + + Q_FOREACH (const Protocol::StreamPayloadResponse &part, data.parts()) { + ProtocolHelper::PartNamespace ns; + const QByteArray plainKey = decodePartIdentifier(part.payloadName(), ns); + switch (ns) { + case ProtocolHelper::PartPayload: + ItemSerializer::deserialize(item, plainKey, part.data(), part.metaData().version(), part.metaData().isExternal()); + break; + case ProtocolHelper::PartAttribute: { + Attribute *attr = AttributeFactory::createAttribute(plainKey); + Q_ASSERT(attr); + if (part.metaData().isExternal()) { + const QString filename = ExternalPartStorage::resolveAbsolutePath(part.data()); + QFile file(filename); + if (file.open(QFile::ReadOnly)) { + attr->deserialize(file.readAll()); + } else { + qCWarning(AKONADICORE_LOG) << "Failed to open attribute file: " << filename; + delete attr; + attr = 0; + } + } else { + attr->deserialize(part.data()); + } + if (attr) { + item.addAttribute(attr); + } + break; + } + case ProtocolHelper::PartGlobal: + default: + qCWarning(AKONADICORE_LOG) << "Unknown item part type:" << part.payloadName(); + } + } + + item.d_ptr->resetChangeLog(); + return item; +} + +Tag ProtocolHelper::parseTagFetchResult(const Protocol::FetchTagsResponse &data) +{ + Tag tag; + tag.setId(data.id()); + tag.setGid(data.gid()); + tag.setRemoteId(data.remoteId()); + tag.setType(data.type()); + tag.setParent(data.parentId() > 0 ? Tag(data.parentId()) : Tag()); + + parseAttributes(data.attributes(), &tag); + return tag; +} + +Relation ProtocolHelper::parseRelationFetchResult(const Protocol::FetchRelationsResponse &data) +{ + Relation relation; + relation.setLeft(Item(data.left())); + relation.setRight(Item(data.right())); + relation.setRemoteId(data.remoteId()); + relation.setType(data.type()); + return relation; +} + +bool ProtocolHelper::streamPayloadToFile(const QString &fileName, const QByteArray &data, QByteArray &error) +{ + const QString filePath = ExternalPartStorage::resolveAbsolutePath(fileName); + qCDebug(AKONADICORE_LOG) << filePath << fileName; + if (!filePath.startsWith(ExternalPartStorage::akonadiStoragePath())) { + qCWarning(AKONADICORE_LOG) << "Invalid file path" << fileName; + error = "Invalid file path"; + return false; + } + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qCWarning(AKONADICORE_LOG) << "Failed to open destination payload file" << file.errorString(); + error = "Failed to store payload into file"; + return false; + } + if (file.write(data) != data.size()) { + qCWarning(AKONADICORE_LOG) << "Failed to write all payload data to file"; + error = "Failed to store payload into file"; + return false; + } + qCDebug(AKONADICORE_LOG) << "Wrote" << data.size() << "bytes to " << file.fileName(); + + // Make sure stuff is written to disk + file.close(); + return true; +} + +Akonadi::Tristate ProtocolHelper::listPreference(Collection::ListPreference pref) +{ + switch (pref) { + case Collection::ListEnabled: + return Tristate::True; + case Collection::ListDisabled: + return Tristate::False; + case Collection::ListDefault: + return Tristate::Undefined; + } + + Q_ASSERT(false); + return Tristate::Undefined; +} diff --git a/src/core/protocolhelper_p.h b/src/core/protocolhelper_p.h new file mode 100644 index 0000000..d413441 --- /dev/null +++ b/src/core/protocolhelper_p.h @@ -0,0 +1,340 @@ +/* + 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 "cachepolicy.h" +#include "collection.h" +#include "collectionutils.h" +#include "item.h" +#include "itemfetchscope.h" +#include "sharedvaluepool_p.h" +#include "tag.h" + +#include "private/imapparser_p.h" +#include "private/protocol_p.h" +#include "private/scope_p.h" +#include "private/tristate_p.h" + +#include + +#include +#include +#include +#include + +namespace Akonadi +{ + +struct ProtocolHelperValuePool { + typedef Internal::SharedValuePool FlagPool; + typedef Internal::SharedValuePool MimeTypePool; + + FlagPool flagPool; + MimeTypePool mimeTypePool; + QHash ancestorCollections; +}; + +/** + @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 policy The parsed cache policy. + @returns Akonadi::CachePolicy + */ + static CachePolicy parseCachePolicy(const Protocol::CachePolicy &policy); + + /** + Convert a cache policy object into its protocol representation. + */ + static Protocol::CachePolicy cachePolicyToProtocol(const CachePolicy &policy); + + /** + Convert a ancestor chain from its protocol representation into an Item object. + */ + static void parseAncestors(const QVector &ancestors, Item *item); + + /** + Convert a ancestor chain from its protocol representation into a Collection object. + */ + static void parseAncestors(const QVector &ancestors, Collection *collection); + + /** + Convert a ancestor chain from its protocol representation into an Item object. + + This method allows to pass a @p valuePool which acts as cache, so ancestor paths for the + same @p parentCollection don't have to be parsed twice. + */ + static void parseAncestorsCached(const QVector &ancestors, + Item *item, + Collection::Id parentCollection, + ProtocolHelperValuePool *valuePool = 0); + + /** + Convert a ancestor chain from its protocol representation into an Collection object. + + This method allows to pass a @p valuePool which acts as cache, so ancestor paths for the + same @p parentCollection don't have to be parsed twice. + */ + static void parseAncestorsCached(const QVector &ancestors, + Collection *collection, + Collection::Id parentCollection, + ProtocolHelperValuePool *valuePool = 0); + /** + Parse a collection description. + @param data The input data. + @param requireParent Whether or not we require a parent as part of the data. + @returns The parsed collection + */ + static Collection parseCollection(const Protocol::FetchCollectionsResponse &data, bool requireParent = true); + + static void parseAttributes(const Protocol::Attributes &attributes, Item *item); + static void parseAttributes(const Protocol::Attributes &attributes, Collection *collection); + static void parseAttributes(const Protocol::Attributes &attributes, Tag *entity); + + static CollectionStatistics parseCollectionStatistics(const Protocol::FetchCollectionStatsResponse &stats); + + /** + Convert attributes to their protocol representation. + */ + static Protocol::Attributes attributesToProtocol(const Item &item, bool ns = false); + static Protocol::Attributes attributesToProtocol(const Collection &collection, bool ns = false); + static Protocol::Attributes attributesToProtocol(const Tag &entity, bool ns = false); + + /** + Encodes part label and namespace. + */ + static QByteArray encodePartIdentifier(PartNamespace ns, const QByteArray &label); + + /** + 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. + */ + template class Container> + static Scope entitySetToScope(const Container &_objects) + { + if (_objects.isEmpty()) { + throw Exception("No objects specified"); + } + + Container objects(_objects); + using namespace std::placeholders; + std::sort(objects.begin(), objects.end(), + [](const T &a, const T &b) -> bool { + return a.id() < b.id(); + }); + if (objects.at(0).isValid()) { + QVector uids; + uids.reserve(objects.size()); + for (const T &object : objects) { + uids << object.id(); + } + ImapSet set; + set.add(uids); + return Scope(set); + } + + qDebug() << entitySetHasGID(_objects); + if (entitySetHasGID(_objects)) { + return entitySetToGID(_objects); + } + + if (!entitySetHasRemoteIdentifier(_objects, std::mem_fn(&T::remoteId))) { + throw Exception("No remote identifier specified"); + } + + // check if we have RIDs or HRIDs + if (entitySetHasHRID(_objects)) { + return hierarchicalRidToScope(objects.first()); + } + + return entitySetToRemoteIdentifier(Scope::Rid, _objects, std::mem_fn(&T::remoteId)); + } + + static Protocol::ScopeContext commandContextToProtocol(const Akonadi::Collection &collection, const Akonadi::Tag &tag, + const Item::List &requestedItems); + + /** + Converts the given object identifier into a protocol representation. + @throws A Akonadi::Exception if the item set contains items with missing/invalid identifiers. + */ + template + static Scope entityToScope(const T &object) + { + return entitySetToScope(QVector() << object); + } + + /** + Converts the given collection's hierarchical RID into a protocol representation. + Assumes @p col has a valid hierarchical RID, so check that before! + */ + static Scope hierarchicalRidToScope(const Collection &col); + + /** + Converts the HRID of the given item into an ASAP protocol representation. + Assumes @p item has a valid HRID. + */ + static Scope hierarchicalRidToScope(const Item &item); + + static Scope hierarchicalRidToScope(const Tag &/*tag*/) + { + assert(false); + return Scope(); + } + + /** + Converts a given ItemFetchScope object into a protocol representation. + */ + static Protocol::FetchScope itemFetchScopeToProtocol(const ItemFetchScope &fetchScope); + + /** + Converts a given TagFetchScope object into a protocol representation. + */ + static QVector tagFetchScopeToProtocol(const TagFetchScope &fetchScope); + + /** + Parses a single line from an item fetch job result into an Item object. + */ + static Item parseItemFetchResult(const Protocol::FetchItemsResponse &data, ProtocolHelperValuePool *valuePool = 0); + static Tag parseTagFetchResult(const Protocol::FetchTagsResponse &data); + static Relation parseRelationFetchResult(const Protocol::FetchRelationsResponse &data); + + static bool streamPayloadToFile(const QString &file, const QByteArray &data, QByteArray &error); + + static Akonadi::Tristate listPreference(const Collection::ListPreference pref); + +private: + template class Container> + inline static + typename std::enable_if < !std::is_same::value, bool >::type + entitySetHasGID(const Container &objects) + { + return entitySetHasRemoteIdentifier(objects, std::mem_fn(&T::gid)); + } + + template class Container> + inline static + typename std::enable_if::value, bool>::type + entitySetHasGID(const Container &/*objects*/, int */*dummy*/ = 0) + { + return false; + } + + template class Container> + inline static + typename std::enable_if < !std::is_same::value, Scope >::type + entitySetToGID(const Container &objects) + { + return entitySetToRemoteIdentifier(Scope::Gid, objects, std::mem_fn(&T::gid)); + } + + template class Container> + inline static + typename std::enable_if::value, Scope>::type + entitySetToGID(const Container &/*objects*/, int */*dummy*/ = 0) + { + return Scope(); + } + + template class Container, typename RIDFunc> + inline static + bool entitySetHasRemoteIdentifier(const Container &objects, const RIDFunc &ridFunc) + { + return std::find_if(objects.constBegin(), objects.constEnd(), + [=](const T &obj) { + return ridFunc(obj).isEmpty(); + }) + == objects.constEnd(); + } + + template class Container, typename RIDFunc> + inline static + typename std::enable_if::value, Scope>::type + entitySetToRemoteIdentifier(Scope::SelectionScope scope, const Container &objects, const RIDFunc &ridFunc) + { + QStringList rids; + rids.reserve(objects.size()); + std::transform(objects.cbegin(), objects.cend(), + std::back_inserter(rids), [=](const T &obj) -> QString { + return ridFunc(obj); + }); + return Scope(scope, rids); + } + + template class Container, typename RIDFunc> + inline static + typename std::enable_if::value, Scope>::type + entitySetToRemoteIdentifier(Scope::SelectionScope scope, const Container &objects, const RIDFunc &ridFunc, int */*dummy*/ = 0) + { + QStringList rids; + rids.reserve(objects.size()); + std::transform(objects.cbegin(), objects.cend(), + std::back_inserter(rids), [=](const T &obj) -> QString { + return QString::fromLatin1(ridFunc(obj)); + }); + return Scope(scope, rids); + } + + template class Container> + inline static + typename std::enable_if::value, bool>::type + entitySetHasHRID(const Container &objects) + { + return objects.size() == 1 && + std::find_if(objects.constBegin(), objects.constEnd(), + [](const T &obj) -> bool { + return !CollectionUtils::hasValidHierarchicalRID(obj); + }) + == objects.constEnd(); // ### HRID sets are not yet specified + } + + template class Container> + inline static + typename std::enable_if::value, bool>::type + entitySetHasHRID(const Container &/*objects*/, int */*dummy*/ = 0) + { + return false; + } +}; + +} + +#endif diff --git a/src/core/qtest_akonadi.h b/src/core/qtest_akonadi.h new file mode 100644 index 0000000..ed953c3 --- /dev/null +++ b/src/core/qtest_akonadi.h @@ -0,0 +1,103 @@ +/* This file is based on qtest_kde.h from kdelibs + Copyright (C) 2006 David Faure + 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 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. +*/ + +#ifndef QTEST_AKONADI_H +#define QTEST_AKONADI_H + +#include +#include + +#include +#include + +/** +* \short Akonadi Replacement for QTEST_MAIN from QTestLib +* +* This macro should be used for classes that run inside the Akonadi Testrunner. +* So instead of writing QTEST_MAIN( TestClass ) you write +* QTEST_AKONADIMAIN( TestClass ). +* +* \param TestObject The class you use for testing. +* +* \see QTestLib +* \see QTEST_KDEMAIN +*/ +#define QTEST_AKONADIMAIN(TestObject) \ + int main(int argc, char *argv[]) \ + { \ + setenv( "LC_ALL", "C", 1); \ + unsetenv( "KDE_COLOR_DEBUG" ); \ + QApplication app( argc, argv ); \ + app.setApplicationName(QLatin1String("qttest")); \ + app.setOrganizationDomain(QLatin1String("kde.org")); \ + app.setOrganizationName(QLatin1String("KDE")); \ + QGuiApplication::setQuitOnLastWindowClosed(false); \ + qRegisterMetaType>(); \ + TestObject tc; \ + return QTest::qExec( &tc, argc, argv ); \ + } + +namespace AkonadiTest +{ +/** + * Checks that the test is running in the proper test environment + */ +void checkTestIsIsolated() +{ + Q_ASSERT_X(!qgetenv("TESTRUNNER_DB_ENVIRONMENT").isEmpty(), + "AkonadiTest::checkTestIsIsolated", + "This test must be run using ctest, in order to use the testrunner environment. Aborting, to avoid messing up your real akonadi"); + Q_ASSERT_X(qgetenv("XDG_DATA_HOME").contains("testrunner"), + "AkonadiTest::checkTestIsIsolated", + "Did you forget to run the test using QTEST_AKONADIMAIN?"); +} + +/** + * Switch all resources offline to reduce interference from them + */ +void setAllResourcesOffline() +{ + // switch all resources offline to reduce interference from them + Q_FOREACH (Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances()) { //krazy:exclude=foreach + agent.setIsOnline(false); + } +} + +bool akWaitForSignal(QObject *sender, const char *member, int timeout = 1000) +{ + QSignalSpy spy(sender, member); + bool ok = false; + [&]() { + QTRY_VERIFY_WITH_TIMEOUT(spy.count() > 0, timeout); + ok = true; + }(); + return ok; +} + +} // namespace + +/** + * Runs an Akonadi::Job synchronously and aborts if the job failed. + * Similar to QVERIFY( job->exec() ) but includes the job error message + * in the output in case of a failure. + */ +#define AKVERIFYEXEC( job ) \ + QVERIFY2( job->exec(), job->errorString().toUtf8().constData() ) + +#endif diff --git a/src/core/relation.cpp b/src/core/relation.cpp new file mode 100644 index 0000000..45e1558 --- /dev/null +++ b/src/core/relation.cpp @@ -0,0 +1,136 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "relation.h" + +#include "item.h" + +using namespace Akonadi; + +const char *Akonadi::Relation::GENERIC = "GENERIC"; + +struct Q_DECL_HIDDEN Relation::Private { + Item left; + Item right; + QByteArray type; + QByteArray remoteId; +}; + +Relation::Relation() + : d(new Private) +{ + +} + +Relation::Relation(const QByteArray &type, const Item &left, const Item &right) + : d(new Private) +{ + d->left = left; + d->right = right; + d->type = type; +} + +Relation::Relation(const Relation &other) + : d(new Private) +{ + operator=(other); +} + +Relation::~Relation() +{ +} + +Relation &Relation::operator=(const Relation &other) +{ + d->left = other.d->left; + d->right = other.d->right; + d->type = other.d->type; + return *this; +} + +bool Relation::operator==(const Relation &other) const +{ + if (isValid() && other.isValid()) { + return d->left == other.d->left + && d->right == other.d->right + && d->type == other.d->type; + } + return false; +} + +bool Relation::operator!=(const Relation &other) const +{ + return !operator==(other); +} + +void Relation::setLeft(const Item &left) +{ + d->left = left; +} + +Item Relation::left() const +{ + return d->left; +} + +void Relation::setRight(const Item &right) +{ + d->right = right; +} + +Item Relation::right() const +{ + return d->right; +} + +void Relation::setType(const QByteArray &type) const +{ + d->type = type; +} + +QByteArray Relation::type() const +{ + return d->type; +} + +void Relation::setRemoteId(const QByteArray &remoteId) const +{ + d->remoteId = remoteId; +} + +QByteArray Relation::remoteId() const +{ + return d->remoteId; +} + +bool Relation::isValid() const +{ + return (d->left.isValid() || !d->left.remoteId().isEmpty()) && (d->right.isValid() || !d->left.remoteId().isEmpty()) && !d->type.isEmpty(); +} + +uint qHash(const Relation &relation) +{ + return (3 * qHash(relation.left()) + qHash(relation.right()) + qHash(relation.type())); +} + +QDebug &operator<<(QDebug &debug, const Relation &relation) +{ + debug << "Akonadi::Relation( TYPE " << relation.type() << ", LEFT " << relation.left().id() << ", RIGHT " << relation.right().id() << ")"; + return debug; +} diff --git a/src/core/relation.h b/src/core/relation.h new file mode 100644 index 0000000..6bf4823 --- /dev/null +++ b/src/core/relation.h @@ -0,0 +1,135 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_RELATION_H +#define AKONADI_RELATION_H + +#include "akonadicore_export.h" + +namespace Akonadi +{ +class Relation; +} + +AKONADICORE_EXPORT unsigned int qHash(const Akonadi::Relation &); + +#include +#include +#include +#include + +namespace Akonadi +{ +class Item; + +/** + * An Akonadi Relation. + * + * A Relation object represents an relation between two Akonadi items. + * + * An example usecase could be a association of a note with an email. The note (that for instance contains personal notes for the email), + * can be stored independently but is easily retrieved by asking for relations the email. + * + * The relation type allows to distinguish various types of relations that could for instance be bidirectional or not. + * + * @since 4.15 + */ +class AKONADICORE_EXPORT Relation +{ +public: + typedef QVector List; + + /** + * The GENERIC type represents a generic relation between two items. + */ + static const char *GENERIC; + + /** + * Creates an invalid relation. + */ + Relation(); + + /** + * Creates a relation + */ + explicit Relation(const QByteArray &type, const Item &left, const Item &right); + + Relation(const Relation &other); + ~Relation(); + + Relation &operator=(const Relation &); + bool operator==(const Relation &) const; + bool operator!=(const Relation &) const; + + /** + * Sets the @p item of the left side of the relation. + */ + void setLeft(const Item &item); + + /** + * Returns the identifier of the left side of the relation. + */ + Item left() const; + + /** + * Sets the @p item of the right side of the relation. + */ + void setRight(const Akonadi::Item &item); + + /** + * Returns the identifier of the right side of the relation. + */ + Item right() const; + + /** + * Sets the type of the relation. + */ + void setType(const QByteArray &type) const; + + /** + * Returns the type of the relation. + */ + QByteArray type() const; + + /** + * Sets the remote id of the relation. + */ + void setRemoteId(const QByteArray &type) const; + + /** + * Returns the remote idof the relation. + */ + QByteArray remoteId() const; + + bool isValid() const; + +private: + class Private; + QSharedPointer d; +}; + +} + +AKONADICORE_EXPORT QDebug &operator<<(QDebug &debug, const Akonadi::Relation &tag); + +Q_DECLARE_METATYPE(Akonadi::Relation) +Q_DECLARE_METATYPE(Akonadi::Relation::List) +Q_DECLARE_METATYPE(QSet) +Q_DECLARE_TYPEINFO(Akonadi::Relation, Q_MOVABLE_TYPE); +#endif diff --git a/src/core/relationsync.cpp b/src/core/relationsync.cpp new file mode 100644 index 0000000..0187076 --- /dev/null +++ b/src/core/relationsync.cpp @@ -0,0 +1,123 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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. +*/ +namespace Akonadi +{ +class Item; +} + +#include "relationsync.h" +#include "akonadicore_debug.h" +#include "itemfetchscope.h" + +#include "jobs/itemfetchjob.h" +#include "jobs/relationfetchjob.h" +#include "jobs/relationcreatejob.h" +#include "jobs/relationdeletejob.h" + +#include + +using namespace Akonadi; + +RelationSync::RelationSync(QObject *parent) + : Job(parent), + mRemoteRelationsSet(false), + mLocalRelationsFetched(false) +{ + +} + +RelationSync::~RelationSync() +{ + +} + +void RelationSync::setRemoteRelations(const Akonadi::Relation::List &relations) +{ + mRemoteRelations = relations; + mRemoteRelationsSet = true; + diffRelations(); +} + +void RelationSync::doStart() +{ + Akonadi::RelationFetchJob *fetch = new Akonadi::RelationFetchJob({ Akonadi::Relation::GENERIC }, this); + connect(fetch, &KJob::result, this, &RelationSync::onLocalFetchDone); +} + +void RelationSync::onLocalFetchDone(KJob *job) +{ + Akonadi::RelationFetchJob *fetch = static_cast(job); + mLocalRelations = fetch->relations(); + mLocalRelationsFetched = true; + diffRelations(); +} + +void RelationSync::diffRelations() +{ + if (!mRemoteRelationsSet || !mLocalRelationsFetched) { + qCDebug(AKONADICORE_LOG) << "waiting for delivery: " << mRemoteRelationsSet << mLocalRelationsFetched; + return; + } + + QHash relationByRid; + Q_FOREACH (const Akonadi::Relation &localRelation, mLocalRelations) { + if (!localRelation.remoteId().isEmpty()) { + relationByRid.insert(localRelation.remoteId(), localRelation); + } + } + + Q_FOREACH (const Akonadi::Relation &remoteRelation, mRemoteRelations) { + if (relationByRid.contains(remoteRelation.remoteId())) { + relationByRid.remove(remoteRelation.remoteId()); + } else { + //New relation or had its GID updated, so create one now + RelationCreateJob *createJob = new RelationCreateJob(remoteRelation, this); + connect(createJob, &KJob::result, this, &RelationSync::checkDone); + } + } + + Q_FOREACH (const Akonadi::Relation &removedRelation, relationByRid) { + //Removed remotely, remove locally + RelationDeleteJob *removeJob = new RelationDeleteJob(removedRelation, this); + connect(removeJob, &KJob::result, this, &RelationSync::checkDone); + } + checkDone(); +} + +void RelationSync::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error during CollectionSync: " << job->errorString() << job->metaObject()->className(); + // pretend there were no errors + Akonadi::Job::removeSubjob(job); + } else { + Akonadi::Job::slotResult(job); + } +} + +void RelationSync::checkDone() +{ + if (hasSubjobs()) { + qCDebug(AKONADICORE_LOG) << "Still going"; + return; + } + qCDebug(AKONADICORE_LOG) << "done"; + emitResult(); +} + diff --git a/src/core/relationsync.h b/src/core/relationsync.h new file mode 100644 index 0000000..97867ec --- /dev/null +++ b/src/core/relationsync.h @@ -0,0 +1,58 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 RELATIONSYNC_H +#define RELATIONSYNC_H + +#include "akonadicore_export.h" + +#include "jobs/job.h" +#include "relation.h" + +namespace Akonadi { + +class AKONADICORE_EXPORT RelationSync : public Akonadi::Job +{ + Q_OBJECT +public: + RelationSync(QObject *parent = 0); + virtual ~RelationSync(); + + void setRemoteRelations(const Akonadi::Relation::List &relations); + +protected: + void doStart() Q_DECL_OVERRIDE; + +private Q_SLOTS: + void onLocalFetchDone(KJob *job); + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +private: + void diffRelations(); + void checkDone(); + +private: + Akonadi::Relation::List mRemoteRelations; + Akonadi::Relation::List mLocalRelations; + bool mRemoteRelationsSet; + bool mLocalRelationsFetched; +}; + +} + +#endif diff --git a/src/core/searchquery.cpp b/src/core/searchquery.cpp new file mode 100644 index 0000000..c159706 --- /dev/null +++ b/src/core/searchquery.cpp @@ -0,0 +1,411 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "searchquery.h" +#include "akonadicore_debug.h" + +#include +#include +#include +#include + + +using namespace Akonadi; + +class SearchTerm::Private : public QSharedData +{ +public: + Private() + : QSharedData() + , condition(SearchTerm::CondEqual) + , relation(SearchTerm::RelAnd) + , isNegated(false) + { + } + + Private(const Private &other) + : QSharedData(other) + , key(other.key) + , value(other.value) + , condition(other.condition) + , relation(other.relation) + , terms(other.terms) + , isNegated(other.isNegated) + { + } + + bool operator==(const Private &other) const + { + return relation == other.relation + && isNegated == other.isNegated + && terms == other.terms + && key == other.key + && value == other.value + && condition == other.condition; + } + + QString key; + QVariant value; + Condition condition; + Relation relation; + QList terms; + bool isNegated; +}; + +class SearchQuery::Private : public QSharedData +{ +public: + Private() + : QSharedData() + , limit(-1) + { + } + + Private(const Private &other) + : QSharedData(other) + , rootTerm(other.rootTerm) + , limit(other.limit) + { + } + + bool operator==(const Private &other) const + { + return rootTerm == other.rootTerm && limit == other.limit; + } + + static QVariantMap termToJSON(const SearchTerm &term) + { + const QList &subTerms = term.subTerms(); + QVariantMap termJSON; + termJSON.insert(QStringLiteral("negated"), term.isNegated()); + if (subTerms.isEmpty()) { + termJSON.insert(QStringLiteral("key"), term.key()); + termJSON.insert(QStringLiteral("value"), term.value()); + termJSON.insert(QStringLiteral("cond"), static_cast(term.condition())); + } else { + termJSON.insert(QStringLiteral("rel"), static_cast(term.relation())); + QVariantList subTermsJSON; + subTermsJSON.reserve(subTerms.count()); + Q_FOREACH (const SearchTerm &term, subTerms) { + subTermsJSON.append(termToJSON(term)); + } + termJSON.insert(QStringLiteral("subTerms"), subTermsJSON); + } + + return termJSON; + } + + static SearchTerm JSONToTerm(const QVariantMap &map) + { + if (map.contains(QStringLiteral("key"))) { + SearchTerm term(map[QStringLiteral("key")].toString(), + map[QStringLiteral("value")], + static_cast(map[QStringLiteral("cond")].toInt())); + term.setIsNegated(map[QStringLiteral("negated")].toBool()); + return term; + } else if (map.contains(QStringLiteral("rel"))) { + SearchTerm term(static_cast(map[QStringLiteral("rel")].toInt())); + term.setIsNegated(map[QStringLiteral("negated")].toBool()); + const QList list = map[QStringLiteral("subTerms")].toList(); + Q_FOREACH (const QVariant &var, list) { + term.addSubTerm(JSONToTerm(var.toMap())); + } + return term; + } else { + qCWarning(AKONADICORE_LOG) << "Invalid JSON for term: " << map; + return SearchTerm(); + } + } + + SearchTerm rootTerm; + int limit; +}; + +SearchTerm::SearchTerm(SearchTerm::Relation relation) + : d(new Private) +{ + d->relation = relation; +} + +SearchTerm::SearchTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition) + : d(new Private) +{ + d->relation = RelAnd; + d->key = key; + d->value = value; + d->condition = condition; +} + +SearchTerm::SearchTerm(const SearchTerm &other) + : d(other.d) +{ +} + +SearchTerm::~SearchTerm() +{ +} + +SearchTerm &SearchTerm::operator=(const SearchTerm &other) +{ + d = other.d; + return *this; +} + +bool SearchTerm::operator==(const SearchTerm &other) const +{ + return *d == *other.d; +} + +bool SearchTerm::isNull() const +{ + return d->key.isEmpty() && d->value.isNull() && d->terms.isEmpty(); +} + +QString SearchTerm::key() const +{ + return d->key; +} + +QVariant SearchTerm::value() const +{ + return d->value; +} + +SearchTerm::Condition SearchTerm::condition() const +{ + return d->condition; +} + +void SearchTerm::setIsNegated(bool negated) +{ + d->isNegated = negated; +} + +bool SearchTerm::isNegated() const +{ + return d->isNegated; +} + +void SearchTerm::addSubTerm(const SearchTerm &term) +{ + d->terms << term; +} + +QList< SearchTerm > SearchTerm::subTerms() const +{ + return d->terms; +} + +SearchTerm::Relation SearchTerm::relation() const +{ + return d->relation; +} + +SearchQuery::SearchQuery(SearchTerm::Relation rel) + : d(new Private) +{ + d->rootTerm = SearchTerm(rel); +} + +SearchQuery::SearchQuery(const SearchQuery &other) + : d(other.d) +{ +} + +SearchQuery::~SearchQuery() +{ +} + +SearchQuery &SearchQuery::operator=(const SearchQuery &other) +{ + d = other.d; + return *this; +} + +bool SearchQuery::operator==(const SearchQuery &other) const +{ + return *d == *other.d; +} + +bool SearchQuery::isNull() const +{ + return d->rootTerm.isNull(); +} + +SearchTerm SearchQuery::term() const +{ + return d->rootTerm; +} + +void SearchQuery::addTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition) +{ + addTerm(SearchTerm(key, value, condition)); +} + +void SearchQuery::addTerm(const SearchTerm &term) +{ + d->rootTerm.addSubTerm(term); +} + +void SearchQuery::setTerm(const SearchTerm &term) +{ + d->rootTerm = term; +} + +void SearchQuery::setLimit(int limit) +{ + d->limit = limit; +} + +int SearchQuery::limit() const +{ + return d->limit; +} + +QByteArray SearchQuery::toJSON() const +{ + QVariantMap root = Private::termToJSON(d->rootTerm); + root.insert(QStringLiteral("limit"), d->limit); + + QJsonObject jo = QJsonObject::fromVariantMap(root); + QJsonDocument jdoc; + jdoc.setObject(jo); + return jdoc.toJson(); +} + +SearchQuery SearchQuery::fromJSON(const QByteArray &jsonData) +{ + QJsonParseError error; + const QJsonDocument json = QJsonDocument::fromJson(jsonData, &error); + if (error.error != QJsonParseError::NoError || json.isNull()) { + return SearchQuery(); + } + + SearchQuery query; + const QJsonObject obj = json.object(); + query.d->rootTerm = Private::JSONToTerm(obj.toVariantMap()); + if (obj.contains(QStringLiteral("limit"))) { + query.d->limit = obj.value(QStringLiteral("limit")).toInt(); + } + return query; +} + +static QMap emailSearchFieldMapping() +{ + static QMap mapping; + if (mapping.isEmpty()) { + mapping.insert(EmailSearchTerm::Body, QStringLiteral("body")); + mapping.insert(EmailSearchTerm::Headers, QStringLiteral("headers")); + mapping.insert(EmailSearchTerm::Subject, QStringLiteral("subject")); + mapping.insert(EmailSearchTerm::Message, QStringLiteral("message")); + mapping.insert(EmailSearchTerm::HeaderFrom, QStringLiteral("from")); + mapping.insert(EmailSearchTerm::HeaderTo, QStringLiteral("to")); + mapping.insert(EmailSearchTerm::HeaderCC, QStringLiteral("cc")); + mapping.insert(EmailSearchTerm::HeaderBCC, QStringLiteral("bcc")); + mapping.insert(EmailSearchTerm::HeaderReplyTo, QStringLiteral("replyto")); + mapping.insert(EmailSearchTerm::HeaderOrganization, QStringLiteral("organization")); + mapping.insert(EmailSearchTerm::HeaderListId, QStringLiteral("listid")); + mapping.insert(EmailSearchTerm::HeaderResentFrom, QStringLiteral("resentfrom")); + mapping.insert(EmailSearchTerm::HeaderXLoop, QStringLiteral("xloop")); + mapping.insert(EmailSearchTerm::HeaderXMailingList, QStringLiteral("xmailinglist")); + mapping.insert(EmailSearchTerm::HeaderXSpamFlag, QStringLiteral("xspamflag")); + mapping.insert(EmailSearchTerm::HeaderDate, QStringLiteral("date")); + mapping.insert(EmailSearchTerm::HeaderOnlyDate, QStringLiteral("onlydate")); + mapping.insert(EmailSearchTerm::MessageStatus, QStringLiteral("messagestatus")); + mapping.insert(EmailSearchTerm::MessageTag, QStringLiteral("messagetag")); + mapping.insert(EmailSearchTerm::ByteSize, QStringLiteral("size")); + mapping.insert(EmailSearchTerm::Attachment, QStringLiteral("attachment")); + } + + return mapping; +} + +EmailSearchTerm::EmailSearchTerm(EmailSearchTerm::EmailSearchField field, const QVariant &value, SearchTerm::Condition condition) + : SearchTerm(toKey(field), value, condition) +{ + +} + +QString EmailSearchTerm::toKey(EmailSearchTerm::EmailSearchField field) +{ + return emailSearchFieldMapping().value(field); +} + +EmailSearchTerm::EmailSearchField EmailSearchTerm::fromKey(const QString &key) +{ + return emailSearchFieldMapping().key(key); +} + +static QMap contactSearchFieldMapping() +{ + static QMap mapping; + if (mapping.isEmpty()) { + mapping.insert(ContactSearchTerm::Name, QStringLiteral("name")); + mapping.insert(ContactSearchTerm::Nickname, QStringLiteral("nickname")); + mapping.insert(ContactSearchTerm::Email, QStringLiteral("email")); + mapping.insert(ContactSearchTerm::Uid, QStringLiteral("uid")); + mapping.insert(ContactSearchTerm::All, QStringLiteral("all")); + } + return mapping; +} + +ContactSearchTerm::ContactSearchTerm(ContactSearchTerm::ContactSearchField field, const QVariant &value, SearchTerm::Condition condition) + : SearchTerm(toKey(field), value, condition) +{ + +} + +QString ContactSearchTerm::toKey(ContactSearchTerm::ContactSearchField field) +{ + return contactSearchFieldMapping().value(field); +} + +ContactSearchTerm::ContactSearchField ContactSearchTerm::fromKey(const QString &key) +{ + return contactSearchFieldMapping().key(key); +} + +QMap incidenceSearchFieldMapping() +{ + QMap mapping; + if (mapping.isEmpty()) { + mapping.insert(IncidenceSearchTerm::All, QStringLiteral("all")); + mapping.insert(IncidenceSearchTerm::PartStatus, QStringLiteral("partstatus")); + mapping.insert(IncidenceSearchTerm::Organizer, QStringLiteral("organizer")); + mapping.insert(IncidenceSearchTerm::Summary, QStringLiteral("summary")); + mapping.insert(IncidenceSearchTerm::Location, QStringLiteral("location")); + } + return mapping; +} + +IncidenceSearchTerm::IncidenceSearchTerm(IncidenceSearchTerm::IncidenceSearchField field, const QVariant &value, SearchTerm::Condition condition) + : SearchTerm(toKey(field), value, condition) +{ + +} + +QString IncidenceSearchTerm::toKey(IncidenceSearchTerm::IncidenceSearchField field) +{ + return incidenceSearchFieldMapping().value(field); +} + +IncidenceSearchTerm::IncidenceSearchField IncidenceSearchTerm::fromKey(const QString &key) +{ + return incidenceSearchFieldMapping().key(key); +} diff --git a/src/core/searchquery.h b/src/core/searchquery.h new file mode 100644 index 0000000..ad67092 --- /dev/null +++ b/src/core/searchquery.h @@ -0,0 +1,308 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_SEARCHQUERY_H +#define AKONADI_SEARCHQUERY_H + +#include + +#include "akonadicore_export.h" + +namespace Akonadi +{ + +/** + * Search term represents the actual condition within query. + * + * SearchTerm can either have multiple subterms, or can be so-called endterm, when + * there are no more subterms, but instead the actual condition is specified, that + * is have key, value and relation between them. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT SearchTerm +{ +public: + enum Relation { + RelAnd, + RelOr + }; + + enum Condition { + CondEqual, + CondGreaterThan, + CondGreaterOrEqual, + CondLessThan, + CondLessOrEqual, + CondContains + }; + + /** + * Constructs a term where all subterms will be in given relation + */ + SearchTerm(SearchTerm::Relation relation = SearchTerm::RelAnd); + + /** + * Constructs an end term + */ + SearchTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + SearchTerm(const SearchTerm &other); + ~SearchTerm(); + + SearchTerm &operator=(const SearchTerm &other); + bool operator==(const SearchTerm &other) const; + + bool isNull() const; + + /** + * Returns key of this end term. + */ + QString key() const; + + /** + * Returns value of this end term. + */ + QVariant value() const; + + /** + * Returns relation between key and value. + */ + SearchTerm::Condition condition() const; + + /** + * Adds a new subterm to this term. + * + * Subterms will be in relation as specified in SearchTerm constructor. + * + * If there are subterms in a term, key, value and condition are ignored. + */ + void addSubTerm(const SearchTerm &term); + + /** + * Returns all subterms, or an empty list if this is an end term. + */ + QList subTerms() const; + + /** + * Returns relation in which all subterms are. + */ + SearchTerm::Relation relation() const; + + /** + * Sets whether the entire term is negated. + */ + void setIsNegated(bool negated); + + /** + * Returns whether the entire term is negated. + */ + bool isNegated() const; + +private: + class Private; + QSharedDataPointer d; +}; + +/** + * @brief A query that can be passed to ItemSearchJob or others. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT SearchQuery +{ +public: + /** + * Constructs query where all added terms will be in given relation + */ + SearchQuery(SearchTerm::Relation rel = SearchTerm::RelAnd); + + ~SearchQuery(); + SearchQuery(const SearchQuery &other); + SearchQuery &operator=(const SearchQuery &other); + bool operator==(const SearchQuery &other) const; + + bool isNull() const; + + /** + * Adds a new term. + */ + void addTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + /** + * Adds a new term with subterms + */ + void addTerm(const SearchTerm &term); + + /** + * Sets the root term + */ + void setTerm(const SearchTerm &term); + + /** + * Returns the root term. + */ + SearchTerm term() const; + + /** + * Sets the maximum number of results. + * + * Note that this limit is only evaluated per search backend, + * so the total number of results retrieved may be larger. + */ + void setLimit(int limit); + + /** + * Returns the maximum number of results. + * + * The default value is -1, indicating no limit. + */ + int limit() const; + + QByteArray toJSON() const; + static SearchQuery fromJSON(const QByteArray &json); + +private: + class Private; + QSharedDataPointer d; + +}; + +/** + * A search term for an email field. + * + * This class can be used to create queries that akonadi email search backends understand. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT EmailSearchTerm : public SearchTerm +{ +public: + + /** + * All fields expect a search string unless noted otherwise. + */ + enum EmailSearchField { + Unknown, + Subject, + Body, + Message, //Complete message including headers, body and attachment + Headers, //All headers + HeaderFrom, + HeaderTo, + HeaderCC, + HeaderBCC, + HeaderReplyTo, + HeaderOrganization, + HeaderListId, + HeaderResentFrom, + HeaderXLoop, + HeaderXMailingList, + HeaderXSpamFlag, + HeaderDate, //Expects QDateTime + HeaderOnlyDate, //Expectes QDate + MessageStatus, //Expects message flag from Akonadi::MessageFlags. Boolean filter. + ByteSize, //Expects int + Attachment, //Textsearch on attachment + MessageTag + }; + + /** + * Constructs an email end term + */ + EmailSearchTerm(EmailSearchField field, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + /** + * Translates field to key + */ + static QString toKey(EmailSearchField); + + /** + * Translates key to field + */ + static EmailSearchField fromKey(const QString &key); +}; + +/** + * A search term for a contact field. + * + * This class can be used to create queries that akonadi contact search backends understand. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT ContactSearchTerm : public SearchTerm +{ +public: + enum ContactSearchField { + Unknown, + Name, + Email, + Nickname, + Uid, + All //Special field: matches all contacts. + }; + + ContactSearchTerm(ContactSearchField field, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + /** + * Translates field to key + */ + static QString toKey(ContactSearchField); + + /** + * Translates key to field + */ + static ContactSearchField fromKey(const QString &key); +}; + +/** + * A search term for a incidence field. + * + * This class can be used to create queries that akonadi incidence search backends understand. + * + * @since 5.0 + */ +class AKONADICORE_EXPORT IncidenceSearchTerm : public SearchTerm +{ +public: + enum IncidenceSearchField { + Unknown, + All, + PartStatus, // Own PartStatus + Organizer, + Summary, + Location + }; + + IncidenceSearchTerm(IncidenceSearchField field, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + /** + * Translates field to key + */ + static QString toKey(IncidenceSearchField); + + /** + * Translates key to field + */ + static IncidenceSearchField fromKey(const QString &key); +}; + +} + +#endif // AKONADI_SEARCHQUERY_H diff --git a/src/core/servermanager.cpp b/src/core/servermanager.cpp new file mode 100644 index 0000000..5325dab --- /dev/null +++ b/src/core/servermanager.cpp @@ -0,0 +1,374 @@ +/* + 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 "servermanager.h" +#include "servermanager_p.h" + +#include "agenttype.h" +#include "agentmanager.h" +#include "KDBusConnectionPool" +#include "session_p.h" +#include "firstrun_p.h" + +#include "akonadicore_debug.h" + +#include + +#include "private/protocol_p.h" +#include "private/standarddirs_p.h" +#include "private/dbus_p.h" +#include "private/instance_p.h" + +#include +#include +#include + +using namespace Akonadi; + +class Akonadi::ServerManagerPrivate +{ +public: + ServerManagerPrivate() + : instance(new ServerManager(this)) + , mState(ServerManager::NotRunning) + , mSafetyTimer(new QTimer) + , mFirstRunner(0) + { + mState = instance->state(); + mSafetyTimer->setSingleShot(true); + mSafetyTimer->setInterval(30000); + QObject::connect(mSafetyTimer.data(), SIGNAL(timeout()), instance, SLOT(timeout())); + if (mState == ServerManager::Running && Internal::clientType() == Internal::User && !ServerManager::hasInstanceIdentifier()) { + mFirstRunner = new Firstrun(instance); + } + } + + ~ServerManagerPrivate() + { + delete instance; + } + + void serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) + { + if (name == ServerManager::serviceName(ServerManager::ControlLock) && !oldOwner.isEmpty() && newOwner.isEmpty()) { + // Control.Lock has disappeared during startup, which means that akonadi_control + // has terminated, most probably because it was not able to start akonadiserver + // process. Don't wait 30 seconds for sefetyTimeout, but go into Broken state + // immediately. + if (mState == ServerManager::Starting) { + setState(ServerManager::Broken); + return; + } + } + + serverProtocolVersion = -1, + checkStatusChanged(); + } + + void checkStatusChanged() + { + setState(instance->state()); + } + + void setState(ServerManager::State state) + { + + if (mState != state) { + mState = state; + emit instance->stateChanged(state); + if (state == ServerManager::Running) { + emit instance->started(); + if (!mFirstRunner && Internal::clientType() == Internal::User && !ServerManager::hasInstanceIdentifier()) { + mFirstRunner = new Firstrun(instance); + } + } else if (state == ServerManager::NotRunning || state == ServerManager::Broken) { + emit instance->stopped(); + } + + if (state == ServerManager::Starting || state == ServerManager::Stopping) { + QMetaObject::invokeMethod(mSafetyTimer.data(), "start", Qt::QueuedConnection); // in case we are in a different thread + } else { + QMetaObject::invokeMethod(mSafetyTimer.data(), "stop", Qt::QueuedConnection); // in case we are in a different thread + } + } + } + + void timeout() + { + if (mState == ServerManager::Starting || mState == ServerManager::Stopping) { + setState(ServerManager::Broken); + } + } + + ServerManager *instance; + static int serverProtocolVersion; + ServerManager::State mState; + QScopedPointer mSafetyTimer; + Firstrun *mFirstRunner; + static Internal::ClientType clientType; +}; + +int ServerManagerPrivate::serverProtocolVersion = -1; +Internal::ClientType ServerManagerPrivate::clientType = Internal::User; + +Q_GLOBAL_STATIC(ServerManagerPrivate, sInstance) + +ServerManager::ServerManager(ServerManagerPrivate *dd) + : d(dd) +{ + Kdelibs4ConfigMigrator migrate(QStringLiteral("servermanager")); + migrate.setConfigFiles(QStringList() << QStringLiteral("akonadi-firstrunrc")); + migrate.migrate(); + + qRegisterMetaType(); + + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(ServerManager::serviceName(ServerManager::Server), + KDBusConnectionPool::threadConnection(), + QDBusServiceWatcher::WatchForOwnerChange, this); + watcher->addWatchedService(ServerManager::serviceName(ServerManager::Control)); + watcher->addWatchedService(ServerManager::serviceName(ServerManager::ControlLock)); + watcher->addWatchedService(ServerManager::serviceName(ServerManager::UpgradeIndicator)); + + // this (and also the two connects below) are queued so that they trigger after AgentManager is done loading + // the current agent types and instances + // this ensures the invariant of AgentManager reporting a consistent state if ServerManager::state() == Running + // that's the case with direct connections as well, but only after you enter the event loop once + connect(watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)), + this, SLOT(serviceOwnerChanged(QString,QString,QString)), Qt::QueuedConnection); + + // AgentManager is dangerous to use for agents themselves + if (Internal::clientType() != Internal::User) { + return; + } + connect(AgentManager::self(), SIGNAL(typeAdded(Akonadi::AgentType)), SLOT(checkStatusChanged()), Qt::QueuedConnection); + connect(AgentManager::self(), SIGNAL(typeRemoved(Akonadi::AgentType)), SLOT(checkStatusChanged()), Qt::QueuedConnection); +} + +ServerManager *Akonadi::ServerManager::self() +{ + return sInstance->instance; +} + +bool ServerManager::start() +{ + const bool controlRegistered = KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control)); + const bool serverRegistered = KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server)); + if (controlRegistered && serverRegistered) { + return true; + } + + const bool controlLockRegistered = KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::ControlLock)); + if (controlLockRegistered || controlRegistered) { + qCDebug(AKONADICORE_LOG) << "Akonadi server is already starting up"; + sInstance->setState(Starting); + return true; + } + + qCDebug(AKONADICORE_LOG) << "executing akonadi_control"; + QStringList args; + if (hasInstanceIdentifier()) { + args << QStringLiteral("--instance") << instanceIdentifier(); + } + const bool ok = QProcess::startDetached(QStringLiteral("akonadi_control"), args); + if (!ok) { + qCWarning(AKONADICORE_LOG) << "Unable to execute akonadi_control, falling back to D-Bus auto-launch"; + QDBusReply reply = KDBusConnectionPool::threadConnection().interface()->startService(ServerManager::serviceName(ServerManager::Control)); + if (!reply.isValid()) { + qCDebug(AKONADICORE_LOG) << "Akonadi server could not be started via D-Bus either: " + << reply.error().message(); + return false; + } + } + sInstance->setState(Starting); + return true; +} + +bool ServerManager::stop() +{ + QDBusInterface iface(ServerManager::serviceName(ServerManager::Control), + QStringLiteral("/ControlManager"), + QStringLiteral("org.freedesktop.Akonadi.ControlManager")); + if (!iface.isValid()) { + return false; + } + iface.call(QDBus::NoBlock, QStringLiteral("shutdown")); + sInstance->setState(Stopping); + return true; +} + +void ServerManager::showSelfTestDialog(QWidget *parent) +{ + Q_UNUSED(parent); + QProcess::startDetached(QStringLiteral("akonadiselftest")); +} + +bool ServerManager::isRunning() +{ + return state() == Running; +} + +ServerManager::State ServerManager::state() +{ + ServerManager::State previousState = NotRunning; + if (sInstance.exists()) { // be careful, this is called from the ServerManager::Private ctor, so using sInstance unprotected can cause infinite recursion + previousState = sInstance->mState; + } + + const bool serverUpgrading = KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::UpgradeIndicator)); + if (serverUpgrading) { + return Upgrading; + } + + const bool controlRegistered = KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control)); + const bool serverRegistered = KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server)); + if (controlRegistered && serverRegistered) { + // check if the server protocol is recent enough + if (sInstance.exists()) { + if (Internal::serverProtocolVersion() >= 0 && + Internal::serverProtocolVersion() < Protocol::version()) { + return Broken; + } + } + + // AgentManager is dangerous to use for agents themselves + if (Internal::clientType() == Internal::User) { + // besides the running server processes we also need at least one resource to be operational + AgentType::List agentTypes = AgentManager::self()->types(); + foreach (const AgentType &type, agentTypes) { + if (type.capabilities().contains(QStringLiteral("Resource"))) { + return Running; + } + } + return Broken; + } else { + return Running; + } + } + + const bool controlLockRegistered = KDBusConnectionPool::threadConnection().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::ControlLock)); + if (controlLockRegistered || controlRegistered) { + qCDebug(AKONADICORE_LOG) << "Akonadi server is already starting up"; + if (previousState == Running) { + return NotRunning; // we don't know if it's starting or stopping, probably triggered by someone else + } + return previousState; + } + + if (serverRegistered) { + qCWarning(AKONADICORE_LOG) << "Akonadi server running without control process!"; + return Broken; + } + + if (previousState == Starting) { // valid case where nothing is running (yet) + return previousState; + } + return NotRunning; +} + +QString ServerManager::instanceIdentifier() +{ + return Instance::identifier(); +} + +bool ServerManager::hasInstanceIdentifier() +{ + return Instance::hasIdentifier(); +} + +QString ServerManager::serviceName(ServerManager::ServiceType serviceType) +{ + switch (serviceType) { + case Server: + return DBus::serviceName(DBus::Server); + case Control: + return DBus::serviceName(DBus::Control); + case ControlLock: + return DBus::serviceName(DBus::ControlLock); + case UpgradeIndicator: + return DBus::serviceName(DBus::UpgradeIndicator); + } + Q_ASSERT(!"WTF?"); + return QString(); +} + +QString ServerManager::agentServiceName(ServiceAgentType agentType, const QString &identifier) +{ + switch (agentType) { + case Agent: + return DBus::agentServiceName(identifier, DBus::Agent); + case Resource: + return DBus::agentServiceName(identifier, DBus::Resource); + case Preprocessor: + return DBus::agentServiceName(identifier, DBus::Preprocessor); + } + Q_ASSERT(!"WTF?"); + return QString(); +} + +QString ServerManager::serverConfigFilePath(OpenMode openMode) +{ + return XdgBaseDirs::akonadiServerConfigFile(openMode == Akonadi::ServerManager::ReadOnly + ? XdgBaseDirs::ReadOnly + : XdgBaseDirs::ReadWrite); +} + +QString ServerManager::agentConfigFilePath(const QString &identifier) +{ + QString fullRelPath = QStringLiteral("akonadi"); + if (hasInstanceIdentifier()) { + fullRelPath += QStringLiteral("/instance/%1").arg(ServerManager::instanceIdentifier()); + } + fullRelPath += QStringLiteral("agent_config_%1").arg(identifier); + return Akonadi::XdgBaseDirs::findResourceFile("config", fullRelPath); +} + +QString ServerManager::addNamespace(const QString &string) +{ + if (Instance::hasIdentifier()) { + return string % QLatin1Char('_') % Instance::identifier(); + } + return string; +} + +int Internal::serverProtocolVersion() +{ + return ServerManagerPrivate::serverProtocolVersion; +} + +void Internal::setServerProtocolVersion(int version) +{ + ServerManagerPrivate::serverProtocolVersion = version; + if (sInstance.exists()) { + sInstance->checkStatusChanged(); + } +} + +Internal::ClientType Internal::clientType() +{ + return ServerManagerPrivate::clientType; +} + +void Internal::setClientType(ClientType type) +{ + ServerManagerPrivate::clientType = type; +} + + + +#include "moc_servermanager.cpp" diff --git a/src/core/servermanager.h b/src/core/servermanager.h new file mode 100644 index 0000000..282892e --- /dev/null +++ b/src/core/servermanager.h @@ -0,0 +1,214 @@ +/* + 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_SERVERMANAGER_H +#define AKONADI_SERVERMANAGER_H + +#include "akonadicore_export.h" + +#include +#include + +namespace Akonadi +{ + +class ServerManagerPrivate; + +/** + * @short Provides methods to control the Akonadi server process. + * + * Asynchronous, low-level control of the Akonadi server. + * Akonadi::Control provides a synchronous interface to some of the methods in here. + * + * @author Volker Krause + * @see Akonadi::Control + * @since 4.2 + */ +class AKONADICORE_EXPORT ServerManager : public QObject +{ + Q_OBJECT +public: + /** + * Enum for the various states the server can be in. + * @since 4.5 + */ + enum State { + NotRunning, ///< Server is not running, could be no one started it yet or it failed to start. + Starting, ///< Server was started but is not yet running. + Running, ///< Server is running and operational. + Stopping, ///< Server is shutting down. + Broken, ///< Server is not operational and an error has been detected. + Upgrading ///< Server is performing a database upgrade as part of a new startup. + }; + + /** + * Starts the server. This method returns imediately and does not wait + * until the server is actually up and running. + * @return @c true if the start was possible (which not necessarily means + * the server is really running though) and @c false if an immediate error occurred. + * @see Akonadi::Control::start() + */ + static bool start(); + + /** + * Stops the server. This methods returns immediately after the shutdown + * command has been send and does not wait until the server is actually + * shut down. + * @return @c true if the shutdown command was sent successfully, @c false + * otherwise + */ + static bool stop(); + + /** + * Shows the Akonadi self test dialog, which tests Akonadi for various problems + * and reports these to the user if. + * @param parent the parent widget for the dialog + */ + static void showSelfTestDialog(QWidget *parent); + + /** + * Checks if the server is available currently. For more detailed status information + * see state(). + * @see state() + */ + static bool isRunning(); + + /** + * Returns the state of the server. + * @since 4.5 + */ + static State state(); + + /** + * Returns the identifier of the Akonadi instance we are connected to. This is usually + * an empty string (representing the default instance), unless you have explicitly set + * the AKONADI_INSTANCE environment variable to connect to a different one. + * @since 4.10 + */ + static QString instanceIdentifier(); + + /** + * Returns @c true if we are connected to a non-default Akonadi server instance. + * @since 4.10 + */ + static bool hasInstanceIdentifier(); + + /** + * Types of known D-Bus services. + * @since 4.10 + */ + enum ServiceType { + Server, + Control, + ControlLock, + UpgradeIndicator + }; + + /** + * Returns the namespaced D-Bus service name for @p serviceType. + * Use this rather the raw service name strings in order to support usage of a non-default + * instance of the Akonadi server. + * @param serviceType the service type for which to return the D-Bus name + * @since 4.10 + */ + static QString serviceName(ServiceType serviceType); + + /** + * Known agent types. + * @since 4.10 + */ + enum ServiceAgentType { + Agent, + Resource, + Preprocessor + }; + + /** + * Returns the namespaced D-Bus service name for an agent of type @p agentType with agent + * identifier @p identifier. + * @param agentType the agent type to use for D-Bus base name + * @param identifier the agent identifier to include in the D-Bus name + * @since 4.10 + */ + static QString agentServiceName(ServiceAgentType agentType, const QString &identifier); + + /** + * Adds the multi-instance namespace to @p string if required (with '_' as separator). + * Use whenever a multi-instance safe name is required (configfiles, identifiers, ...). + * @param string the string to adapt + * @since 4.10 + */ + static QString addNamespace(const QString &string); + + /** + * Returns the singleton instance of this class, for connecting to its + * signals + */ + static ServerManager *self(); + + enum OpenMode { + ReadOnly, + ReadWrite + }; + /** + * Returns absolute path to akonadiserverrc file with Akonadi server + * configuration. + */ + static QString serverConfigFilePath(OpenMode openMode); + + /** + * Returns absolute path to configuration file of an agent identified by + * given @p identifier. + */ + static QString agentConfigFilePath(const QString &identifier); + +Q_SIGNALS: + /** + * Emitted whenever the server becomes fully operational. + */ + void started(); + + /** + * Emitted whenever the server becomes unavailable. + */ + void stopped(); + + /** + * Emitted whenever the server state changes. + * @param state the new server state + * @since 4.5 + */ + void stateChanged(Akonadi::ServerManager::State state); + +private: + //@cond PRIVATE + friend class ServerManagerPrivate; + ServerManager(ServerManagerPrivate *dd); + ServerManagerPrivate *const d; + Q_PRIVATE_SLOT(d, void serviceOwnerChanged(const QString &, const QString &, const QString &)) + Q_PRIVATE_SLOT(d, void checkStatusChanged()) + Q_PRIVATE_SLOT(d, void timeout()) + //@endcond +}; + +} + +Q_DECLARE_METATYPE(Akonadi::ServerManager::State) + +#endif diff --git a/src/core/servermanager_p.h b/src/core/servermanager_p.h new file mode 100644 index 0000000..85f17b6 --- /dev/null +++ b/src/core/servermanager_p.h @@ -0,0 +1,47 @@ +/* + 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_SERVERMANAGER_P_H +#define AKONADI_SERVERMANAGER_P_H + +#include "akonadicore_export.h" + +class QString; + +namespace Akonadi +{ + +namespace Internal +{ + +AKONADICORE_EXPORT int serverProtocolVersion(); +AKONADICORE_EXPORT void setServerProtocolVersion(int version); + +enum ClientType { + User, + Agent, + Resource +}; +AKONADICORE_EXPORT ClientType clientType(); +AKONADICORE_EXPORT void setClientType(ClientType type); + +} + +} +#endif diff --git a/src/core/session.cpp b/src/core/session.cpp new file mode 100644 index 0000000..d859237 --- /dev/null +++ b/src/core/session.cpp @@ -0,0 +1,420 @@ +/* + 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 "session.h" +#include "session_p.h" + +#include "job.h" +#include "job_p.h" +#include "servermanager.h" +#include "servermanager_p.h" +#include "protocolhelper_p.h" +#include "connectionthread_p.h" +#include "private/standarddirs_p.h" +#include "private/protocol_p.h" + +#include "akonadicore_debug.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// ### FIXME pipelining got broken by switching result emission in JobPrivate::handleResponse to delayed emission +// in order to work around exec() deadlocks. As a result of that Session knows to late about a finished job and still +// sends responses for the next one to the already finished one +#define PIPELINE_LENGTH 0 +//#define PIPELINE_LENGTH 2 + +using namespace Akonadi; + +//@cond PRIVATE + +void SessionPrivate::startNext() +{ + QTimer::singleShot(0, mParent, SLOT(doStartNext())); +} + +void SessionPrivate::reconnect() +{ + if (!connThread) { + connThread = new ConnectionThread(sessionId); + mParent->connect(connThread, &ConnectionThread::reconnected, mParent, &Session::reconnected, + Qt::QueuedConnection); + mParent->connect(connThread, SIGNAL(commandReceived(qint64,Akonadi::Protocol::Command)), + mParent, SLOT(handleCommand(qint64, Akonadi::Protocol::Command)), + Qt::QueuedConnection); + mParent->connect(connThread, SIGNAL(socketDisconnected()), mParent, SLOT(socketDisconnected()), + Qt::QueuedConnection); + mParent->connect(connThread, SIGNAL(socketError(QString)), mParent, SLOT(socketError(QString)), + Qt::QueuedConnection); + + } + + connThread->reconnect(); +} + +QString SessionPrivate::connectionFile() +{ + return StandardDirs::saveDir("config") + QStringLiteral("/akonadiconnectionrc"); +} + +void SessionPrivate::socketError(const QString &error) +{ + qCWarning(AKONADICORE_LOG) << "Socket error occurred:" << error; + socketDisconnected(); +} + +void SessionPrivate::socketDisconnected() +{ + if (currentJob) { + currentJob->d_ptr->lostConnection(); + } + connected = false; +} + +bool SessionPrivate::handleCommand(qint64 tag, const Protocol::Command &cmd) +{ + // Handle Hello response -> send Login + if (cmd.type() == Protocol::Command::Hello) { + Protocol::HelloResponse hello(cmd); + if (hello.isError()) { + qCWarning(AKONADICORE_LOG) << "Error when establishing connection with Akonadi server:" << hello.errorMessage(); + connThread->disconnect(); + QTimer::singleShot(1000, connThread, &ConnectionThread::reconnect); + return false; + } + + qCDebug(AKONADICORE_LOG) << "Connected to" << hello.serverName() << ", using protocol version" << hello.protocolVersion(); + qCDebug(AKONADICORE_LOG) << "Server says:" << hello.message(); + // Version mismatch is handled in SessionPrivate::startJob() so that + // we can report the error out via KJob API + protocolVersion = hello.protocolVersion(); + Internal::setServerProtocolVersion(protocolVersion); + + Protocol::LoginCommand login(sessionId); + sendCommand(nextTag(), login); + return true; + } + + // Login response + if (cmd.type() == Protocol::Command::Login) { + Protocol::LoginResponse login(cmd); + if (login.isError()) { + qCWarning(AKONADICORE_LOG) << "Unable to login to Akonadi server:" << login.errorMessage(); + connThread->disconnect(); + QTimer::singleShot(1000, mParent, SLOT(reconnect())); + return false; + } + + connected = true; + startNext(); + return true; + } + + // work for the current job + if (currentJob) { + currentJob->d_ptr->handleResponse(tag, cmd); + } + + return true; +} + +bool SessionPrivate::canPipelineNext() +{ + if (queue.isEmpty() || pipeline.count() >= PIPELINE_LENGTH) { + return false; + } + if (pipeline.isEmpty() && currentJob) { + return currentJob->d_ptr->mWriteFinished; + } + if (!pipeline.isEmpty()) { + return pipeline.last()->d_ptr->mWriteFinished; + } + return false; +} + +void SessionPrivate::doStartNext() +{ + if (!connected || (queue.isEmpty() && pipeline.isEmpty())) { + return; + } + if (canPipelineNext()) { + Akonadi::Job *nextJob = queue.dequeue(); + pipeline.enqueue(nextJob); + startJob(nextJob); + } + if (jobRunning) { + return; + } + jobRunning = true; + if (!pipeline.isEmpty()) { + currentJob = pipeline.dequeue(); + } else { + currentJob = queue.dequeue(); + startJob(currentJob); + } +} + +void SessionPrivate::startJob(Job *job) +{ + if (protocolVersion != Protocol::version()) { + job->setError(Job::ProtocolVersionMismatch); + if (protocolVersion < Protocol::version()) { + job->setErrorText(i18n("Protocol version mismatch. Server version is newer (%1) than ours (%2). " + "If you updated your system recently please restart the Akonadi server.", + protocolVersion, Protocol::version())); + qCWarning(AKONADICORE_LOG) << "Protocol version mismatch. Server version is newer (" << protocolVersion << ") than ours (" << Protocol::version() << "). " + "If you updated your system recently please restart the Akonadi server."; + } else { + job->setErrorText(i18n("Protocol version mismatch. Server version is older (%1) than ours (%2). " + "If you updated your system recently please restart all KDE PIM applications.", + protocolVersion, Protocol::version())); + qCWarning(AKONADICORE_LOG) << "Protocol version mismatch. Server version is older (" << protocolVersion << ") than ours (" << Protocol::version() << "). " + "If you updated your system recently please restart all KDE PIM applications."; + } + job->emitResult(); + } else { + job->d_ptr->startQueued(); + } +} + +void SessionPrivate::endJob(Job *job) +{ + job->emitResult(); +} + +void SessionPrivate::jobDone(KJob *job) +{ + // ### careful, this method can be called from the QObject dtor of job (see jobDestroyed() below) + // so don't call any methods on job itself + if (job == currentJob) { + if (pipeline.isEmpty()) { + jobRunning = false; + currentJob = 0; + } else { + currentJob = pipeline.dequeue(); + } + startNext(); + } else { + // non-current job finished, likely canceled while still in the queue + queue.removeAll(static_cast(job)); + // ### likely not enough to really cancel already running jobs + pipeline.removeAll(static_cast(job)); + } +} + +void SessionPrivate::jobWriteFinished(Akonadi::Job *job) +{ + Q_ASSERT((job == currentJob && pipeline.isEmpty()) || (job = pipeline.last())); + Q_UNUSED(job); + + startNext(); +} + +void SessionPrivate::jobDestroyed(QObject *job) +{ + // careful, accessing non-QObject methods of job will fail here already + jobDone(static_cast(job)); +} + +void SessionPrivate::addJob(Job *job) +{ + queue.append(job); + QObject::connect(job, SIGNAL(result(KJob*)), mParent, SLOT(jobDone(KJob*))); + QObject::connect(job, SIGNAL(writeFinished(Akonadi::Job*)), mParent, SLOT(jobWriteFinished(Akonadi::Job*))); + QObject::connect(job, SIGNAL(destroyed(QObject*)), mParent, SLOT(jobDestroyed(QObject*))); + startNext(); +} + +qint64 SessionPrivate::nextTag() +{ + return theNextTag++; +} + +void SessionPrivate::sendCommand(qint64 tag, const Protocol::Command &command) +{ + connThread->sendCommand(tag, command); +} + +void SessionPrivate::serverStateChanged(ServerManager::State state) +{ + qCDebug(AKONADICORE_LOG) << "============== SESSION: Server state changed to " << state; + if (state == ServerManager::Running && !connected) { + reconnect(); + } else if (!connected && state == ServerManager::Broken) { + // If the server is broken, cancel all pending jobs, otherwise they will be + // blocked forever and applications waiting for them to finish would be stuck + Q_FOREACH (Job *job, queue) { + job->setError(Job::ConnectionFailed); + job->kill(KJob::EmitResult); + } + } else if (state == ServerManager::Stopping) { + delete connThread; + connThread = Q_NULLPTR; + } +} + +void SessionPrivate::itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + // only deal with the queue, for the guys in the pipeline it's too late already anyway + // and they shouldn't have gotten there if they depend on a preceding job anyway. + Q_FOREACH (Job *job, queue) { + job->d_ptr->updateItemRevision(itemId, oldRevision, newRevision); + } +} + +//@endcond + +SessionPrivate::SessionPrivate(Session *parent) + : mParent(parent) + , thread(0) + , connThread(0) + , protocolVersion(0) + , currentJob(0) +{ + // Shutdown the thread before QApplication event loop quits - the + // thread()->wait() mechanism in ConnectionThread dtor crashes sometimes + // when called from QApplication destructor + connThreadCleanUp = QObject::connect(qApp, &QCoreApplication::aboutToQuit, + [this]() { + delete connThread; + connThread = Q_NULLPTR; + }); +} + +SessionPrivate::~SessionPrivate() +{ + QObject::disconnect(connThreadCleanUp); + delete connThread; +} + +void SessionPrivate::init(const QByteArray &id) +{ + qCDebug(AKONADICORE_LOG) << id; + + if (!id.isEmpty()) { + sessionId = id; + } else { + sessionId = QCoreApplication::instance()->applicationName().toUtf8() + + '-' + QByteArray::number(qrand()); + } + connected = false; + theNextTag = 2; + jobRunning = false; + + if (ServerManager::state() == ServerManager::NotRunning) { + ServerManager::start(); + } + mParent->connect(ServerManager::self(), SIGNAL(stateChanged(Akonadi::ServerManager::State)), + SLOT(serverStateChanged(Akonadi::ServerManager::State))); + + reconnect(); +} + +void SessionPrivate::forceReconnect() +{ + jobRunning = false; + connected = false; + if (connThread) { + connThread->forceReconnect(); + } + QMetaObject::invokeMethod(mParent, "reconnect", Qt::QueuedConnection); +} + +Session::Session(const QByteArray &sessionId, QObject *parent) + : QObject(parent) + , d(new SessionPrivate(this)) +{ + d->init(sessionId); +} + +Session::Session(SessionPrivate *dd, const QByteArray &sessionId, QObject *parent) + : QObject(parent) + , d(dd) +{ + d->mParent = this; + d->init(sessionId); +} + +Session::~Session() +{ + clear(); + delete d; +} + +QByteArray Session::sessionId() const +{ + return d->sessionId; +} + +Q_GLOBAL_STATIC(QThreadStorage, instances) + +void SessionPrivate::createDefaultSession(const QByteArray &sessionId) +{ + Q_ASSERT_X(!sessionId.isEmpty(), "SessionPrivate::createDefaultSession", + "You tried to create a default session with empty session id!"); + Q_ASSERT_X(!instances()->hasLocalData(), "SessionPrivate::createDefaultSession", + "You tried to create a default session twice!"); + + instances()->setLocalData(new Session(sessionId)); +} + +void SessionPrivate::setDefaultSession(Session *session) +{ + instances()->setLocalData(session); +} + +Session *Session::defaultSession() +{ + if (!instances()->hasLocalData()) { + instances()->setLocalData(new Session()); + } + return instances()->localData(); +} + +void Session::clear() +{ + Q_FOREACH(Job *job, d->queue) { + job->kill(KJob::EmitResult); // safe, not started yet + } + d->queue.clear(); + Q_FOREACH(Job *job, d->pipeline) { + job->d_ptr->mStarted = false; // avoid killing/reconnect loops + job->kill(KJob::EmitResult); + } + d->pipeline.clear(); + if (d->currentJob) { + d->currentJob->d_ptr->mStarted = false; // avoid killing/reconnect loops + d->currentJob->kill(KJob::EmitResult); + } + d->forceReconnect(); +} + +#include "moc_session.cpp" diff --git a/src/core/session.h b/src/core/session.h new file mode 100644 index 0000000..486313c --- /dev/null +++ b/src/core/session.h @@ -0,0 +1,147 @@ +/* + 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_H +#define AKONADI_SESSION_H + +#include "akonadicore_export.h" +#include + +class KJob; +class FakeSession; + +namespace Akonadi +{ +namespace Protocol +{ +class Command; +} + +class Job; +class SessionPrivate; +class ChangeNotificationDependenciesFactory; + +/** + * @short A communication session with the Akonadi storage. + * + * Every Job object has to be associated with a Session. + * The session is responsible of scheduling its jobs. + * For now only a simple serial execution is implemented (the IMAP-like + * protocol to communicate with the storage backend is capable of parallel + * execution on a single session though). + * + * @code + * + * using namespace Akonadi; + * + * Session *session = new Session( "mySession" ); + * + * CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), + * CollectionFetchJob::Recursive, + * session ); + * + * connect( job, SIGNAL(result(KJob*)), this, SLOT(slotResult(KJob*)) ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT Session : public QObject +{ + Q_OBJECT + + friend class Job; + friend class JobPrivate; + friend class SessionPrivate; + +public: + /** + * Creates a new session. + * + * @param sessionId The identifier for this session, will be a + * random value if empty. + * @param parent The parent object. + * + * @see defaultSession() + */ + explicit Session(const QByteArray &sessionId = QByteArray(), QObject *parent = Q_NULLPTR); + + /** + * Destroys the session. + */ + ~Session(); + + /** + * Returns the session identifier. + */ + QByteArray sessionId() const; + + /** + * Returns the default session for this thread. + */ + static Session *defaultSession(); + + /** + * Stops all jobs queued for execution. + */ + void clear(); + +Q_SIGNALS: + /** + * This signal is emitted whenever the session has been reconnected + * to the server (e.g. after a server crash). + * + * @since 4.6 + */ + void reconnected(); + +protected: + /** + * Creates a new session with shared private object. + * + * @param d The private object. + * @param sessionId The identifier for this session, will be a + * random value if empty. + * @param parent The parent object. + * + * @note This constructor is needed for unit testing only. + */ + explicit Session(SessionPrivate *d, const QByteArray &sessionId = QByteArray(), QObject *parent = Q_NULLPTR); + +private: + //@cond PRIVATE + SessionPrivate *const d; + friend class ::FakeSession; + friend class ChangeNotificationDependenciesFactory; + + Q_PRIVATE_SLOT(d, void reconnect()) + Q_PRIVATE_SLOT(d, void socketError(const QString &error)) + Q_PRIVATE_SLOT(d, void socketDisconnected()) + Q_PRIVATE_SLOT(d, bool handleCommand(qint64 tag, const Akonadi::Protocol::Command &cmd)) + Q_PRIVATE_SLOT(d, void doStartNext()) + Q_PRIVATE_SLOT(d, void jobDone(KJob *)) + Q_PRIVATE_SLOT(d, void jobWriteFinished(Akonadi::Job *)) + Q_PRIVATE_SLOT(d, void jobDestroyed(QObject *)) + Q_PRIVATE_SLOT(d, void serverStateChanged(Akonadi::ServerManager::State)) + //@endcond PRIVATE +}; + +} + +#endif diff --git a/src/core/session_p.h b/src/core/session_p.h new file mode 100644 index 0000000..1be448e --- /dev/null +++ b/src/core/session_p.h @@ -0,0 +1,146 @@ +/* + 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 "akonadicore_export.h" +#include "session.h" +#include "item.h" +#include "servermanager.h" + +#include + +#include +#include +#include +#include + +class QIODevice; + +namespace Akonadi +{ +class ConnectionThread; + +namespace Protocol +{ +class Command; +} + +/** + * @internal + */ +class AKONADICORE_EXPORT SessionPrivate +{ +public: + explicit SessionPrivate(Session *parent); + + virtual ~SessionPrivate(); + + virtual void init(const QByteArray &sessionId); + + void startNext(); + /// Disconnects a previously existing connection and tries to reconnect + void forceReconnect(); + /// Attemps to establish a connections to the Akonadi server. + virtual void reconnect(); + void serverStateChanged(ServerManager::State); + void socketDisconnected(); + void socketError(const QString &error); + void dataReceived(); + virtual bool handleCommand(qint64 tag, const Protocol::Command &cmd); + void doStartNext(); + void startJob(Job *job); + + /** + @internal For testing purposes only. See FakeSesson. + @param job the job to end + */ + void endJob(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. + * @param sessionId the id of new default session + */ + static void createDefaultSession(const QByteArray &sessionId); + + /** + * Sets the default session. + * @internal Only for unit tests. + */ + static void setDefaultSession(Session *session); + + /** + Associates the given Job object with this session. + */ + virtual void addJob(Job *job); + + /** + Returns the next IMAP tag. + */ + qint64 nextTag(); + + /** + Sends the given command to server + */ + void sendCommand(qint64 tag, const Protocol::Command &command); + + /** + * Propagate item revision changes to following jobs. + */ + void itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision); + + /** + * Default location for akonadiconnectionrc + */ + static QString connectionFile(); + + Session *mParent; + QThread *thread; + ConnectionThread *connThread; + QMetaObject::Connection connThreadCleanUp; + QByteArray sessionId; + bool connected; + qint64 theNextTag; + int protocolVersion; + + // job management + QQueue queue; + QQueue pipeline; + Job *currentJob; + bool jobRunning; + + QFile *logFile; +}; + +} + +#endif diff --git a/src/core/sharedvaluepool_p.h b/src/core/sharedvaluepool_p.h new file mode 100644 index 0000000..26a026a --- /dev/null +++ b/src/core/sharedvaluepool_p.h @@ -0,0 +1,57 @@ +/* + Copyright (c) 2010 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_SHAREDVALUEPOOL_P_H +#define AKONADI_SHAREDVALUEPOOL_P_H + +#include + +namespace Akonadi +{ +namespace Internal +{ + +/** + * Pool of implicitly shared values, use for optimizing memory use + * when having a large amount of copies from a small set of different values. + */ +template class Container> +class SharedValuePool +{ +public: + /** Returns the shared value equal to @p value .*/ + T sharedValue(const T &value) + { + // for small pool sizes this is actually faster than using lower_bound and a sorted vector + typename Container::const_iterator it = std::find(m_pool.constBegin(), m_pool.constEnd(), value); + if (it != m_pool.constEnd()) { + return *it; + } + m_pool.push_back(value); + return value; + } + +private: + Container m_pool; +}; + +} +} + +#endif diff --git a/src/core/specialcollectionattribute.cpp b/src/core/specialcollectionattribute.cpp new file mode 100644 index 0000000..43b3606 --- /dev/null +++ b/src/core/specialcollectionattribute.cpp @@ -0,0 +1,89 @@ +/* + Copyright 2009 Constantin Berzan + + 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 "specialcollectionattribute.h" +#include "attributefactory.h" + +using namespace Akonadi; + +/** + @internal +*/ +class Q_DECL_HIDDEN SpecialCollectionAttribute::Private +{ +public: + QByteArray mType; +}; + +SpecialCollectionAttribute::SpecialCollectionAttribute(const QByteArray &type) + : d(new Private) +{ + d->mType = type; +} + +SpecialCollectionAttribute::~SpecialCollectionAttribute() +{ + delete d; +} + +SpecialCollectionAttribute *SpecialCollectionAttribute::clone() const +{ + return new SpecialCollectionAttribute(d->mType); +} + +QByteArray SpecialCollectionAttribute::type() const +{ + static const QByteArray sType("SpecialCollectionAttribute"); + return sType; +} + +QByteArray SpecialCollectionAttribute::serialized() const +{ + return d->mType; +} + +void SpecialCollectionAttribute::deserialize(const QByteArray &data) +{ + d->mType = data; +} + +void SpecialCollectionAttribute::setCollectionType(const QByteArray &type) +{ + d->mType = type; +} + +QByteArray SpecialCollectionAttribute::collectionType() const +{ + return d->mType; +} + +// Register the attribute when the library is loaded. +namespace +{ + +bool dummySpecialCollectionAttribute() +{ + using namespace Akonadi; + AttributeFactory::registerAttribute(); + return true; +} + +const bool registeredSpecialCollectionAttribute = dummySpecialCollectionAttribute(); + +} diff --git a/src/core/specialcollectionattribute.h b/src/core/specialcollectionattribute.h new file mode 100644 index 0000000..2e2b985 --- /dev/null +++ b/src/core/specialcollectionattribute.h @@ -0,0 +1,77 @@ +/* + Copyright 2009 Constantin Berzan + + 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_SPECIALCOLLECTIONATTRIBUTE_P_H +#define AKONADI_SPECIALCOLLECTIONATTRIBUTE_P_H + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +namespace Akonadi +{ + +/** + * @short An Attribute that stores the special collection type of a collection. + * + * All collections registered with SpecialCollections must have this attribute set. + * + * @author Constantin Berzan + * @since 4.4 + */ +class AKONADICORE_EXPORT SpecialCollectionAttribute : public Akonadi::Attribute +{ +public: + /** + * Creates a new special collection attribute. + */ + explicit SpecialCollectionAttribute(const QByteArray &type = QByteArray()); + + /** + * Destroys the special collection attribute. + */ + virtual ~SpecialCollectionAttribute(); + + /** + * Sets the special collections @p type of the collection. + */ + void setCollectionType(const QByteArray &type); + + /** + * Returns the special collections type of the collection. + */ + QByteArray collectionType() const; + + /* reimpl */ + SpecialCollectionAttribute *clone() const Q_DECL_OVERRIDE; + QByteArray type() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + +private: + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} // namespace Akonadi + +#endif // AKONADI_SPECIALCOLLECTIONATTRIBUTE_H diff --git a/src/core/specialcollections.cpp b/src/core/specialcollections.cpp new file mode 100644 index 0000000..7417d7f --- /dev/null +++ b/src/core/specialcollections.cpp @@ -0,0 +1,279 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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 "specialcollections.h" +#include "akonadicore_debug.h" +#include "specialcollections_p.h" +#include "specialcollectionattribute.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collectionmodifyjob.h" +#include "collectionfetchjob.h" +#include "monitor.h" +#include "collectionfetchscope.h" + +#include + +#include +#include +#include + +using namespace Akonadi; + +SpecialCollectionsPrivate::SpecialCollectionsPrivate(KCoreConfigSkeleton *settings, SpecialCollections *qq) + : q(qq) + , mSettings(settings) + , mBatchMode(false) +{ + mMonitor = new Monitor(q); + mMonitor->fetchCollectionStatistics(true); + + /// In order to know if items are added or deleted + /// from one of our specialcollection folders, + /// we have to watch all mail item add/move/delete notifications + /// and check for the parent to see if it is one we care about + QObject::connect(mMonitor, SIGNAL(collectionRemoved(Akonadi::Collection)), + q, SLOT(collectionRemoved(Akonadi::Collection))); + QObject::connect(mMonitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), + q, SLOT(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics))); +} + +SpecialCollectionsPrivate::~SpecialCollectionsPrivate() +{ +} + +QString SpecialCollectionsPrivate::defaultResourceId() const +{ + if (mDefaultResourceId.isEmpty()) { + mSettings->load(); + const KConfigSkeletonItem *item = mSettings->findItem(QStringLiteral("DefaultResourceId")); + Q_ASSERT(item); + + mDefaultResourceId = item->property().toString(); + } + return mDefaultResourceId; +} + +void SpecialCollectionsPrivate::emitChanged(const QString &resourceId) +{ + if (mBatchMode) { + mToEmitChangedFor.insert(resourceId); + } else { + qCDebug(AKONADICORE_LOG) << "Emitting changed for" << resourceId; + const AgentInstance agentInstance = AgentManager::self()->instance(resourceId); + emit q->collectionsChanged(agentInstance); + // first compare with local value then with config value (which also updates the local value) + if (resourceId == mDefaultResourceId || resourceId == defaultResourceId()) { + qCDebug(AKONADICORE_LOG) << "Emitting defaultFoldersChanged."; + emit q->defaultCollectionsChanged(); + } + } +} + +void SpecialCollectionsPrivate::collectionRemoved(const Collection &collection) +{ + qCDebug(AKONADICORE_LOG) << "Collection" << collection.id() << "resource" << collection.resource(); + if (mFoldersForResource.contains(collection.resource())) { + + // Retrieve the list of special folders for the resource the collection belongs to + QHash &folders = mFoldersForResource[collection.resource()]; + { + QMutableHashIterator it(folders); + while (it.hasNext()) { + it.next(); + if (it.value() == collection) { + // The collection to be removed is a special folder + it.remove(); + emitChanged(collection.resource()); + } + } + } + + if (folders.isEmpty()) { + // This resource has no more folders, so remove it completely. + mFoldersForResource.remove(collection.resource()); + } + } +} + +void SpecialCollectionsPrivate::collectionStatisticsChanged(Akonadi::Collection::Id collectionId, const Akonadi::CollectionStatistics &statistics) +{ + // need to get the name of the collection in order to be able to check if we are storing it, + // but we have the id from the monitor, so fetch the name. + Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(Collection(collectionId), Akonadi::CollectionFetchJob::Base); + fetchJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::None); + fetchJob->setProperty("statistics", QVariant::fromValue(statistics)); + + q->connect(fetchJob, SIGNAL(result(KJob*)), q, SLOT(collectionFetchJobFinished(KJob*))); +} + +void SpecialCollectionsPrivate::collectionFetchJobFinished(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error fetching collection to get name from id for statistics updating in specialcollections!"; + return; + } + + const Akonadi::CollectionFetchJob *fetchJob = qobject_cast(job); + + Q_ASSERT(fetchJob->collections().size() > 0); + const Akonadi::Collection collection = fetchJob->collections().at(0); + const Akonadi::CollectionStatistics statistics = fetchJob->property("statistics").value(); + + mFoldersForResource[collection.resource()][collection.name().toUtf8()].setStatistics(statistics); +} + +void SpecialCollectionsPrivate::beginBatchRegister() +{ + Q_ASSERT(!mBatchMode); + mBatchMode = true; + Q_ASSERT(mToEmitChangedFor.isEmpty()); +} + +void SpecialCollectionsPrivate::endBatchRegister() +{ + Q_ASSERT(mBatchMode); + mBatchMode = false; + + foreach (const QString &resourceId, mToEmitChangedFor) { + emitChanged(resourceId); + } + + mToEmitChangedFor.clear(); +} + +void SpecialCollectionsPrivate::forgetFoldersForResource(const QString &resourceId) +{ + if (mFoldersForResource.contains(resourceId)) { + foreach (const Collection &collection, mFoldersForResource[resourceId]) { + mMonitor->setCollectionMonitored(collection, false); + } + + mFoldersForResource.remove(resourceId); + emitChanged(resourceId); + } +} + +AgentInstance SpecialCollectionsPrivate::defaultResource() const +{ + const QString identifier = defaultResourceId(); + return AgentManager::self()->instance(identifier); +} + +SpecialCollections::SpecialCollections(KCoreConfigSkeleton *settings, QObject *parent) + : QObject(parent) + , d(new SpecialCollectionsPrivate(settings, this)) +{ +} + +SpecialCollections::~SpecialCollections() +{ + delete d; +} + +bool SpecialCollections::hasCollection(const QByteArray &type, const AgentInstance &instance) const +{ + return d->mFoldersForResource.value(instance.identifier()).contains(type); +} + +Akonadi::Collection SpecialCollections::collection(const QByteArray &type, const AgentInstance &instance) const +{ + return d->mFoldersForResource.value(instance.identifier()).value(type); +} + +void SpecialCollections::setSpecialCollectionType(const QByteArray &type, const Akonadi::Collection &collection) +{ + if (!collection.hasAttribute() || collection.attribute()->collectionType() != type) { + Collection attributeCollection(collection); + SpecialCollectionAttribute *attribute = attributeCollection.attribute(Collection::AddIfMissing); + attribute->setCollectionType(type); + new CollectionModifyJob(attributeCollection); + } +} + +void SpecialCollections::unsetSpecialCollection(const Akonadi::Collection &collection) +{ + if (collection.hasAttribute()) { + Collection attributeCollection(collection); + attributeCollection.removeAttribute(); + new CollectionModifyJob(attributeCollection); + } +} + +bool SpecialCollections::unregisterCollection(const Collection &collection) +{ + if (!collection.isValid()) { + qCWarning(AKONADICORE_LOG) << "Invalid collection."; + return false; + } + + const QString &resourceId = collection.resource(); + if (resourceId.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Collection has empty resourceId."; + return false; + } + + unsetSpecialCollection(collection); + + d->mMonitor->setCollectionMonitored(collection, false); + //Remove from list of collection + d->collectionRemoved(collection); + return true; +} + +bool SpecialCollections::registerCollection(const QByteArray &type, const Collection &collection) +{ + if (!collection.isValid()) { + qCWarning(AKONADICORE_LOG) << "Invalid collection."; + return false; + } + + const QString &resourceId = collection.resource(); + if (resourceId.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Collection has empty resourceId."; + return false; + } + + setSpecialCollectionType(type, collection); + + const Collection oldCollection = d->mFoldersForResource.value(resourceId).value(type); + if (oldCollection != collection) { + if (oldCollection.isValid()) { + d->mMonitor->setCollectionMonitored(oldCollection, false); + } + d->mMonitor->setCollectionMonitored(collection, true); + d->mFoldersForResource[resourceId].insert(type, collection); + d->emitChanged(resourceId); + } + + return true; +} + +bool SpecialCollections::hasDefaultCollection(const QByteArray &type) const +{ + return hasCollection(type, d->defaultResource()); +} + +Akonadi::Collection SpecialCollections::defaultCollection(const QByteArray &type) const +{ + return collection(type, d->defaultResource()); +} + +#include "moc_specialcollections.cpp" diff --git a/src/core/specialcollections.h b/src/core/specialcollections.h new file mode 100644 index 0000000..596c122 --- /dev/null +++ b/src/core/specialcollections.h @@ -0,0 +1,179 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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_SPECIALCOLLECTIONS_H +#define AKONADI_SPECIALCOLLECTIONS_H + +#include "akonadicore_export.h" +#include "collection.h" +#include "item.h" + +#include + +class KCoreConfigSkeleton; +class KJob; + +namespace Akonadi +{ + +class AgentInstance; +class SpecialCollectionsPrivate; + +/** + @short An interface to special collections. + + This class is the central interface to special collections like inbox or + outbox in a mail resource or recent contacts in a contacts resource. + The class is not meant to be used directly, but to inherit the a type + specific special collections class from it (e.g. SpecialMailCollections). + + To check whether a special collection is available, simply use the hasCollection() and + hasDefaultCollection() methods. Available special collections are accessible through + the collection() and defaultCollection() methods. + + To create a special collection, use a SpecialCollectionsRequestJob. + This will create the special collections you request and automatically + register them with SpecialCollections, so that it now knows they are available. + + This class monitors all special collections known to it, and removes it + from the known list if they are deleted. Note that this class does not + automatically rebuild the collections that disappeared. + + The defaultCollectionsChanged() and collectionsChanged() signals are emitted when + the special collections for a resource change (i.e. some became available or some + become unavailable). + + @author Constantin Berzan + @since 4.4 +*/ +class AKONADICORE_EXPORT SpecialCollections : public QObject +{ + Q_OBJECT + +public: + /** + * Destroys the special collections object. + */ + ~SpecialCollections(); + + /** + * Returns whether the given agent @p instance has a special collection of + * the given @p type. + */ + bool hasCollection(const QByteArray &type, const AgentInstance &instance) const; + + /** + * Returns the special collection of the given @p type in the given agent + * @p instance, or an invalid collection if such a collection is unknown. + */ + Akonadi::Collection collection(const QByteArray &type, const AgentInstance &instance) const; + + /** + * Registers the given @p collection as a special collection + * with the given @p type. + * @param type the special type of @c collection + * @param collection the given collection to register + * The collection must be owned by a valid resource. + * Registering a new collection of a previously registered type forgets the + * old collection. + */ + bool registerCollection(const QByteArray &type, const Akonadi::Collection &collection); + + /** + * Unregisters the given @p collection as a special collection. + * @param type the special type of @c collection + * @since 4.12 + */ + bool unregisterCollection(const Collection &collection); + + /** + * unsets the special collection attribute which marks @p collection as being a special + * collection. + * @since 4.12 + */ + static void unsetSpecialCollection(const Akonadi::Collection &collection); + + /** + * Sets the special collection attribute which marks @p collection as being a special + * collection of type @p type. + * This is typically used by configuration dialog for resources, when the user can choose + * a specific special collection (ex: IMAP trash). + * + * @since 4.11 + */ + static void setSpecialCollectionType(const QByteArray &type, const Akonadi::Collection &collection); + + /** + * Returns whether the default resource has a special collection of + * the given @p type. + */ + bool hasDefaultCollection(const QByteArray &type) const; + + /** + * Returns the special collection of given @p type in the default + * resource, or an invalid collection if such a collection is unknown. + */ + Akonadi::Collection defaultCollection(const QByteArray &type) const; + +Q_SIGNALS: + /** + * Emitted when the special collections for a resource have been changed + * (for example, some become available, or some become unavailable). + * + * @param instance The instance of the resource the collection belongs to. + */ + void collectionsChanged(const Akonadi::AgentInstance &instance); + + /** + * Emitted when the special collections for the default resource have + * been changed (for example, some become available, or some become unavailable). + */ + void defaultCollectionsChanged(); + +protected: + /** + * Creates a new special collections object. + * + * @param config The configuration skeleton that provides the default resource id. + * @param parent The parent object. + */ + explicit SpecialCollections(KCoreConfigSkeleton *config, QObject *parent = Q_NULLPTR); + +private: + //@cond PRIVATE + friend class SpecialCollectionsRequestJob; + friend class SpecialCollectionsRequestJobPrivate; + friend class SpecialCollectionsPrivate; + +#if 1 // TODO do this only if building tests: + friend class SpecialMailCollectionsTesting; + friend class LocalFoldersTest; +#endif + + SpecialCollectionsPrivate *const d; + + Q_PRIVATE_SLOT(d, void collectionRemoved(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d, void collectionStatisticsChanged(Akonadi::Collection::Id, const Akonadi::CollectionStatistics &)) + Q_PRIVATE_SLOT(d, void collectionFetchJobFinished(KJob *)) + //@endcond +}; + +} // namespace Akonadi + +#endif // AKONADI_SPECIALCOLLECTIONS_H diff --git a/src/core/specialcollections_p.h b/src/core/specialcollections_p.h new file mode 100644 index 0000000..1eb13a4 --- /dev/null +++ b/src/core/specialcollections_p.h @@ -0,0 +1,93 @@ +/* + Copyright (c) 2009 Constantin Berzan + + 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_SPECIALCOLLECTIONS_P_H +#define AKONADI_SPECIALCOLLECTIONS_P_H + +#include +#include + +#include "akonaditests_export.h" + +#include "collection.h" +#include "collectionstatistics.h" +#include "item.h" + +class KCoreConfigSkeleton; +class KJob; + +namespace Akonadi +{ + +class AgentInstance; +class SpecialCollections; +class Monitor; + +/** + @internal +*/ +class AKONADI_TESTS_EXPORT SpecialCollectionsPrivate +{ +public: + SpecialCollectionsPrivate(KCoreConfigSkeleton *settings, SpecialCollections *qq); + ~SpecialCollectionsPrivate(); + + QString defaultResourceId() const; + void emitChanged(const QString &resourceId); + void collectionRemoved(const Collection &collection); // slot + void collectionFetchJobFinished(KJob *job); // slot + void collectionStatisticsChanged(Akonadi::Collection::Id collectionId, + const Akonadi::CollectionStatistics &statistics); // slot + + /** + Forgets all folders owned by the given resource. + This method is used by SpecialCollectionsRequestJob. + @param resourceId the identifier of the resource for which to forget folders + */ + void forgetFoldersForResource(const QString &resourceId); + + /** + Avoids emitting the foldersChanged() signal until endBatchRegister() + is called. This is used to avoid emitting repeated signals when multiple + folders are registered in a row. + This method is used by SpecialCollectionsRequestJob. + */ + void beginBatchRegister(); + + /** + @see beginBatchRegister() + This method is used by SpecialCollectionsRequestJob. + */ + void endBatchRegister(); + + AgentInstance defaultResource() const; + + SpecialCollections *q; + KCoreConfigSkeleton *mSettings; + QHash > mFoldersForResource; + bool mBatchMode; + QSet mToEmitChangedFor; + Monitor *mMonitor; + + mutable QString mDefaultResourceId; +}; + +} // namespace Akonadi + +#endif // AKONADI_SPECIALCOLLECTIONS_P_H diff --git a/src/core/std_exception.h.in b/src/core/std_exception.h.in new file mode 100644 index 0000000..f2a4702 --- /dev/null +++ b/src/core/std_exception.h.in @@ -0,0 +1 @@ +#include "@std_exception_file@" \ No newline at end of file diff --git a/src/core/supertrait.h b/src/core/supertrait.h new file mode 100644 index 0000000..661d07a --- /dev/null +++ b/src/core/supertrait.h @@ -0,0 +1,50 @@ +/* + 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. +*/ + +#ifndef AKONADI_SUPERTRAIT_H +#define AKONADI_SUPERTRAIT_H + +namespace Akonadi +{ + +/** + @internal + @see super_class +*/ +template +struct SuperClassTrait { + typedef Super Type; +}; + +/** + Type trait to provide information about a base class for a given class. + Used eg. for the Akonadi payload mechanism. + + To provide base class introspection for own types, extend this trait as follows: + @code + namespace Akonadi + { + template <> struct SuperClass : public SuperClassTrait{}; + } + @endcode +*/ +template struct SuperClass : public SuperClassTrait {}; +} + +#endif diff --git a/src/core/tag.cpp b/src/core/tag.cpp new file mode 100644 index 0000000..ae0c1bf --- /dev/null +++ b/src/core/tag.cpp @@ -0,0 +1,260 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "tag.h" +#include "tag_p.h" +#include "tagattribute.h" +#include +#include + +using namespace Akonadi; + +const char Akonadi::Tag::PLAIN[] = "PLAIN"; +const char Akonadi::Tag::GENERIC[] = "GENERIC"; + +uint Akonadi::qHash(const Tag &tag) +{ + return ::qHash(tag.id()); +} + + +Tag::Tag() + : d_ptr(new TagPrivate) +{ + +} + +Tag::Tag(Tag::Id id) + : d_ptr(new TagPrivate) +{ + d_ptr->id = id; +} + +Tag::Tag(const QString &name) + : d_ptr(new TagPrivate) +{ + d_ptr->gid = name.toUtf8(); + d_ptr->type = PLAIN; +} + +Tag::Tag(const Tag &other) + : d_ptr(other.d_ptr) +{ +} + +Tag::~Tag() +{ +} + +Tag &Tag::operator=(const Tag &other) +{ + if (this != &other) { + d_ptr = other.d_ptr; + } + + return *this; +} + +bool Tag::operator==(const Tag &other) const +{ + // Valid tags are equal if their IDs are equal + if (isValid() && other.isValid()) { + return d_ptr->id == other.d_ptr->id; + } + + // Invalid tags are equal if their GIDs are non empty but equal + if (!d_ptr->gid.isEmpty() || !other.d_ptr->gid.isEmpty()) { + return d_ptr->gid == other.d_ptr->gid; + } + + // Invalid tags are equal if both are invalid + return !isValid() && !other.isValid(); +} + +bool Tag::operator!=(const Tag &other) const +{ + return !operator==(other); +} + +Tag Tag::fromUrl(const QUrl &url) +{ + if (url.scheme() != QLatin1String("akonadi")) { + return Tag(); + } + + const QString tagStr = QUrlQuery(url).queryItemValue(QStringLiteral("tag")); + bool ok = false; + Tag::Id itemId = tagStr.toLongLong(&ok); + if (!ok) { + return Tag(); + } + + return Tag(itemId); +} + +QUrl Tag::url() const +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("tag"), QString::number(id())); + + QUrl url; + url.setScheme(QStringLiteral("akonadi")); + url.setQuery(query); + return url; +} + +void Tag::addAttribute(Attribute *attr) +{ + if (d_ptr->mAttributes.contains(attr->type())) { + Attribute *existing = d_ptr->mAttributes.value(attr->type()); + if (attr == existing) { + return; + } + d_ptr->mAttributes.remove(attr->type()); + delete existing; + } + d_ptr->mAttributes.insert(attr->type(), attr); + d_ptr->mDeletedAttributes.remove(attr->type()); +} + +void Tag::removeAttribute(const QByteArray &type) +{ + d_ptr->mDeletedAttributes.insert(type); + delete d_ptr->mAttributes.take(type); +} + +bool Tag::hasAttribute(const QByteArray &type) const +{ + return d_ptr->mAttributes.contains(type); +} + +Attribute::List Tag::attributes() const +{ + return d_ptr->mAttributes.values(); +} + +void Tag::clearAttributes() +{ + Q_FOREACH (Attribute *attr, d_ptr->mAttributes) { + d_ptr->mDeletedAttributes.insert(attr->type()); + delete attr; + } + d_ptr->mAttributes.clear(); +} + +Attribute *Tag::attribute(const QByteArray &type) const +{ + if (d_ptr->mAttributes.contains(type)) { + return d_ptr->mAttributes.value(type); + } + return 0; +} + +void Tag::setId(Tag::Id identifier) +{ + d_ptr->id = identifier; +} + +Tag::Id Tag::id() const +{ + return d_ptr->id; +} + +void Tag::setGid(const QByteArray &gid) +{ + d_ptr->gid = gid; +} + +QByteArray Tag::gid() const +{ + return d_ptr->gid; +} + +void Tag::setRemoteId(const QByteArray &remoteId) +{ + d_ptr->remoteId = remoteId; +} + +QByteArray Tag::remoteId() const +{ + return d_ptr->remoteId; +} + +void Tag::setName(const QString &name) +{ + if (!name.isEmpty()) { + TagAttribute *const attr = attribute(Tag::AddIfMissing); + attr->setDisplayName(name); + } +} + +QString Tag::name() const +{ + const TagAttribute *const attr = attribute(); + const QString displayName = attr ? attr->displayName() : QString(); + return !displayName.isEmpty() ? displayName : QString::fromUtf8(d_ptr->gid); +} + +void Tag::setParent(const Tag &parent) +{ + d_ptr->parent.reset(new Tag(parent)); +} + +Tag Tag::parent() const +{ + if (!d_ptr->parent) { + return Tag(); + } + return *d_ptr->parent; +} + +void Tag::setType(const QByteArray &type) +{ + d_ptr->type = type; +} + +QByteArray Tag::type() const +{ + return d_ptr->type; +} + +bool Tag::isValid() const +{ + return d_ptr->id >= 0; +} + +bool Tag::isImmutable() const +{ + return (d_ptr->type.isEmpty() || d_ptr->type == PLAIN); +} + +QDebug &operator<<(QDebug &debug, const Tag &tag) +{ + debug << "Akonadi::Tag( ID " << tag.id() << ", GID " << tag.gid() << ", parent" << tag.parent().id() << ")"; + return debug; +} + +Tag Tag::genericTag(const QString &name) +{ + Tag tag; + tag.d_ptr->type = GENERIC; + tag.d_ptr->gid = QUuid::createUuid().toByteArray().mid(1, 36); + tag.setName(name); + return tag; +} diff --git a/src/core/tag.h b/src/core/tag.h new file mode 100644 index 0000000..cb41e24 --- /dev/null +++ b/src/core/tag.h @@ -0,0 +1,273 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_TAG_H +#define AKONADI_TAG_H + +#include "akonadicore_export.h" +#include "attribute.h" + +#include +#include +#include +#include + +namespace Akonadi +{ +class TagModifyJob; +class TagPrivate; + +/** + * An Akonadi Tag. + */ +class AKONADICORE_EXPORT Tag +{ +public: + typedef QVector List; + typedef qint64 Id; + + /** + * The PLAIN type has the following properties: + * * gid == displayName + * * immutable + * * no hierarchy (no parent) + * + * PLAIN tags are general purpose tags that are easy to map by backends. + */ + static const char PLAIN[]; + + /** + * The GENERIC type has the following properties: + * * mutable + * * gid is RFC 4122 compatible + * * no hierarchy (no parent) + * + * GENERIC tags are general purpose tags, that are used, if you can change tag name. + */ + static const char GENERIC[]; + + Tag(); + explicit Tag(Id id); + /** + * Creates a PLAIN tag + */ + explicit Tag(const QString &name); + + Tag(const Tag &other); + + ~Tag(); + + Tag &operator=(const Tag &); + //Avoid slicing + bool operator==(const Tag &) const; + bool operator!=(const Tag &) const; + + static Tag fromUrl(const QUrl &url); + + /** + * Adds an attribute to the entity. + * + * If an attribute of the same type name already exists, it is deleted and + * replaced with the new one. + * + * @param attribute The new attribute. + * + * @note The entity takes the ownership of the attribute. + */ + void addAttribute(Attribute *attribute); + + /** + * Removes and deletes the attribute of the given type @p name. + */ + void removeAttribute(const QByteArray &name); + + /** + * Returns @c true if the entity has an attribute of the given type @p name, + * false otherwise. + */ + bool hasAttribute(const QByteArray &name) const; + + /** + * Returns a list of all attributes of the entity. + */ + Attribute::List attributes() const; + + /** + * Removes and deletes all attributes of the entity. + */ + void clearAttributes(); + + /** + * Returns the attribute of the given type @p name if available, 0 otherwise. + */ + Attribute *attribute(const QByteArray &name) const; + + /** + * Describes the options that can be passed to access attributes. + */ + enum CreateOption { + AddIfMissing ///< Creates the attribute if it is missing + }; + + /** + * Returns the attribute of the requested type. + * If the entity has no attribute of that type yet, a new one + * is created and added to the entity. + * + * @param option The create options. + */ + template + inline T *attribute(CreateOption option); + + /** + * Returns the attribute of the requested type or 0 if it is not available. + */ + template + inline T *attribute() const; + + /** + * Removes and deletes the attribute of the requested type. + */ + template + inline void removeAttribute(); + + /** + * Returns whether the entity has an attribute of the requested type. + */ + template + inline bool hasAttribute() const; + + /** + * Returns the url of the tag. + */ + QUrl url() const; + + /** + * Sets the unique @p identifier of the tag. + */ + void setId(Id identifier); + + /** + * Returns the unique identifier of the tag. + */ + Id id() const; + + void setGid(const QByteArray &gid); + QByteArray gid() const; + + void setRemoteId(const QByteArray &remoteId); + QByteArray remoteId() const; + + void setType(const QByteArray &type); + QByteArray type() const; + + void setName(const QString &name); + QString name() const; + + void setParent(const Tag &parent); + Tag parent() const; + + bool isValid() const; + + /** + * Returns true if the tag is immutable (cannot be modified after creation). + * Note that the immutability does not affect the attributes. + */ + bool isImmutable() const; + + /** + * Returns a GENERIC tag with the given name and a valid gid + */ + static Tag genericTag(const QString &name); + +private: + //@cond PRIVATE + friend class TagModifyJob; + + QSharedDataPointer d_ptr; + //@endcond +}; + + +AKONADICORE_EXPORT uint qHash(const Akonadi::Tag &); + + +template +inline T *Tag::attribute(CreateOption option) +{ + Q_UNUSED(option); + + const T dummy; + if (hasAttribute(dummy.type())) { + T *attr = dynamic_cast(attribute(dummy.type())); + if (attr) { + return attr; + } + //reuse 5250 + qWarning() << "Found attribute of unknown type" << dummy.type() + << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } + + T *attr = new T(); + addAttribute(attr); + return attr; +} + +template +inline T *Tag::attribute() const +{ + const T dummy; + if (hasAttribute(dummy.type())) { + T *attr = dynamic_cast(attribute(dummy.type())); + if (attr) { + return attr; + } + //Reuse 5250 + qWarning() << "Found attribute of unknown type" << dummy.type() + << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } + + return 0; +} + +template +inline void Tag::removeAttribute() +{ + const T dummy; + removeAttribute(dummy.type()); +} + +template +inline bool Tag::hasAttribute() const +{ + const T dummy; + return hasAttribute(dummy.type()); +} + + +} // namespace Akonadi + +AKONADICORE_EXPORT QDebug &operator<<(QDebug &debug, const Akonadi::Tag &tag); + +Q_DECLARE_METATYPE(Akonadi::Tag) +Q_DECLARE_METATYPE(Akonadi::Tag::List) +Q_DECLARE_METATYPE(QSet) +Q_DECLARE_TYPEINFO(Akonadi::Tag, Q_MOVABLE_TYPE); + +#endif diff --git a/src/core/tag_p.h b/src/core/tag_p.h new file mode 100644 index 0000000..c4cc29b --- /dev/null +++ b/src/core/tag_p.h @@ -0,0 +1,71 @@ +/* + Copyright (c) 2014 Christian Mollekopf + Copyright (c) 2015 Daniel Vrátil + + 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 TAG_P_H +#define TAG_P_H + +#include "tag.h" + +namespace Akonadi +{ + +class TagPrivate : public QSharedData +{ +public: + TagPrivate() + : id(-1) + { + } + + TagPrivate(const TagPrivate &other) + : QSharedData(other) + { + id = other.id; + gid = other.gid; + remoteId = other.remoteId; + if (other.parent) { + parent.reset(new Tag(*other.parent)); + } + type = other.type; + Q_FOREACH (Attribute *attr, other.mAttributes) { + mAttributes.insert(attr->type(), attr->clone()); + } + mDeletedAttributes = other.mDeletedAttributes; + } + + ~TagPrivate() + { + qDeleteAll(mAttributes); + } + + // 4 bytes padding here (after QSharedData) + + Tag::Id id; + QByteArray gid; + QByteArray remoteId; + QScopedPointer parent; + QByteArray type; + QHash mAttributes; + QSet mDeletedAttributes; +}; + +} + +#endif diff --git a/src/core/tagattribute.cpp b/src/core/tagattribute.cpp new file mode 100644 index 0000000..f1a6547 --- /dev/null +++ b/src/core/tagattribute.cpp @@ -0,0 +1,226 @@ +/* + 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 "tagattribute.h" + +#include "private/imapparser_p.h" + +using namespace Akonadi; + +class Q_DECL_HIDDEN TagAttribute::Private +{ +public: + Private() + : inToolbar(false) + , priority(-1) + { + } + + QString name; + QString icon; + QColor backgroundColor; + QColor textColor; + QString font; + bool inToolbar; + QString shortcut; + int priority; +}; + +TagAttribute::TagAttribute() + : d(new Private) +{ +} + +TagAttribute::~TagAttribute() +{ + delete d; +} + +QString TagAttribute::displayName() const +{ + return d->name; +} + +void TagAttribute::setDisplayName(const QString &name) +{ + d->name = name; +} + +QString TagAttribute::iconName() const +{ + return d->icon; +} + +void TagAttribute::setIconName(const QString &icon) +{ + d->icon = icon; +} + +QByteArray Akonadi::TagAttribute::type() const +{ + static const QByteArray sType("TAG"); + return sType; +} + +TagAttribute *TagAttribute::clone() const +{ + TagAttribute *attr = new TagAttribute(); + attr->d->name = d->name; + attr->d->icon = d->icon; + attr->d->backgroundColor = d->backgroundColor; + attr->d->textColor = d->textColor; + attr->d->font = d->font; + attr->d->inToolbar = d->inToolbar; + attr->d->shortcut = d->shortcut; + attr->d->priority = d->priority; + return attr; +} + +QByteArray TagAttribute::serialized() const +{ + QList l; + l << ImapParser::quote(d->name.toUtf8()); + l << ImapParser::quote(d->icon.toUtf8()); + l << ImapParser::quote(d->font.toUtf8()); + l << ImapParser::quote(d->shortcut.toUtf8()); + l << ImapParser::quote(QString::number(d->inToolbar).toUtf8()); + { + QList components; + if (d->backgroundColor.isValid()) { + components = QList() << QByteArray::number(d->backgroundColor.red()) + << QByteArray::number(d->backgroundColor.green()) + << QByteArray::number(d->backgroundColor.blue()) + << QByteArray::number(d->backgroundColor.alpha()); + } + l << '(' + ImapParser::join(components, " ") + ')'; + } + { + QList components; + if (d->textColor.isValid()) { + components = QList() << QByteArray::number(d->textColor.red()) + << QByteArray::number(d->textColor.green()) + << QByteArray::number(d->textColor.blue()) + << QByteArray::number(d->textColor.alpha()); + } + l << '(' + ImapParser::join(components, " ") + ')'; + } + l << ImapParser::quote(QString::number(d->priority).toUtf8()); + return '(' + ImapParser::join(l, " ") + ')'; +} + +static QColor parseColor(const QByteArray &data) +{ + QList componentData; + ImapParser::parseParenthesizedList(data, componentData); + if (componentData.size() != 4) { + return QColor(); + } + QList components; + components.reserve(4); + bool ok; + for (int i = 0; i <= 3; ++i) { + components << componentData.at(i).toInt(&ok); + if (!ok) { + return QColor(); + } + } + return QColor(components.at(0), components.at(1), components.at(2), components.at(3)); +} + +void TagAttribute::deserialize(const QByteArray &data) +{ + QList l; + ImapParser::parseParenthesizedList(data, l); + int size = l.size(); + Q_ASSERT(size >= 7); + d->name = QString::fromUtf8(l[0]); + d->icon = QString::fromUtf8(l[1]); + d->font = QString::fromUtf8(l[2]); + d->shortcut = QString::fromUtf8(l[3]); + d->inToolbar = QString::fromUtf8(l[4]).toInt(); + if (!l[5].isEmpty()) { + d->backgroundColor = parseColor(l[5]); + } + if (!l[6].isEmpty()) { + d->textColor = parseColor(l[6]); + } + if (l.size() >= 8) { + d->priority = QString::fromUtf8(l[7]).toInt(); + } +} + +QColor TagAttribute::backgroundColor() const +{ + return d->backgroundColor; +} + +void TagAttribute::setBackgroundColor(const QColor &color) +{ + d->backgroundColor = color; +} + +void TagAttribute::setTextColor(const QColor &color) +{ + d->textColor = color; +} + +QColor TagAttribute::textColor() const +{ + return d->textColor; +} + +void TagAttribute::setFont(const QString &font) +{ + d->font = font; +} + +QString TagAttribute::font() const +{ + return d->font; +} + +void TagAttribute::setInToolbar(bool inToolbar) +{ + d->inToolbar = inToolbar; +} + +bool TagAttribute::inToolbar() const +{ + return d->inToolbar; +} + +void TagAttribute::setShortcut(const QString &shortcut) +{ + d->shortcut = shortcut; +} + +QString TagAttribute::shortcut() const +{ + return d->shortcut; +} + +void TagAttribute::setPriority(int priority) +{ + d->priority = priority; +} + +int TagAttribute::priority() const +{ + return d->priority; +} diff --git a/src/core/tagattribute.h b/src/core/tagattribute.h new file mode 100644 index 0000000..bc7c848 --- /dev/null +++ b/src/core/tagattribute.h @@ -0,0 +1,107 @@ +/* + Copyright (c) 2008 Volker Krause + Copyright (c) 2014 Christian Mollekopf + + 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_TAGATTRIBUTE_H +#define AKONADI_TAGATTRIBUTE_H + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +namespace Akonadi +{ + +/** + * @short Attribute that stores the properties that are used to display a tag. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT TagAttribute : public Attribute +{ +public: + TagAttribute(); + + ~TagAttribute(); + + /** + * Sets the @p name that should be used for display. + */ + void setDisplayName(const QString &name); + + /** + * Returns the name that should be used for display. + * Users of this should fall back to Collection::name() if this is empty. + */ + QString displayName() const; + + /** + * Sets the icon @p name for the default icon. + */ + void setIconName(const QString &name); + + /** + * Returns the icon name of the icon returned by icon(). + */ + QString iconName() const; + + void setBackgroundColor(const QColor &color); + QColor backgroundColor() const; + void setTextColor(const QColor &color); + QColor textColor() const; + void setFont(const QString &fontKey); + QString font() const; + void setInToolbar(bool inToolbar); + bool inToolbar() const; + void setShortcut(const QString &shortcut); + QString shortcut() const; + + /** + * Sets the priority of the tag. + * The priority is primarily used for presentation, e.g. for sorting. + * If only one tag can be displayed for a given item, the one with the highest + * priority should be shown. + */ + void setPriority(int priority); + + /** + * Returns the priority of the tag. + * The default value is -1 + */ + int priority() const; + + /* reimpl */ + QByteArray type() const Q_DECL_OVERRIDE; + TagAttribute *clone() const Q_DECL_OVERRIDE; + QByteArray serialized() const Q_DECL_OVERRIDE; + void deserialize(const QByteArray &data) Q_DECL_OVERRIDE; + +private: + TagAttribute(const TagAttribute &other); + TagAttribute &operator=(const TagAttribute &other); + //@cond PRIVATE + class Private; + Private *const d; + //@endcond +}; + +} + +#endif diff --git a/src/core/tagfetchscope.cpp b/src/core/tagfetchscope.cpp new file mode 100644 index 0000000..dc1ffd0 --- /dev/null +++ b/src/core/tagfetchscope.cpp @@ -0,0 +1,81 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "tagfetchscope.h" +#include + +using namespace Akonadi; + +struct Q_DECL_HIDDEN Akonadi::TagFetchScope::Private { + Private() + : mFetchIdOnly(false) + { + } + + QSet mAttributes; + bool mFetchIdOnly; +}; + +TagFetchScope::TagFetchScope() + : d(new Private) +{ + +} + +TagFetchScope::~TagFetchScope() +{ +} + +TagFetchScope::TagFetchScope(const TagFetchScope &other) + : d(new Private) +{ + operator=(other); +} + +TagFetchScope &TagFetchScope::operator=(const TagFetchScope &other) +{ + d->mAttributes = other.d->mAttributes; + d->mFetchIdOnly = other.d->mFetchIdOnly; + return *this; +} + +QSet TagFetchScope::attributes() const +{ + return d->mAttributes; +} + +void TagFetchScope::fetchAttribute(const QByteArray &type, bool fetch) +{ + if (fetch) { + d->mAttributes.insert(type); + } else { + d->mAttributes.remove(type); + } +} + +void TagFetchScope::setFetchIdOnly(bool idOnly) +{ + d->mFetchIdOnly = idOnly; + d->mAttributes.clear(); +} + +bool TagFetchScope::fetchIdOnly() const +{ + return d->mFetchIdOnly; +} diff --git a/src/core/tagfetchscope.h b/src/core/tagfetchscope.h new file mode 100644 index 0000000..33c1f42 --- /dev/null +++ b/src/core/tagfetchscope.h @@ -0,0 +1,118 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 TAGFETCHSCOPE_H +#define TAGFETCHSCOPE_H + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ + +/** + * @short Specifies which parts of a tag should be fetched from the Akonadi storage. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT TagFetchScope +{ +public: + + /** + * Creates an empty tag fetch scope. + * + * Using an empty scope will only fetch the very basic meta data of tags, + * e.g. local id, remote id and mime type + */ + TagFetchScope(); + + /** + * Creates a new tag fetch scope from an @p other. + */ + TagFetchScope(const TagFetchScope &other); + + /** + * Destroys the tag fetch scope. + */ + ~TagFetchScope(); + + /** + * Assigns the @p other to this scope and returns a reference to this scope. + */ + TagFetchScope &operator=(const TagFetchScope &other); + + /** + * Returns all explicitly fetched attributes. + * + * Undefined if fetchAllAttributes() returns true. + * + * @see fetchAttribute() + */ + QSet attributes() const; + + /** + * Sets whether the attribute of the given @p type should be fetched. + * + * @param type The attribute type to fetch. + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + void fetchAttribute(const QByteArray &type, bool fetch = true); + + /** + * Sets whether the attribute of the requested type should be fetched. + * + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + template inline void fetchAttribute(bool fetch = true) + { + T dummy; + fetchAttribute(dummy.type(), fetch); + } + + /** + * Sets whether only the id or the complete tag should be fetched. + * + * The default is @c false. + * + * @since 4.15 + */ + void setFetchIdOnly(bool fetchIdOnly); + + /** + * Sets whether only the id of the tags should be retieved or the complete tag. + * + * @see tagFetchScope() + * @since 4.15 + */ + bool fetchIdOnly() const; + +private: + class Private; + //@cond PRIVATE + QSharedPointer d; + //@endcond +}; + +} + +// Q_DECLARE_METATYPE(Akonadi::TagFetchScope) + +#endif diff --git a/src/core/tagsync.cpp b/src/core/tagsync.cpp new file mode 100644 index 0000000..79f1c6a --- /dev/null +++ b/src/core/tagsync.cpp @@ -0,0 +1,254 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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. +*/ +namespace Akonadi +{ +class Item; +} + +#include "tagsync.h" +#include "akonadicore_debug.h" +#include "jobs/itemfetchjob.h" +#include "itemfetchscope.h" +#include "jobs/itemmodifyjob.h" +#include "jobs/tagfetchjob.h" +#include "jobs/tagcreatejob.h" +#include "jobs/tagmodifyjob.h" + +using namespace Akonadi; + +bool operator==(const Item &left, const Item &right) +{ + if (left.isValid() && right.isValid() && (left.id() == right.id())) { + return true; + } + if (!left.remoteId().isEmpty() && !right.remoteId().isEmpty() && (left.remoteId() == right.remoteId())) { + return true; + } + if (!left.gid().isEmpty() && !right.gid().isEmpty() && (left.gid() == right.gid())) { + return true; + } + return false; +} + +TagSync::TagSync(QObject *parent) + : Job(parent), + mDeliveryDone(false), + mTagMembersDeliveryDone(false), + mLocalTagsFetched(false) +{ + +} + +TagSync::~TagSync() +{ + +} + +void TagSync::setFullTagList(const Akonadi::Tag::List &tags) +{ + mRemoteTags = tags; + mDeliveryDone = true; + diffTags(); +} + +void TagSync::setTagMembers(const QHash &ridMemberMap) +{ + mRidMemberMap = ridMemberMap; + mTagMembersDeliveryDone = true; + diffTags(); +} + +void TagSync::doStart() +{ + // qCDebug(AKONADICORE_LOG); + //This should include all tags, including the ones that don't have a remote id + Akonadi::TagFetchJob *fetch = new Akonadi::TagFetchJob(this); + connect(fetch, &KJob::result, this, &TagSync::onLocalTagFetchDone); +} + +void TagSync::onLocalTagFetchDone(KJob *job) +{ + // qCDebug(AKONADICORE_LOG); + TagFetchJob *fetch = static_cast(job); + mLocalTags = fetch->tags(); + mLocalTagsFetched = true; + diffTags(); +} + +void TagSync::diffTags() +{ + if (!mDeliveryDone || !mTagMembersDeliveryDone || !mLocalTagsFetched) { + qCDebug(AKONADICORE_LOG) << "waiting for delivery: " << mDeliveryDone << mLocalTagsFetched; + return; + } + // qCDebug(AKONADICORE_LOG) << "diffing"; + QHash tagByGid; + QHash tagByRid; + QHash tagById; + Q_FOREACH (const Akonadi::Tag &localTag, mLocalTags) { + tagByRid.insert(localTag.remoteId(), localTag); + tagByGid.insert(localTag.gid(), localTag); + if (!localTag.remoteId().isEmpty()) { + tagById.insert(localTag.id(), localTag); + } + } + Q_FOREACH (const Akonadi::Tag &remoteTag, mRemoteTags) { + if (tagByRid.contains(remoteTag.remoteId())) { + //Tag still exists, check members + Tag tag = tagByRid.value(remoteTag.remoteId()); + ItemFetchJob *itemFetch = new ItemFetchJob(tag, this); + itemFetch->setProperty("tag", QVariant::fromValue(tag)); + itemFetch->setProperty("merge", false); + itemFetch->fetchScope().setFetchGid(true); + connect(itemFetch, &KJob::result, this, &TagSync::onTagItemsFetchDone); + connect(itemFetch, &KJob::result, this, &TagSync::onJobDone); + tagById.remove(tagByRid.value(remoteTag.remoteId()).id()); + } else if (tagByGid.contains(remoteTag.gid())) { + //Tag exists but has no rid + //Merge members and set rid + Tag tag = tagByGid.value(remoteTag.gid()); + tag.setRemoteId(remoteTag.remoteId()); + ItemFetchJob *itemFetch = new ItemFetchJob(tag, this); + itemFetch->setProperty("tag", QVariant::fromValue(tag)); + itemFetch->setProperty("merge", true); + itemFetch->fetchScope().setFetchGid(true); + connect(itemFetch, &KJob::result, this, &TagSync::onTagItemsFetchDone); + connect(itemFetch, &KJob::result, this, &TagSync::onJobDone); + tagById.remove(tagByGid.value(remoteTag.gid()).id()); + } else { + //New tag, create + TagCreateJob *createJob = new TagCreateJob(remoteTag, this); + createJob->setMergeIfExisting(true); + connect(createJob, &KJob::result, this, &TagSync::onCreateTagDone); + connect(createJob, &KJob::result, this, &TagSync::onJobDone); + } + } + Q_FOREACH (const Tag &tag, tagById) { + //Removed remotely, unset rid + Tag copy(tag); + copy.setRemoteId(QByteArray("")); + TagModifyJob *modJob = new TagModifyJob(copy, this); + connect(modJob, &KJob::result, this, &TagSync::onJobDone); + } + checkDone(); +} + +void TagSync::onCreateTagDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "ItemFetch failed: " << job->errorString(); + return; + } + + Akonadi::Tag tag = static_cast(job)->tag(); + const Item::List remoteMembers = mRidMemberMap.value(QString::fromLatin1(tag.remoteId())); + Q_FOREACH (Item item, remoteMembers) { + item.setTag(tag); + ItemModifyJob *modJob = new ItemModifyJob(item, this); + connect(modJob, &KJob::result, this, &TagSync::onJobDone); + qCDebug(AKONADICORE_LOG) << "setting tag " << item.remoteId(); + } +} + +static bool containsByGidOrRid(const Item::List &items, const Item &key) +{ + Q_FOREACH (const Item &item, items) { + if ((!item.gid().isEmpty() && !key.gid().isEmpty()) && (item.gid() == key.gid())) { + return true; + } else if (item.remoteId() == key.remoteId()) { + return true; + } + } + return false; +} + +void TagSync::onTagItemsFetchDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "ItemFetch failed: " << job->errorString(); + return; + } + + const Akonadi::Item::List items = static_cast(job)->items(); + const Akonadi::Tag tag = job->property("tag").value(); + const bool merge = job->property("merge").toBool(); + const Item::List remoteMembers = mRidMemberMap.value(QString::fromLatin1(tag.remoteId())); + + //add = remote - local + Item::List toAdd; + Q_FOREACH (const Item &remote, remoteMembers) { + if (!containsByGidOrRid(items, remote)) { + toAdd << remote; + } + } + + //remove = local - remote + Item::List toRemove; + Q_FOREACH (const Item &local, items) { + //Skip items that have no remote id yet + //Trying to them will only result in a conflict + if (local.remoteId().isEmpty()) { + continue; + } + if (!containsByGidOrRid(remoteMembers, local)) { + toRemove << local; + } + } + + if (!merge) { + Q_FOREACH (Item item, toRemove) { + item.clearTag(tag); + ItemModifyJob *modJob = new ItemModifyJob(item, this); + connect(modJob, &KJob::result, this, &TagSync::onJobDone); + qCDebug(AKONADICORE_LOG) << "removing tag " << item.remoteId(); + } + } + Q_FOREACH (Item item, toAdd) { + item.setTag(tag); + ItemModifyJob *modJob = new ItemModifyJob(item, this); + connect(modJob, &KJob::result, this, &TagSync::onJobDone); + qCDebug(AKONADICORE_LOG) << "setting tag " << item.remoteId(); + } +} + +void TagSync::onJobDone(KJob *) +{ + checkDone(); +} + +void TagSync::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error during TagSync: " << job->errorString() << job->metaObject()->className(); + // pretent there were no errors + Akonadi::Job::removeSubjob(job); + } else { + Akonadi::Job::slotResult(job); + } +} + +void TagSync::checkDone() +{ + if (hasSubjobs()) { + return; + } + qCDebug(AKONADICORE_LOG) << "done"; + emitResult(); +} + diff --git a/src/core/tagsync.h b/src/core/tagsync.h new file mode 100644 index 0000000..85af963 --- /dev/null +++ b/src/core/tagsync.h @@ -0,0 +1,65 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 TAGSYNC_H +#define TAGSYNC_H + +#include "akonadicore_export.h" + +#include "jobs/job.h" +#include "tag.h" +#include "item.h" + +namespace Akonadi { + +class AKONADICORE_EXPORT TagSync : public Akonadi::Job +{ + Q_OBJECT +public: + TagSync(QObject *parent = 0); + virtual ~TagSync(); + + void setFullTagList(const Akonadi::Tag::List &tags); + void setTagMembers(const QHash &ridMemberMap); + +protected: + void doStart() Q_DECL_OVERRIDE; + +private Q_SLOTS: + void onLocalTagFetchDone(KJob *job); + void onCreateTagDone(KJob *job); + void onTagItemsFetchDone(KJob *job); + void onJobDone(KJob *job); + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +private: + void diffTags(); + void checkDone(); + +private: + Akonadi::Tag::List mRemoteTags; + Akonadi::Tag::List mLocalTags; + bool mDeliveryDone; + bool mTagMembersDeliveryDone; + bool mLocalTagsFetched; + QHash mRidMemberMap; +}; + +} + +#endif diff --git a/src/core/trashsettings.cpp b/src/core/trashsettings.cpp new file mode 100644 index 0000000..0c80d6b --- /dev/null +++ b/src/core/trashsettings.cpp @@ -0,0 +1,46 @@ +/* + Copyright (c) 2011 Christian Mollekopf + + 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 "trashsettings.h" +#include "akonadicore_debug.h" + +#include +#include + +#include +#include + +using namespace Akonadi; + +Akonadi::Collection TrashSettings::getTrashCollection(const QString &resource) +{ + KConfig config(QStringLiteral("akonaditrashrc")); + KConfigGroup group(&config, resource); + const Akonadi::Collection::Id colId = group.readEntry ("TrashCollection", -1); + qCWarning(AKONADICORE_LOG) << resource << colId; + return Collection(colId); +} + +void TrashSettings::setTrashCollection(const QString &resource, const Akonadi::Collection &collection) +{ + KConfig config(QStringLiteral("akonaditrashrc")); + KConfigGroup group(&config, resource); + qCWarning(AKONADICORE_LOG) << resource << collection.id(); + group.writeEntry("TrashCollection", collection.id()); +} diff --git a/src/core/trashsettings.h b/src/core/trashsettings.h new file mode 100644 index 0000000..41b91e7 --- /dev/null +++ b/src/core/trashsettings.h @@ -0,0 +1,54 @@ +/* + Copyright (c) 2011 Christian Mollekopf + + 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_TRASHSETTINGS_H +#define AKONADI_TRASHSETTINGS_H + +#include "akonadicore_export.h" +#include "collection.h" + +class QString; + +namespace Akonadi +{ + +/** + * @short Global Trash-related Settings + * + * All settings concerning the trashhandling should go here. + * + * @author Christian Mollekopf + * @since 4.8 + */ +//TODO setting for time before items are deleted by janitor agent +namespace TrashSettings +{ +/** + * Set the trash collection for the given @p resource which is then used by the TrashJob + */ +AKONADICORE_EXPORT void setTrashCollection(const QString &resource, const Collection &collection); +/** + * Get the trash collection for the given @p resource + */ +AKONADICORE_EXPORT Collection getTrashCollection(const QString &resource); +} + +} + +#endif diff --git a/src/core/typepluginloader.cpp b/src/core/typepluginloader.cpp new file mode 100644 index 0000000..b90b23c --- /dev/null +++ b/src/core/typepluginloader.cpp @@ -0,0 +1,446 @@ +/* + 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 "typepluginloader_p.h" + +#include "item.h" +#include "itemserializer_p.h" +#include "itemserializerplugin.h" + +#include "akonadicore_debug.h" + +// Qt +#include +#include +#include +#include +#include +#include + +#include +#include + +// temporary +#include "pluginloader_p.h" + +#include +#include + +static const char LEGACY_NAME[] = "legacy"; +static const char DEFAULT_NAME[] = "default"; +static const char _APPLICATION_OCTETSTREAM[] = "application/octet-stream"; + +namespace Akonadi +{ + +Q_GLOBAL_STATIC(DefaultItemSerializerPlugin, s_defaultItemSerializerPlugin) + +class PluginEntry +{ +public: + PluginEntry() + : mPlugin(0) + { + } + + explicit PluginEntry(const QString &identifier, QObject *plugin = 0) + : mIdentifier(identifier) + , mPlugin(plugin) + { + qCDebug(AKONADICORE_LOG) << " PLUGIN : identifier" << identifier; + } + + QObject *plugin() const + { + if (mPlugin) { + return mPlugin; + } + + QObject *object = PluginLoader::self()->createForName(mIdentifier); + if (!object) { + qCWarning(AKONADICORE_LOG) << "ItemSerializerPluginLoader: " + << "plugin" << mIdentifier << "is not valid!" << endl; + + // we try to use the default in that case + mPlugin = s_defaultItemSerializerPlugin; + } + + mPlugin = object; + if (!qobject_cast(mPlugin)) { + qCWarning(AKONADICORE_LOG) << "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; + } + + const char *pluginClassName() const + { + return plugin()->metaObject()->className(); + } + + QString identifier() const + { + return mIdentifier; + } + + bool operator<(const PluginEntry &other) const + { + return mIdentifier < other.mIdentifier; + } + + bool operator<(const QString &identifier) const + { + return mIdentifier < identifier; + } + +private: + QString mIdentifier; + mutable QObject *mPlugin; +}; + +class MimeTypeEntry +{ +public: + explicit MimeTypeEntry(const QString &mimeType) + : m_mimeType(mimeType) + , m_plugins() + , m_pluginsByMetaTypeId() + { + } + + QString type() const + { + return m_mimeType; + } + + void add(const QByteArray &class_, const PluginEntry &entry) + { + m_pluginsByMetaTypeId.clear(); // iterators will be invalidated by next line + m_plugins.insert(class_, entry); + } + + const PluginEntry *plugin(const QByteArray &class_) const + { + const QHash::const_iterator it = m_plugins.find(class_); + return it == m_plugins.end() ? 0 : it.operator->(); + } + + const PluginEntry *defaultPlugin() const + { + // 1. If there's an explicit default plugin, use that one: + if (const PluginEntry *pe = plugin(DEFAULT_NAME)) { + return pe; + } + + // 2. Otherwise, look through the already instantiated plugins, + // and return one of them (preferably not the legacy one): + bool sawZero = false; + for (QMap::const_iterator>::const_iterator it = m_pluginsByMetaTypeId.constBegin(), end = m_pluginsByMetaTypeId.constEnd(); it != end; ++it) { + if (it.key() == 0) { + sawZero = true; + } else if (*it != m_plugins.end()) { + return it->operator->(); + } + } + + // 3. Otherwise, look through the whole list (again, preferably not the legacy one): + for (QHash::const_iterator it = m_plugins.constBegin(), end = m_plugins.constEnd(); it != end; ++it) { + if (it.key() == LEGACY_NAME) { + sawZero = true; + } else { + return it.operator->(); + } + } + + // 4. take the legacy one: + if (sawZero) { + return plugin(0); + } + return 0; + } + + const PluginEntry *plugin(int metaTypeId) const + { + const QMap::const_iterator> &c_pluginsByMetaTypeId = m_pluginsByMetaTypeId; + QMap::const_iterator>::const_iterator it = c_pluginsByMetaTypeId.find(metaTypeId); + if (it == c_pluginsByMetaTypeId.end()) { + it = QMap::const_iterator>::const_iterator(m_pluginsByMetaTypeId.insert(metaTypeId, m_plugins.find(metaTypeId ? QMetaType::typeName(metaTypeId) : LEGACY_NAME))); + } + return *it == m_plugins.end() ? 0 : it->operator->(); + } + + const PluginEntry *plugin(const QVector &metaTypeIds, int &chosen) const + { + bool sawZero = false; + for (QVector::const_iterator it = metaTypeIds.begin(), end = metaTypeIds.end(); it != end; ++it) { + if (*it == 0) { + sawZero = true; // skip the legacy type and see if we can find something else first + } else if (const PluginEntry *const entry = plugin(*it)) { + chosen = *it; + return entry; + } + } + if (sawZero) { + chosen = 0; + return plugin(0); + } + return 0; + } + +private: + QString m_mimeType; + QHash m_plugins; + mutable QMap::const_iterator> m_pluginsByMetaTypeId; +}; + +class PluginRegistry +{ +public: + PluginRegistry() + : mDefaultPlugin(PluginEntry(QStringLiteral("application/octet-stream@QByteArray"), s_defaultItemSerializerPlugin)) + , mOverridePlugin(0) + { + const PluginLoader *pl = PluginLoader::self(); + if (!pl) { + qCWarning(AKONADICORE_LOG) << "Cannot instantiate plugin loader!" << endl; + return; + } + const QStringList names = pl->names(); + qCDebug(AKONADICORE_LOG) << "ItemSerializerPluginLoader: " + << "found" << names.size() << "plugins." << endl; + QMap map; + QRegExp rx(QStringLiteral("(.+)@(.+)")); + QMimeDatabase mimeDb; + Q_FOREACH (const QString &name, names) { + if (rx.exactMatch(name)) { + const QMimeType mime = mimeDb.mimeTypeForName(rx.cap(1)); + if (mime.isValid()) { + const QString mimeType = mime.name(); + const QByteArray classType = rx.cap(2).toLatin1(); + QMap::iterator it = map.find(mimeType); + if (it == map.end()) { + it = map.insert(mimeType, MimeTypeEntry(mimeType)); + } + it->add(classType, PluginEntry(name)); + } + } else { + qCDebug(AKONADICORE_LOG) << "ItemSerializerPluginLoader: " + << "name" << name << "doesn't look like mimetype@classtype" << endl; + } + } + const QString APPLICATION_OCTETSTREAM = QLatin1String(_APPLICATION_OCTETSTREAM); + QMap::iterator it = map.find(APPLICATION_OCTETSTREAM); + if (it == map.end()) { + it = map.insert(APPLICATION_OCTETSTREAM, MimeTypeEntry(APPLICATION_OCTETSTREAM)); + } + it->add("QByteArray", mDefaultPlugin); + it->add(LEGACY_NAME, mDefaultPlugin); + const int size = map.size(); + allMimeTypes.reserve(size); + std::copy(map.begin(), map.end(), std::back_inserter(allMimeTypes)); + } + + QObject *findBestMatch(const QString &type, const QVector &metaTypeId, TypePluginLoader::Options opt) + { + if (QObject *const plugin = findBestMatch(type, metaTypeId)) { + { + if ((opt & TypePluginLoader::NoDefault) && plugin == mDefaultPlugin.plugin()) { + return 0; + } + return plugin; + } + } + return 0; + } + + QObject *findBestMatch(const QString &type, const QVector &metaTypeIds) + { + if (mOverridePlugin) { + return mOverridePlugin; + } + if (QObject *const plugin = cacheLookup(type, metaTypeIds)) { + // plugin cached, so let's take that one + return plugin; + } + int chosen = -1; + QObject *const plugin = findBestMatchImpl(type, metaTypeIds, chosen); + if (metaTypeIds.empty()) { + if (plugin) { + cachedDefaultPlugins[type] = plugin; + } + } + if (chosen >= 0) { + cachedPlugins[type][chosen] = plugin; + } + return plugin; + } + + void overrideDefaultPlugin(QObject *p) + { + mOverridePlugin = p; + } + +private: + QObject *findBestMatchImpl(const QString &type, const QVector &metaTypeIds, int &chosen) const + { + const QMimeDatabase mimeDb; + const QMimeType mimeType = mimeDb.mimeTypeForName(type); + if (!mimeType.isValid()) { + return mDefaultPlugin.plugin(); + } + + // step 1: find all plugins that match at all + QVector matchingIndexes; + for (int i = 0, end = allMimeTypes.size(); i < end; ++i) { + if (mimeType.inherits(allMimeTypes[i].type())) { + matchingIndexes.append(i); + } + } + + // step 2: if we have more than one match, find the most specific one using topological sort + QVector order; + if (matchingIndexes.size() <= 1) { + order.push_back(0); + } else { + boost::adjacency_list<> graph(matchingIndexes.size()); + for (int i = 0, end = matchingIndexes.size(); i != end; ++i) { + const QMimeType mimeType = mimeDb.mimeTypeForName(allMimeTypes[matchingIndexes[i]].type()); + if (!mimeType.isValid()) { + continue; + } + for (int j = 0; j != end; ++j) { + if (i != j && mimeType.inherits(allMimeTypes[matchingIndexes[j]].type())) { + boost::add_edge(j, i, graph); + } + } + } + + order.reserve(matchingIndexes.size()); + try { + boost::topological_sort(graph, std::back_inserter(order)); + } catch (const boost::not_a_dag &e) { + qCWarning(AKONADICORE_LOG) << "Mimetype tree is not a DAG!"; + return mDefaultPlugin.plugin(); + } + } + + // step 3: ask each one in turn if it can handle any of the metaTypeIds: +// qCDebug(AKONADICORE_LOG) << "Looking for " << format( type, metaTypeIds ); + for (QVector::const_iterator it = order.constBegin(), end = order.constEnd(); it != end; ++it) { +// qCDebug(AKONADICORE_LOG) << " Considering serializer plugin for type" << allMimeTypes[matchingIndexes[*it]].type() +// // << "as the closest match"; + const MimeTypeEntry &mt = allMimeTypes[matchingIndexes[*it]]; + if (metaTypeIds.empty()) { + if (const PluginEntry *const entry = mt.defaultPlugin()) { +// qCDebug(AKONADICORE_LOG) << " -> got " << entry->pluginClassName() << " and am happy with it."; + //FIXME ? in qt5 we show "application/octet-stream" first so if will use default plugin. Exclude it until we look at all mimetype and use default at the end if necessary + if (allMimeTypes[matchingIndexes[*it]].type() != QLatin1String("application/octet-stream")) { + return entry->plugin(); + } + } else { +// qCDebug(AKONADICORE_LOG) << " -> no default plugin for this mime type, trying next"; + } + } else if (const PluginEntry *const entry = mt.plugin(metaTypeIds, chosen)) { +// qCDebug(AKONADICORE_LOG) << " -> got " << entry->pluginClassName() << " and am happy with it."; + return entry->plugin(); + } else { +// qCDebug(AKONADICORE_LOG) << " -> can't handle any of the types, trying next"; + } + } + +// qCDebug(AKONADICORE_LOG) << " No further candidates, using default plugin"; + // no luck? Use the default plugin + return mDefaultPlugin.plugin(); + } + + std::vector allMimeTypes; + QHash > cachedPlugins; + QHash cachedDefaultPlugins; + + // ### cache NULLs, too + QObject *cacheLookup(const QString &mimeType, const QVector &metaTypeIds) const + { + if (metaTypeIds.empty()) { + const QHash::const_iterator hit = cachedDefaultPlugins.find(mimeType); + if (hit != cachedDefaultPlugins.end()) { + return *hit; + } + } + + const QHash >::const_iterator hit = cachedPlugins.find(mimeType); + if (hit == cachedPlugins.end()) { + return 0; + } + bool sawZero = false; + for (QVector::const_iterator it = metaTypeIds.begin(), end = metaTypeIds.end(); it != end; ++it) { + if (*it == 0) { + sawZero = true; // skip the legacy type and see if we can find something else first + } else if (QObject *const o = hit->value(*it)) { + return o; + } + } + if (sawZero) { + return hit->value(0); + } + return 0; + } + +private: + PluginEntry mDefaultPlugin; + QObject *mOverridePlugin; +}; + +Q_GLOBAL_STATIC(PluginRegistry, s_pluginRegistry) + +QObject *TypePluginLoader::objectForMimeTypeAndClass(const QString &mimetype, const QVector &metaTypeIds, Options opt) +{ + return s_pluginRegistry->findBestMatch(mimetype, metaTypeIds, opt); +} + +QObject *TypePluginLoader::defaultObjectForMimeType(const QString &mimetype) +{ + return objectForMimeTypeAndClass(mimetype, QVector()); +} + +ItemSerializerPlugin *TypePluginLoader::pluginForMimeTypeAndClass(const QString &mimetype, const QVector &metaTypeIds, Options opt) +{ + return qobject_cast(objectForMimeTypeAndClass(mimetype, metaTypeIds, opt)); +} + +ItemSerializerPlugin *TypePluginLoader::defaultPluginForMimeType(const QString &mimetype) +{ + ItemSerializerPlugin *plugin = qobject_cast(defaultObjectForMimeType(mimetype)); + Q_ASSERT(plugin); + return plugin; +} + +void TypePluginLoader::overridePluginLookup(QObject *p) +{ + s_pluginRegistry->overrideDefaultPlugin(p); +} + +} diff --git a/src/core/typepluginloader_p.h b/src/core/typepluginloader_p.h new file mode 100644 index 0000000..c16acf8 --- /dev/null +++ b/src/core/typepluginloader_p.h @@ -0,0 +1,99 @@ +/* + 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_TYPEPLUGINLOADER_P_H +#define AKONADI_TYPEPLUGINLOADER_P_H + +#include +#include "akonadicore_export.h" + +class QObject; +class QString; +template +class QVector; + +namespace Akonadi +{ +class ItemSerializerPlugin; + +/** + * @internal + * + * With KDE 4.6 we are on the way to change the ItemSerializer plugins into general TypePlugins + * which provide several type specific actions, namely: + * @li Serializing/Deserializing of payload + * @li Comparing two payloads and reporting the differences + * + * To share the code of loading the plugins and finding the right plugin for a given mime type + * the old code from ItemSerializer has been extracted into the pluginForMimeType() method + * inside the TypePluginLoader namespace. + */ +namespace TypePluginLoader +{ + +enum Option { + NoOptions, + NoDefault = 1, + + _LastOption, + OptionMask = 2 * _LastOption - 1 +}; +Q_DECLARE_FLAGS(Options, Option) + +/** + * Returns the default item serializer plugin that matches the given @p mimetype. + */ +AKONADICORE_EXPORT ItemSerializerPlugin *defaultPluginForMimeType(const QString &mimetype); + +/** + * Returns the item serializer plugin that matches the given + * @p mimetype, and any of the classes described by @p metaTypeIds. + */ +AKONADICORE_EXPORT ItemSerializerPlugin *pluginForMimeTypeAndClass(const QString &mimetype, const QVector &metaTypeIds, Options options = NoOptions); + +/** + * Returns the default type plugin object that matches the given @p mimetype. + */ +AKONADICORE_EXPORT QObject *defaultObjectForMimeType(const QString &mimetype); + +/** + * Returns the type plugin object that matches the given @p mimetype, + * and any of the classes described by @p metaTypeIds. + */ +AKONADICORE_EXPORT QObject *objectForMimeTypeAndClass(const QString &mimetype, const QVector &metaTypeIds, Options options = NoOptions); + +/** + * Override the plugin-lookup with @p plugin. + * + * After calling this each lookup will always return @p plugin. + * This is useful to inject a special plugin for testing purposes. + * To reset the plugin, set to 0. + * + * @since 4.12 + */ +AKONADICORE_EXPORT void overridePluginLookup(QObject *plugin); + +} + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(Akonadi::TypePluginLoader::Options) + +#endif diff --git a/src/core/vectorhelper.h b/src/core/vectorhelper.h new file mode 100644 index 0000000..bd05af6 --- /dev/null +++ b/src/core/vectorhelper.h @@ -0,0 +1,51 @@ +/* + Copyright (c) 2015 Laurent Montel + + 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 VECTORHELPER_H +#define VECTORHELPER_H + +#include +#include + +namespace Akonadi { +template class Container> +QVector valuesToVector(const Container &container) +{ + QVector values; + values.reserve(container.size()); + Q_FOREACH (const Value &value, container) { + values << value; + } + return values; +} + +template +QSet vectorToSet(const QVector &container) +{ + QSet set; + set.reserve(container.size()); + Q_FOREACH (const T &value, container) { + set.insert(value); + } + return set; +} + +} + +#endif // VECTORHELPER_H diff --git a/src/interfaces/CMakeLists.txt b/src/interfaces/CMakeLists.txt new file mode 100644 index 0000000..9ee7452 --- /dev/null +++ b/src/interfaces/CMakeLists.txt @@ -0,0 +1,21 @@ +SET(DBUS_INTERFACE_XMLS + org.freedesktop.Akonadi.AgentManager.xml + org.freedesktop.Akonadi.NotificationManager.xml + org.freedesktop.Akonadi.Preprocessor.xml + org.freedesktop.Akonadi.Tracer.xml + org.freedesktop.Akonadi.Agent.Control.xml + org.freedesktop.Akonadi.Agent.Search.xml + org.freedesktop.Akonadi.Agent.Status.xml + org.freedesktop.Akonadi.Resource.xml + org.freedesktop.Akonadi.ControlManager.xml + org.freedesktop.Akonadi.NotificationSource.xml + org.freedesktop.Akonadi.Server.xml + org.freedesktop.Akonadi.StorageDebugger.xml + org.freedesktop.Akonadi.TracerNotification.xml +) + +install(FILES ${DBUS_INTERFACE_XMLS} + DESTINATION ${AKONADI_DBUS_INTERFACES_INSTALL_DIR} +) + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml b/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml new file mode 100644 index 0000000..9aab3c3 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml b/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml new file mode 100644 index 0000000..35a18fb --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml b/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml new file mode 100644 index 0000000..3641b03 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml b/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml new file mode 100644 index 0000000..55b6bb9 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml b/src/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml new file mode 100644 index 0000000..4110421 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.AgentServer.xml b/src/interfaces/org.freedesktop.Akonadi.AgentServer.xml new file mode 100644 index 0000000..01540e5 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.AgentServer.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml b/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml new file mode 100644 index 0000000..07ad225 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Janitor.xml b/src/interfaces/org.freedesktop.Akonadi.Janitor.xml new file mode 100644 index 0000000..c1502df --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Janitor.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml b/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml new file mode 100644 index 0000000..18ef48f --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml b/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml new file mode 100644 index 0000000..cdef9bf --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml b/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml new file mode 100644 index 0000000..2535294 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml b/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml new file mode 100644 index 0000000..bf1ef66 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Resource.Transport.xml b/src/interfaces/org.freedesktop.Akonadi.Resource.Transport.xml new file mode 100644 index 0000000..b372cba --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Resource.Transport.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Resource.xml b/src/interfaces/org.freedesktop.Akonadi.Resource.xml new file mode 100644 index 0000000..7999960 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Resource.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml b/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml new file mode 100644 index 0000000..3f5b62c --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.SearchManager.xml b/src/interfaces/org.freedesktop.Akonadi.SearchManager.xml new file mode 100644 index 0000000..b2ba6ba --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.SearchManager.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Server.xml b/src/interfaces/org.freedesktop.Akonadi.Server.xml new file mode 100644 index 0000000..7c6da49 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Server.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml b/src/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml new file mode 100644 index 0000000..5b02450 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Tracer.xml b/src/interfaces/org.freedesktop.Akonadi.Tracer.xml new file mode 100644 index 0000000..09e88a7 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Tracer.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.TracerNotification.xml b/src/interfaces/org.freedesktop.Akonadi.TracerNotification.xml new file mode 100644 index 0000000..1e290b6 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.TracerNotification.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/private/CMakeLists.txt b/src/private/CMakeLists.txt new file mode 100644 index 0000000..d153d86 --- /dev/null +++ b/src/private/CMakeLists.txt @@ -0,0 +1,66 @@ +set(akonadiprivate_SRCS + imapparser.cpp + imapset.cpp + instance.cpp + datastream_p.cpp + externalpartstorage.cpp + protocol.cpp + scope.cpp + tristate.cpp + xdgbasedirs.cpp + standarddirs.cpp + dbus.cpp +) + +ecm_qt_declare_logging_category(akonadiprivate_SRCS HEADER akonadiprivate_debug.h IDENTIFIER AKONADIPRIVATE_LOG CATEGORY_NAME akonadiprivate_log) + +add_library(KF5AkonadiPrivate ${akonadiprivate_SRCS}) +add_library(KF5::AkonadiPrivate ALIAS KF5AkonadiPrivate) + +target_include_directories(KF5AkonadiPrivate PUBLIC "$") + + +target_link_libraries(KF5AkonadiPrivate +PUBLIC + Qt5::Core + Qt5::DBus +) +generate_export_header(KF5AkonadiPrivate BASE_NAME akonadiprivate) + +set_target_properties(KF5AkonadiPrivate PROPERTIES + VERSION ${AKONADI_VERSION_STRING} + SOVERSION ${AKONADI_SOVERSION} + EXPORT_NAME AkonadiPrivate +) + +install(TARGETS + KF5AkonadiPrivate + EXPORT KF5AkonadiTargets + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadiprivate_export.h + standarddirs_p.h + dbus_p.h + imapparser_p.h + imapset_p.h + instance_p.h + externalpartstorage_p.h + protocol_p.h + protocol_exception_p.h + xdgbasedirs_p.h + capabilities_p.h + scope_p.h + tristate_p.h + DESTINATION ${KF5_INCLUDE_INSTALL_DIR}/akonadi/private +) + + +### Private static library used by unit-tests #### + +add_library(akonadiprivate_static STATIC ${akonadiprivate_SRCS}) +target_link_libraries(akonadiprivate_static + Qt5::Core + Qt5::DBus +) diff --git a/src/private/capabilities_p.h b/src/private/capabilities_p.h new file mode 100644 index 0000000..190361d --- /dev/null +++ b/src/private/capabilities_p.h @@ -0,0 +1,37 @@ +/* + 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. +*/ + +#ifndef AKONADI_CAPABILITIES_P_H +#define AKONADI_CAPABILITIES_P_H + +/** + @file capabilites_p.h Shared constants for agent capabilities. + + @todo Fill this file with the missing capabilities. +*/ + +#define AKONADI_AGENT_CAPABILITY_AUTOSTART "Autostart" +#define AKONADI_AGENT_CAPABILITY_NOCONFIG "NoConfig" +#define AKONADI_AGENT_CAPABILITY_PREPROCESSOR "Preprocessor" +#define AKONADI_AGENT_CAPABILITY_RESOURCE "Resource" +#define AKONADI_AGENT_CAPABILITY_SEARCH "Search" +#define AKONADI_AGENT_CAPABILITY_UNIQUE "Unique" +#define AKONADI_AGENT_CAPABILITY_VIRTUAL "Virtual" + +#endif diff --git a/src/private/datastream_p.cpp b/src/private/datastream_p.cpp new file mode 100644 index 0000000..6d4c721 --- /dev/null +++ b/src/private/datastream_p.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2015 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "datastream_p_p.h" + +using namespace Akonadi; +using namespace Akonadi::Protocol; + +DataStream::DataStream() + : mDev(Q_NULLPTR) + , mWaitTimeout(30000) +{ +} + +DataStream::DataStream(QIODevice *device) + : mDev(device) + , mWaitTimeout(30000) +{ +} + +DataStream::~DataStream() +{ +} + +QIODevice *DataStream::device() const +{ + return mDev; +} + +void DataStream::setDevice(QIODevice *device) +{ + mDev = device; +} + +int DataStream::waitTimeout() const +{ + return mWaitTimeout; +} +void DataStream::setWaitTimeout(int timeout) +{ + mWaitTimeout = timeout; +} + +void DataStream::waitForData(quint32 size) +{ + while (mDev->bytesAvailable() < size) { + if (!mDev->waitForReadyRead(mWaitTimeout)) { + throw ProtocolException("Timeout while waiting for data"); + } + } +} + +void DataStream::writeRawData(const char *data, int len) +{ + Q_ASSERT(mDev); + + int ret = mDev->write(data, len); + if (ret != len) { + // TODO: Try to write data again unless ret is -1? + throw ProtocolException("Failed to write all data"); + } +} + +void DataStream::writeBytes(const char *bytes, int len) +{ + *this << (quint32) len; + if (len) { + writeRawData(bytes, len); + } +} + +int DataStream::readRawData(char *buffer, int len) +{ + Q_ASSERT(mDev); + return mDev->read(buffer, len); +} diff --git a/src/private/datastream_p_p.h b/src/private/datastream_p_p.h new file mode 100644 index 0000000..bced92b --- /dev/null +++ b/src/private/datastream_p_p.h @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2015 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef AKONADI_PROTOCOL_DATASTREAM_H +#define AKONADI_PROTOCOL_DATASTREAM_H + +#include + +#include "protocol_exception_p.h" + +#include +#include +#include + +namespace Akonadi { +namespace Protocol { + +class DataStream +{ + +public: + explicit DataStream(); + explicit DataStream(QIODevice *device); + ~DataStream(); + + QIODevice *device() const; + void setDevice(QIODevice *device); + + int waitTimeout() const; + void setWaitTimeout(int timeout); + + template + inline typename std::enable_if::value, DataStream>::type + &operator<<(T val); + template + inline typename std::enable_if::value, DataStream>::type + &operator<<(T val); + template + inline DataStream &operator<<(const QFlags &flags); + inline DataStream &operator<<(const QString &str); + inline DataStream &operator<<(const QByteArray &data); + inline DataStream &operator<<(const QDateTime &dt); + + + template + inline typename std::enable_if::value, DataStream>::type + &operator>>(T &val); + template + inline typename std::enable_if::value, DataStream>::type + &operator>>(T &val); + template + inline DataStream &operator>>(QFlags &flags); + inline DataStream &operator>>(QString &str); + inline DataStream &operator>>(QByteArray &data); + inline DataStream &operator>>(QDateTime &dt); + + + void writeRawData(const char *data, int len); + void writeBytes(const char *bytes, int len); + + int readRawData(char *buffer, int len); + + void waitForData(quint32 size); +private: + + + Q_DISABLE_COPY(DataStream) + + QIODevice *mDev; + int mWaitTimeout; +}; + +template +inline typename std::enable_if::value, DataStream>::type +&DataStream::operator<<(T val) +{ + Q_ASSERT(mDev); + if (mDev->write((char *)&val, sizeof(T)) != sizeof(T)) { + throw Akonadi::ProtocolException("Failed to write data to stream"); + } + return *this; +} + +template +inline typename std::enable_if::value, DataStream>::type +&DataStream::operator<<(T val) +{ + return *this << (typename std::underlying_type::type) val; +} + +template +inline DataStream &DataStream::operator<<(const QFlags &flags) +{ + return *this << (int) flags; +} + +inline DataStream &DataStream::operator<<(const QString &str) +{ + Q_ASSERT(mDev); + if (str.isNull()) { + *this << (quint32) 0xffffffff; + } else { + writeBytes(reinterpret_cast(str.unicode()), sizeof(QChar) * str.length()); + } + return *this; +} + +inline DataStream &DataStream::operator<<(const QByteArray &data) +{ + Q_ASSERT(mDev); + if (data.isNull()) { + *this << (quint32) 0xffffffff; + } else { + writeBytes(data.constData(), data.size()); + } + return *this; +} + +inline DataStream &DataStream::operator<<(const QDateTime &dt) +{ + *this << dt.date().toJulianDay() + << dt.time().msecsSinceStartOfDay() + << dt.timeSpec(); + if (dt.timeSpec() == Qt::OffsetFromUTC) { + *this << dt.offsetFromUtc(); + } else if (dt.timeSpec() == Qt::TimeZone) { + *this << dt.timeZone().id(); + } + return *this; +} + + + + +template +inline typename std::enable_if::value, DataStream>::type +&DataStream::operator>>(T &val) +{ + Q_ASSERT(mDev); + waitForData(sizeof(T)); + + if (mDev->read((char *)&val, sizeof(T)) != sizeof(T)) { + throw Akonadi::ProtocolException("Failed to read enough data from stream"); + } + return *this; +} + +template +inline typename std::enable_if::value, DataStream>::type +&DataStream::operator>>(T &val) +{ + return *this >> reinterpret_cast::type &>(val); +} + +template +inline DataStream &DataStream::operator>>(QFlags &flags) +{ + int i; + *this >> i; + flags = QFlags(i); + return *this; +} + +inline DataStream &DataStream::operator>>(QString &str) +{ + str.clear(); + + quint32 bytes = 0; + *this >> bytes; + if (bytes == 0xffffffff) { + return *this; + } else if (bytes == 0) { + str = QString(QLatin1String("")); + return *this; + } + + + if (bytes & 0x1) { + str.clear(); + throw Akonadi::ProtocolException("Read corrupt data"); + } + + const quint32 step = 1024 * 1024; + const quint32 len = bytes / 2; + quint32 allocated = 0; + + while (allocated < len) { + const int blockSize = qMin(step, len - allocated); + waitForData(blockSize * sizeof(QChar)); + str.resize(allocated + blockSize); + if (readRawData(reinterpret_cast(str.data()) + allocated * sizeof(QChar), + blockSize * sizeof(QChar)) != int(blockSize * sizeof(QChar))) { + throw Akonadi::ProtocolException("Failed to read enough data from stream"); + } + allocated += blockSize; + } + + return *this; +} + +inline DataStream &DataStream::operator>>(QByteArray &data) +{ + data.clear(); + + quint32 len = 0; + *this >> len; + if (len == 0xffffffff) { + return *this; + } + + const quint32 step = 1024 * 1024; + quint32 allocated = 0; + + while (allocated < len) { + const int blockSize = qMin(step, len - allocated); + waitForData(blockSize); + data.resize(allocated + blockSize); + if (readRawData(data.data() + allocated, blockSize) != blockSize) { + throw Akonadi::ProtocolException("Failed to read enough data from stream"); + } + allocated += blockSize; + } + + return *this; +} + +inline DataStream &DataStream::operator>>(QDateTime &dt) +{ + QDate date; + QTime time; + qint64 jd; + int mds; + Qt::TimeSpec spec; + + *this >> jd + >> mds + >> spec; + date = QDate::fromJulianDay(jd); + time = QTime::fromMSecsSinceStartOfDay(mds); + if (spec == Qt::OffsetFromUTC) { + int offset = 0; + *this >> offset; + dt = QDateTime(date, time, spec, offset); + } else if (spec == Qt::TimeZone) { + QByteArray id; + *this >> id; + dt = QDateTime(date, time, QTimeZone(id)); + } else { + dt = QDateTime(date, time, spec); + } + + return *this; +} + +} // namespace Protocol +} // namespace Akonadi + + +// Inline functions + +// Generic streaming for all Qt value-based containers (as well as std containers that +// implement operator<< for appending) +template class Container> +//typename std::enable_if::value, Akonadi::Protocol::DataStream>::type +inline Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Container &list) +{ + stream << (quint32) list.size(); + for (auto iter = list.cbegin(), end = list.cend(); iter != end; ++iter) { + stream << *iter; + } + return stream; +} + +template class Container> +// //typename std::enable_if::value, Akonadi::Protocol::DataStream>::type +inline Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Container &list) +{ + list.clear(); + quint32 size = 0; + stream >> size; + list.reserve(size); + for (quint32 i = 0; i < size; ++i) { + T t; + stream >> t; + list << t; + } + return stream; +} + +inline Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const QStringList &list) +{ + return stream << static_cast>(list); +} + +inline Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, QStringList &list) +{ + return stream >> static_cast&>(list); +} + + +namespace Akonadi { +namespace Protocol { +namespace Private { +template class Container> +inline void container_reserve(Container &container, int size) +{ + container.reserve(size); +} + +template +inline void container_reserve(QMap &, int) +{ + // noop +} +} // namespace Private +} // namespace Protocol +} // namespace Akonadi + + +// Generic streaming for all Qt dictionary-based containers +template class Container> +// typename std::enable_if::value, Akonadi::Protocol::DataStream>::type +inline Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Container &map) +{ + stream << (quint32) map.size(); + auto iter = map.cend(), begin = map.cbegin(); + while (iter != begin) { + --iter; + stream << iter.key() << iter.value(); + } + return stream; +} + +template class Container> +// typename std::enable_if::value, Akonadi::Protocol::DataStream>::type +inline Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Container &map) +{ + map.clear(); + quint32 size = 0; + stream >> size; + Akonadi::Protocol::Private::container_reserve(map, size); + for (quint32 i = 0; i < size; ++i) { + Key key; + Value value; + stream >> key >> value; + map.insertMulti(key, value); + } + return stream; +} + +#endif // AKONADI_PROTOCOL_DATASTREAM_H diff --git a/src/private/dbus.cpp b/src/private/dbus.cpp new file mode 100644 index 0000000..5df6de3 --- /dev/null +++ b/src/private/dbus.cpp @@ -0,0 +1,112 @@ +/* + Copyright (c) 2011 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 "dbus_p.h" +#include "instance_p.h" + +#include +#include +#include + +using namespace Akonadi; + +#define AKONADI_DBUS_SERVER_SERVICE "org.freedesktop.Akonadi" +#define AKONADI_DBUS_CONTROL_SERVICE "org.freedesktop.Akonadi.Control" +#define AKONADI_DBUS_CONTROL_SERVICE_LOCK "org.freedesktop.Akonadi.Control.lock" +#define AKONADI_DBUS_AGENTSERVER_SERVICE "org.freedesktop.Akonadi.AgentServer" +#define AKONADI_DBUS_STORAGEJANITOR_SERVICE "org.freedesktop.Akonadi.Janitor" +#define AKONADI_DBUS_SERVER_SERVICE_UPGRADING "org.freedesktop.Akonadi.upgrading" + +static QString makeServiceName(const char *base) +{ + if (!Instance::hasIdentifier()) { + return QLatin1String(base); + } + return QLatin1String(base) % QLatin1Literal(".") % Instance::identifier(); +} + +QString DBus::serviceName(DBus::ServiceType serviceType) +{ + switch (serviceType) { + case Server: + return makeServiceName(AKONADI_DBUS_SERVER_SERVICE); + case Control: + return makeServiceName(AKONADI_DBUS_CONTROL_SERVICE); + case ControlLock: + return makeServiceName(AKONADI_DBUS_CONTROL_SERVICE_LOCK); + case AgentServer: + return makeServiceName(AKONADI_DBUS_AGENTSERVER_SERVICE); + case StorageJanitor: + return makeServiceName(AKONADI_DBUS_STORAGEJANITOR_SERVICE); + case UpgradeIndicator: + return makeServiceName(AKONADI_DBUS_SERVER_SERVICE_UPGRADING); + } + Q_ASSERT(!"WTF?"); + return QString(); +} + +QString DBus::parseAgentServiceName(const QString &serviceName, DBus::AgentType &agentType) +{ + agentType = Unknown; + if (!serviceName.startsWith(QLatin1String("org.freedesktop.Akonadi."))) { + return QString(); + } + const QStringList parts = serviceName.mid(24).split(QLatin1Char('.')); + if ((parts.size() == 2 && !Akonadi::Instance::hasIdentifier()) + || (parts.size() == 3 && Akonadi::Instance::hasIdentifier() && Akonadi::Instance::identifier() == parts.at(2))) { + // switch on parts.at( 0 ) + if (parts.first() == QLatin1String("Agent")) { + agentType = Agent; + } else if (parts.first() == QLatin1String("Resource")) { + agentType = Resource; + } else if (parts.first() == QLatin1String("Preprocessor")) { + agentType = Preprocessor; + } else { + return QString(); + } + return parts.at(1); + } + + return QString(); +} + +QString DBus::agentServiceName(const QString &agentIdentifier, DBus::AgentType agentType) +{ + Q_ASSERT(!agentIdentifier.isEmpty()); + Q_ASSERT(agentType != Unknown); + QString serviceName = QStringLiteral("org.freedesktop.Akonadi."); + switch (agentType) { + case Agent: + serviceName += QLatin1String("Agent."); + break; + case Resource: + serviceName += QLatin1String("Resource."); + break; + case Preprocessor: + serviceName += QLatin1String("Preprocessor."); + break; + default: + Q_ASSERT(!"WTF?"); + } + serviceName += agentIdentifier; + if (Akonadi::Instance::hasIdentifier()) { + serviceName += QLatin1Char('.') % Akonadi::Instance::identifier(); + } + return serviceName; +} diff --git a/src/private/dbus_p.h b/src/private/dbus_p.h new file mode 100644 index 0000000..e55da1c --- /dev/null +++ b/src/private/dbus_p.h @@ -0,0 +1,83 @@ +/* + Copyright (c) 2011 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_DBUS_H +#define AKONADI_DBUS_H + +#include "akonadiprivate_export.h" + +class QString; + +/** + * Helper methods for obtaining D-Bus identifiers. + * This should be used instead of hardcoded identifiers or constants to support multi-instance namespacing + * @since 1.7 + */ + + + +#define AKONADI_DBUS_AGENTMANAGER_PATH "/AgentManager" +#define AKONADI_DBUS_AGENTSERVER_PATH "/AgentServer" +#define AKONADI_DBUS_STORAGEJANITOR_PATH "/Janitor" + +namespace Akonadi { + +namespace DBus { + +/** D-Bus service types used by the Akonadi server processes. */ +enum ServiceType { + Server, + Control, + ControlLock, + AgentServer, + StorageJanitor, + UpgradeIndicator +}; + +/** + * Returns the service name for the given @p serviceType. + */ +AKONADIPRIVATE_EXPORT QString serviceName(ServiceType serviceType); + +/** Known D-Bus service name types for agents. */ +enum AgentType { + Unknown, + Agent, + Resource, + Preprocessor +}; + +/** + * Parses a D-Bus service name and checks if it belongs to an agent of this instance. + * @param serviceName The service name to parse. + * @param agentType Output parameter containing the agent type. + * @return The identifier of the agent, empty string if that's not an agent (or an agent of a different Akonadi instance) + */ +AKONADIPRIVATE_EXPORT QString parseAgentServiceName(const QString &serviceName, DBus::AgentType &agentType); + +/** + * Returns the D-Bus service name of the agent @p agentIdentifier for type @p agentType. + */ +AKONADIPRIVATE_EXPORT QString agentServiceName(const QString &agentIdentifier, DBus::AgentType agentType); + +} + +} + +#endif diff --git a/src/private/externalpartstorage.cpp b/src/private/externalpartstorage.cpp new file mode 100644 index 0000000..4c2cbeb --- /dev/null +++ b/src/private/externalpartstorage.cpp @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2015 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "externalpartstorage_p.h" +#include "standarddirs_p.h" +#include "akonadiprivate_debug.h" + +#include +#include +#include +#include + +using namespace Akonadi; + +ExternalPartStorage *ExternalPartStorage::sInstance = Q_NULLPTR; + +ExternalPartStorageTransaction::ExternalPartStorageTransaction() +{ + ExternalPartStorage::self()->beginTransaction(); +} + +ExternalPartStorageTransaction::~ExternalPartStorageTransaction() +{ + if (ExternalPartStorage::self()->inTransaction()) { + rollback(); + } +} + +bool ExternalPartStorageTransaction::commit() +{ + return ExternalPartStorage::self()->commitTransaction(); +} + +bool ExternalPartStorageTransaction::rollback() +{ + return ExternalPartStorage::self()->rollbackTransaction(); +} + + + + +ExternalPartStorage::ExternalPartStorage() +{ +} + +ExternalPartStorage *ExternalPartStorage::self() +{ + static QMutex instanceLock; + QMutexLocker locker(&instanceLock); + if (!sInstance) { + sInstance = new ExternalPartStorage(); + } + return sInstance; +} + +QString ExternalPartStorage::resolveAbsolutePath(const QByteArray &filename, bool *exists, bool legacyFallback) +{ + return resolveAbsolutePath(QString::fromLocal8Bit(filename), exists, legacyFallback); +} + +QString ExternalPartStorage::resolveAbsolutePath(const QString &filename, bool *exists, bool legacyFallback) +{ + if (exists) { + *exists = false; + } + + QFileInfo finfo(filename); + if (finfo.isAbsolute()) { + if (exists && finfo.exists()) { + *exists = true; + } + return filename; + } + + const QString basePath = StandardDirs::saveDir("data", QStringLiteral("file_db_data")); + Q_ASSERT(!basePath.isEmpty()); + + // Part files are stored in levelled cache. We use modulo 100 of the partID + // to ensure even distribution of the part files into the subfolders. + // PartID is encoded in filename as "PARTID_rX". + const int revPos = filename.indexOf(QLatin1Char('_')); + const QString path = basePath + + QDir::separator() + + (revPos > 1 ? filename[revPos - 2] : QLatin1Char('0')) + + filename[revPos - 1] + + QDir::separator() + + filename; + // If legacy fallback is disabled, return it in any case + if (!legacyFallback) { + QFileInfo finfo(path); + QDir().mkpath(finfo.path()); + return path; + } + + // ..otherwise return it only if it exists + if (QFile::exists(path)) { + if (exists) { + *exists = true; + } + return path; + } + + // .. and fallback to legacy if it does not, but only when legacy exists + const QString legacyPath = basePath + QDir::separator() + filename; + if (QFile::exists(legacyPath)) { + if (exists) { + *exists = true; + } + return legacyPath; + } else { + QFileInfo finfo(path); + QDir().mkpath(finfo.path()); + // If neither legacy or new path exists, return the new path, so that + // new items are created in the correct location + return path; + } +} + +bool ExternalPartStorage::createPartFile(const QByteArray &data, qint64 partId, + QByteArray &partFileName) +{ + bool exists = false; + partFileName = updateFileNameRevision(QByteArray::number(partId)); + const QString path = resolveAbsolutePath(partFileName, &exists); + if (exists) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to create a part" << partFileName << ", which already exists!"; + return false; + } + + QFile f(path); + if (!f.open(QIODevice::WriteOnly)) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to open new part file for writing:" << f.errorString(); + return false; + } + if (f.write(data) != data.size()) { + // TODO: Maybe just try again? + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to write all data into the part file"; + return false; + } + f.close(); + + if (inTransaction()) { + addToTransaction({ { Operation::Create, path } }); + } + return true; +} + +bool ExternalPartStorage::updatePartFile(const QByteArray &newData, const QByteArray &partFile, + QByteArray &newPartFile) +{ + bool exists = false; + const QString currentPartPath = resolveAbsolutePath(partFile, &exists); + if (!exists) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to update a non-existent part, aborting update"; + return false; + } + + newPartFile = updateFileNameRevision(partFile); + exists = false; + const QString newPartPath = resolveAbsolutePath(newPartFile, &exists); + if (exists) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to update part" << partFile << ", but" << newPartFile << "already exists, aborting update"; + return false; + } + + QFile f(newPartPath); + if (!f.open(QIODevice::WriteOnly)) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to open new part file for update:" << f.errorString(); + return false; + } + + if (f.write(newData) != newData.size()) { + // TODO: Maybe just try again? + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to write all data into the part file"; + return false; + } + f.close(); + + if (inTransaction()) { + addToTransaction({ { Operation::Create, newPartPath }, + { Operation::Delete, currentPartPath } }); + } else { + if (!QFile::remove(currentPartPath)) { + // Not a reason to fail the operation + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to remove old part payload file" << currentPartPath; + } + } + + return true; +} + +bool ExternalPartStorage::removePartFile(const QString &partFile) +{ + if (inTransaction()) { + addToTransaction({ { Operation::Delete, partFile } }); + } else { + if (!QFile::remove(partFile)) { + // Not a reason to fail the operation + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to remove part file" << partFile; + } + } + + return true; +} + +QByteArray ExternalPartStorage::updateFileNameRevision(const QByteArray &filename) +{ + const int revIndex = filename.indexOf("_r"); + if (revIndex > -1) { + QByteArray rev = filename.mid(revIndex + 2); + int r = rev.toInt(); + r++; + rev = QByteArray::number(r); + return filename.left(revIndex + 2) + rev; + } + + return filename + "_r0"; +} + +QByteArray ExternalPartStorage::nameForPartId(qint64 partId) +{ + return QByteArray::number(partId) + "_r0"; +} + +bool ExternalPartStorage::beginTransaction() +{ + QMutexLocker locker(&mTransactionLock); + if (mTransactions.contains(QThread::currentThread())) { + qCDebug(AKONADIPRIVATE_LOG) << "Error: there is already a transaction in progress in this thread"; + return false; + } + + mTransactions.insert(QThread::currentThread(), QVector()); + return true; +} + +QString ExternalPartStorage::akonadiStoragePath() +{ + return StandardDirs::saveDir("data", QStringLiteral("file_db_data")); +} + +bool ExternalPartStorage::commitTransaction() +{ + QMutexLocker locker(&mTransactionLock); + auto iter = mTransactions.find(QThread::currentThread()); + if (iter == mTransactions.end()) { + qCDebug(AKONADIPRIVATE_LOG) << "Commit error: there is no transaction in progress in this thread"; + return false; + } + + const QVector trx = iter.value(); + mTransactions.erase(iter); + locker.unlock(); + + return replayTransaction(trx, true); +} + +bool ExternalPartStorage::rollbackTransaction() +{ + QMutexLocker locker(&mTransactionLock); + auto iter = mTransactions.find(QThread::currentThread()); + if (iter == mTransactions.end()) { + qCDebug(AKONADIPRIVATE_LOG) << "Rollback error: there is no transaction in progress in this thread"; + return false; + } + + const QVector trx = iter.value(); + mTransactions.erase(iter); + locker.unlock(); + + return replayTransaction(trx, false); +} + +bool ExternalPartStorage::inTransaction() const +{ + QMutexLocker locker(&mTransactionLock); + return mTransactions.contains(QThread::currentThread()); +} + +void ExternalPartStorage::addToTransaction(const QVector &ops) +{ + QMutexLocker locker(&mTransactionLock); + auto iter = mTransactions.find(QThread::currentThread()); + Q_ASSERT(iter != mTransactions.end()); + locker.unlock(); + + Q_FOREACH (const Operation &op, ops) { + iter->append(op); + } +} + +bool ExternalPartStorage::replayTransaction(const QVector &trx, bool commit) +{ + for (auto iter = trx.constBegin(), end = trx.constEnd(); iter != end; ++iter) { + const Operation &op = *iter; + + if (op.type == Operation::Create) { + if (commit) { + // no-op: we actually created that already in createPart()/updatePart() + } else { + if (!QFile::remove(op.filename)) { + // We failed to remove the file, but don't abort the rollback. + // This is an error, but does not cause data loss. + qCDebug(AKONADIPRIVATE_LOG) << "Warning: failed to remove" << op.filename << "while rolling back a transaction"; + } + } + } else if (op.type == Operation::Delete) { + if (commit) { + if (!QFile::remove(op.filename)) { + // We failed to remove the file, but don't abort the commit. + // This is an error, but does not cause data loss. + qCDebug(AKONADIPRIVATE_LOG) << "Warning: failed to remove" << op.filename << "while committing a transaction"; + } + } else { + // no-op: we did not actually delete the file yet + } + } else { + Q_UNREACHABLE(); + } + } + + return true; +} diff --git a/src/private/externalpartstorage_p.h b/src/private/externalpartstorage_p.h new file mode 100644 index 0000000..6c292bf --- /dev/null +++ b/src/private/externalpartstorage_p.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2015 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef EXTERNALPARTSTORAGE_P_H +#define EXTERNALPARTSTORAGE_P_H + +#include "akonadiprivate_export.h" + +#include +#include +#include + +class QString; +class QByteArray; +class QThread; + +namespace Akonadi { + +class AKONADIPRIVATE_EXPORT ExternalPartStorageTransaction +{ +public: + explicit ExternalPartStorageTransaction(); + ~ExternalPartStorageTransaction(); + + bool commit(); + bool rollback(); +private: + Q_DISABLE_COPY(ExternalPartStorageTransaction) +}; + +/** + * Provides access to external payload part file storage + * + * Use ExternalPartStorageTransaction to delay deletion of part files until + * commit. Files created during the transaction will be deleted when transaction + * is rolled back to keep the storage clean. + */ +class AKONADIPRIVATE_EXPORT ExternalPartStorage +{ +public: + static ExternalPartStorage *self(); + + static QString resolveAbsolutePath(const QByteArray &filename, bool *exists = Q_NULLPTR, bool legacyFallback = true); + static QString resolveAbsolutePath(const QString &filename, bool *exists = Q_NULLPTR, bool legacyFallback = true); + static QByteArray updateFileNameRevision(const QByteArray &filename); + static QByteArray nameForPartId(qint64 partId); + static QString akonadiStoragePath(); + + bool updatePartFile(const QByteArray &newData, const QByteArray &partFile, QByteArray &newPartFile); + bool createPartFile(const QByteArray &newData, qint64 partId, QByteArray &partFileName); + bool removePartFile(const QString &partFile); + + bool inTransaction() const; + +private: + friend class ExternalPartStorageTransaction; + + struct Operation + { + enum Type { + Create, + Delete + // We never update files, we always create a new one with increased + // revision number, hence no "Update" + }; + + Type type; + QString filename; + }; + + ExternalPartStorage(); + + bool beginTransaction(); + bool commitTransaction(); + bool rollbackTransaction(); + + bool replayTransaction(const QVector &trx, bool commit); + void addToTransaction(const QVector &ops); + + static ExternalPartStorage *sInstance; + + mutable QMutex mTransactionLock; + QHash> mTransactions; +}; + +} + +#endif // EXTERNALPARTSTORAGE_P_H diff --git a/src/private/imapparser.cpp b/src/private/imapparser.cpp new file mode 100644 index 0000000..93112a0 --- /dev/null +++ b/src/private/imapparser.cpp @@ -0,0 +1,700 @@ +/* + 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 "imapparser_p.h" + +#include +#include + +#include + +using namespace Akonadi; + +class ImapParser::Private +{ +public: + QByteArray tagBuffer; + QByteArray dataBuffer; + int parenthesesCount; + qint64 literalSize; + bool continuation; + + // returns true if readBuffer contains a literal start and sets + // parser state accordingly + bool checkLiteralStart(const QByteArray &readBuffer, int pos = 0) + { + if (readBuffer.trimmed().endsWith('}')) { + const int begin = readBuffer.lastIndexOf('{'); + const int end = readBuffer.lastIndexOf('}'); + + // new literal in previous literal data block + if (begin < pos) { + return false; + } + + // TODO error handling + literalSize = readBuffer.mid(begin + 1, end - begin - 1).toLongLong(); + + // empty literal + if (literalSize == 0) { + return false; + } + + continuation = true; + dataBuffer.reserve(dataBuffer.size() + literalSize + 1); + return true; + } + return false; + } +}; + +namespace { + +template +int parseParenthesizedListHelper(const QByteArray &data, T &result, int start) +{ + result.clear(); + if (start >= data.length()) { + return data.length(); + } + + const int begin = data.indexOf('(', start); + if (begin < 0) { + return start; + } + + result.reserve(16); + + int count = 0; + int sublistBegin = start; + bool insideQuote = false; + for (int i = begin + 1; i < data.length(); ++i) { + const char currentChar = data[i]; + if (currentChar == '(' && !insideQuote) { + ++count; + if (count == 1) { + sublistBegin = i; + } + + continue; + } + + if (currentChar == ')' && !insideQuote) { + if (count <= 0) { + return i + 1; + } + + if (count == 1) { + result.append(data.mid(sublistBegin, i - sublistBegin + 1)); + } + + --count; + continue; + } + + if (currentChar == ' ' || currentChar == '\n' || currentChar == '\r') { + continue; + } + + if (count == 0) { + QByteArray ba; + const int consumed = ImapParser::parseString(data, ba, i); + i = consumed - 1; // compensate for the for loop increment + result.append(ba); + } else if (count > 0) { + if (currentChar == '"') { + insideQuote = !insideQuote; + } else if (currentChar == '\\' && insideQuote) { + ++i; + continue; + } + } + } + + return data.length(); +} + +} + +int ImapParser::parseParenthesizedList(const QByteArray &data, QVarLengthArray &result, int start) +{ + return parseParenthesizedListHelper(data, result, start); +} + +int ImapParser::parseParenthesizedList(const QByteArray &data, QList &result, int start) +{ + return parseParenthesizedListHelper(data, result, start); +} + +int ImapParser::parseString(const QByteArray &data, QByteArray &result, int start) +{ + int begin = stripLeadingSpaces(data, start); + result.clear(); + if (begin >= data.length()) { + return data.length(); + } + + // literal string + // TODO: error handling + if (data[begin] == '{') { + int end = data.indexOf('}', begin); + Q_ASSERT(end > begin); + int size = data.mid(begin + 1, end - begin - 1).toInt(); + + // strip CRLF + begin = end + 1; + if (begin < data.length() && data[begin] == '\r') { + ++begin; + } + if (begin < data.length() && data[begin] == '\n') { + ++begin; + } + + end = begin + size; + result = data.mid(begin, end - begin); + return end; + } + + // quoted string + return parseQuotedString(data, result, begin); +} + +int ImapParser::parseQuotedString(const QByteArray &data, QByteArray &result, int start) +{ + int begin = stripLeadingSpaces(data, start); + int end = begin; + result.clear(); + if (begin >= data.length()) { + return data.length(); + } + + bool foundSlash = false; + // quoted string + if (data[begin] == '"') { + ++begin; + result.reserve(qMin(32, data.size() - begin)); + for (int i = begin; i < data.length(); ++i) { + const char ch = data.at(i); + if (foundSlash) { + foundSlash = false; + if (ch == 'r') { + result += '\r'; + } else if (ch == 'n') { + result += '\n'; + } else if (ch == '\\') { + result += '\\'; + } else if (ch == '\"') { + result += '\"'; + } else { + //TODO: this is actually an error + result += ch; + } + continue; + } + if (ch == '\\') { + foundSlash = true; + continue; + } + if (ch == '"') { + end = i + 1; // skip the '"' + break; + } + result += ch; + } + } else { + // unquoted string + bool reachedInputEnd = true; + for (int i = begin; i < data.length(); ++i) { + const char ch = data.at(i); + if (ch == ' ' || ch == '(' || ch == ')' || ch == '\n' || ch == '\r') { + end = i; + reachedInputEnd = false; + break; + } + if (ch == '\\') { + foundSlash = true; + } + } + if (reachedInputEnd) { + end = data.length(); + } + result = data.mid(begin, end - begin); + + // transform unquoted NIL + if (result == "NIL") { + result.clear(); + } + + // strip quotes + if (foundSlash) { + while (result.contains("\\\"")) { + result.replace("\\\"", "\""); + } + while (result.contains("\\\\")) { + result.replace("\\\\", "\\"); + } + } + } + + return end; +} + +int ImapParser::stripLeadingSpaces(const QByteArray &data, int start) +{ + for (int i = start; i < data.length(); ++i) { + if (data[i] != ' ') { + return i; + } + } + + return data.length(); +} + +int ImapParser::parenthesesBalance(const QByteArray &data, int start) +{ + int count = 0; + bool insideQuote = false; + for (int i = start; i < data.length(); ++i) { + const char ch = data[i]; + if (ch == '"') { + insideQuote = !insideQuote; + continue; + } + if (ch == '\\' && insideQuote) { + ++i; + continue; + } + if (ch == '(' && !insideQuote) { + ++count; + continue; + } + if (ch == ')' && !insideQuote) { + --count; + continue; + } + } + return count; +} + +QByteArray ImapParser::join(const QList &list, const QByteArray &separator) +{ + // shortcuts for the easy cases + if (list.isEmpty()) { + return QByteArray(); + } + if (list.size() == 1) { + return list.first(); + } + + // avoid expensive realloc's by determining the size beforehand + QList::const_iterator it = list.constBegin(); + const QList::const_iterator endIt = list.constEnd(); + int resultSize = (list.size() - 1) * separator.size(); + for (; it != endIt; ++it) { + resultSize += (*it).size(); + } + + QByteArray result; + result.reserve(resultSize); + it = list.constBegin(); + result += (*it); + ++it; + for (; it != endIt; ++it) { + result += separator; + result += (*it); + } + + return result; +} + +QByteArray ImapParser::join(const QSet &set, const QByteArray &separator) +{ + const QList list = QList::fromSet(set); + + return ImapParser::join(list, separator); +} + +int ImapParser::parseString(const QByteArray &data, QString &result, int start) +{ + QByteArray tmp; + const int end = parseString(data, tmp, start); + result = QString::fromUtf8(tmp); + return end; +} + +int ImapParser::parseNumber(const QByteArray &data, qint64 &result, bool *ok, int start) +{ + if (ok) { + *ok = false; + } + + int pos = stripLeadingSpaces(data, start); + if (pos >= data.length()) { + return data.length(); + } + + int begin = pos; + for (; pos < data.length(); ++pos) { + if (!isdigit(data.at(pos))) { + break; + } + } + + const QByteArray tmp = data.mid(begin, pos - begin); + result = tmp.toLongLong(ok); + + return pos; +} + +QByteArray ImapParser::quote(const QByteArray &data) +{ + if (data.isEmpty()) { + static const QByteArray empty("\"\""); + return empty; + } + + const int inputLength = data.length(); + int stuffToQuote = 0; + for (int i = 0; i < inputLength; ++i) { + const char ch = data.at(i); + if (ch == '"' || ch == '\\' || ch == '\n' || ch == '\r') { + ++stuffToQuote; + } + } + + QByteArray result; + result.reserve(inputLength + stuffToQuote + 2); + result += '"'; + + // shortcut for the case that we don't need to quote anything at all + if (stuffToQuote == 0) { + result += data; + } else { + for (int i = 0; i < inputLength; ++i) { + const char ch = data.at(i); + if (ch == '\n') { + result += "\\n"; + continue; + } + + if (ch == '\r') { + result += "\\r"; + continue; + } + + if (ch == '"' || ch == '\\') { + result += '\\'; + } + + result += ch; + } + } + + result += '"'; + return result; +} + +int ImapParser::parseSequenceSet(const QByteArray &data, ImapSet &result, int start) +{ + int begin = stripLeadingSpaces(data, start); + qint64 value = -1, lower = -1, upper = -1; + for (int i = begin; i < data.length(); ++i) { + if (data[i] == '*') { + value = 0; + } else if (data[i] == ':') { + lower = value; + } else if (isdigit(data[i])) { + bool ok = false; + i = parseNumber(data, value, &ok, i); + Q_ASSERT(ok); // TODO handle error + --i; + } else { + upper = value; + if (lower < 0) { + lower = value; + } + result.add(ImapInterval(lower, upper)); + lower = -1; + upper = -1; + value = -1; + if (data[i] != ',') { + return i; + } + } + } + // take care of left-overs at input end + upper = value; + if (lower < 0) { + lower = value; + } + + if (lower >= 0 && upper >= 0) { + result.add(ImapInterval(lower, upper)); + } + + return data.length(); +} + +int ImapParser::parseDateTime(const QByteArray &data, QDateTime &dateTime, int start) +{ + // Syntax: + // date-time = DQUOTE date-day-fixed "-" date-month "-" date-year + // SP time SP zone DQUOTE + // date-day-fixed = (SP DIGIT) / 2DIGIT + // ; Fixed-format version of date-day + // date-month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / + // "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec" + // date-year = 4DIGIT + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + // ; Hours minutes seconds + // zone = ("+" / "-") 4DIGIT + // ; Signed four-digit value of hhmm representing + // ; hours and minutes east of Greenwich (that is, + // ; the amount that the given time differs from + // ; Universal Time). Subtracting the timezone + // ; from the given time will give the UT form. + // ; The Universal Time zone is "+0000". + // Example : "28-May-2006 01:03:35 +0200" + // Position: 0123456789012345678901234567 + // 1 2 + + int pos = stripLeadingSpaces(data, start); + if (data.length() <= pos) { + return pos; + } + + bool quoted = false; + if (data[pos] == '"') { + quoted = true; + ++pos; + + if (data.length() <= pos + 26) { + return start; + } + } else { + if (data.length() < pos + 26) { + return start; + } + } + + bool ok = true; + const int day = (data[pos] == ' ' ? data[pos + 1] - '0' // single digit day + : data.mid(pos, 2).toInt(&ok)); + if (!ok) { + return start; + } + + pos += 3; + static const QByteArray shortMonthNames("janfebmaraprmayjunjulaugsepoctnovdec"); + int month = shortMonthNames.indexOf(data.mid(pos, 3).toLower()); + if (month == -1) { + return start; + } + + month = month / 3 + 1; + pos += 4; + const int year = data.mid(pos, 4).toInt(&ok); + if (!ok) { + return start; + } + + pos += 5; + const int hours = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + pos += 3; + const int minutes = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + pos += 3; + const int seconds = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + pos += 4; + const int tzhh = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + pos += 2; + const int tzmm = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + int tzsecs = tzhh * 60 * 60 + tzmm * 60; + if (data[pos - 3] == '-') { + tzsecs = -tzsecs; + } + + const QDate date(year, month, day); + const QTime time(hours, minutes, seconds); + dateTime = QDateTime(date, time, Qt::UTC); + if (!dateTime.isValid()) { + return start; + } + + dateTime = dateTime.addSecs(-tzsecs); + + pos += 2; + if (data.length() <= pos || !quoted) { + return pos; + } + + if (data[pos] == '"') { + ++pos; + } + + return pos; +} + +void ImapParser::splitVersionedKey(const QByteArray &data, QByteArray &key, int &version) +{ + if (data.contains('[') && data.contains(']')) { + const int startPos = data.indexOf('['); + const int endPos = data.indexOf(']'); + if (startPos != -1 && endPos != -1 && endPos > startPos) { + bool ok = false; + + version = data.mid(startPos + 1, endPos - startPos - 1).toInt(&ok); + if (!ok) { + version = 0; + } + + key = data.left(startPos); + } + } else { + key = data; + version = 0; + } +} + +ImapParser::ImapParser() + : d(new Private) +{ + reset(); +} + +ImapParser::~ImapParser() +{ + delete d; +} + +bool ImapParser::parseNextLine(const QByteArray &readBuffer) +{ + d->continuation = false; + + // first line, get the tag + if (d->tagBuffer.isEmpty()) { + const int startOfData = ImapParser::parseString(readBuffer, d->tagBuffer); + if (startOfData < readBuffer.length() && startOfData >= 0) { + d->dataBuffer = readBuffer.mid(startOfData + 1); + } + + } else { + d->dataBuffer += readBuffer; + } + + // literal read in progress + if (d->literalSize > 0) { + d->literalSize -= readBuffer.size(); + + // still not everything read + if (d->literalSize > 0) { + return false; + } + + // check the remaining (non-literal) part for parentheses + if (d->literalSize < 0) { + // the following looks strange but works since literalSize can be negative here + d->parenthesesCount += ImapParser::parenthesesBalance(readBuffer, readBuffer.length() + d->literalSize); + + // check if another literal read was started + if (d->checkLiteralStart(readBuffer, readBuffer.length() + d->literalSize)) { + return false; + } + } + + // literal string finished but still open parentheses + if (d->parenthesesCount > 0) { + return false; + } + + } else { + + // open parentheses + d->parenthesesCount += ImapParser::parenthesesBalance(readBuffer); + + // start new literal read + if (d->checkLiteralStart(readBuffer)) { + return false; + } + + // still open parentheses + if (d->parenthesesCount > 0) { + return false; + } + + // just a normal response, fall through + } + + return true; +} + +void ImapParser::parseBlock(const QByteArray &data) +{ + Q_ASSERT(d->literalSize >= data.size()); + d->literalSize -= data.size(); + d->dataBuffer += data; +} + +QByteArray ImapParser::tag() const +{ + return d->tagBuffer; +} + +QByteArray ImapParser::data() const +{ + return d->dataBuffer; +} + +void ImapParser::reset() +{ + d->dataBuffer.clear(); + d->tagBuffer.clear(); + d->parenthesesCount = 0; + d->literalSize = 0; + d->continuation = false; +} + +bool ImapParser::continuationStarted() const +{ + return d->continuation; +} + +qint64 ImapParser::continuationSize() const +{ + return d->literalSize; +} diff --git a/src/private/imapparser_p.h b/src/private/imapparser_p.h new file mode 100644 index 0000000..88add05 --- /dev/null +++ b/src/private/imapparser_p.h @@ -0,0 +1,211 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_IMAPPARSER_P_H +#define AKONADI_IMAPPARSER_P_H + +#include "akonadiprivate_export.h" + +#include "imapset_p.h" + +#include +#include +#include + +namespace Akonadi { + +/** + Parser for IMAP messages. +*/ +class AKONADIPRIVATE_EXPORT ImapParser +{ +public: + /** + Parses the next parenthesized list in @p data starting from @p start + and puts the result into @p result. The number of used characters is + returned. + This does not recurse into sub-lists. + @param data Source data. + @param result The parsed list. + @param start Start parsing at this index. + */ + static int parseParenthesizedList(const QByteArray &data, QList &result, int start = 0); + static int parseParenthesizedList(const QByteArray &data, QVarLengthArray &result, int start = 0); + + /** + Parse the next string in @p data (quoted or literal) starting from @p start + and puts the result into @p result. The number of used characters is returned + (this is not equal to result.length()!). + @param data Source data. + @param result Parsed string, quotation, literal marker, etc. are removed, + 'NIL' is transformed into an empty QByteArray. + @param start start parsing at this index. + */ + static int parseString(const QByteArray &data, QByteArray &result, int start = 0); + + /** + Parses the next quoted string from @p data starting at @p start and puts it into + @p result. The number of parsed characters is returned (this is not equal to result.length()!). + @param data Source data. + @param result Parsed string, quotation is removed and 'NIL' is transformed to an empty QByteArray. + @param start Start parsing at this index. + */ + static int parseQuotedString(const QByteArray &data, QByteArray &result, int start = 0); + + /** + Returns the number of leading espaces in @p data starting from @p start. + @param data The source data. + @param start Start parsing at this index. + */ + static int stripLeadingSpaces(const QByteArray &data, int start = 0); + + /** + Returns the parentheses balance for the given data, considering quotes. + @param data The source data. + @param start Start parsing at this index. + */ + static int parenthesesBalance(const QByteArray &data, int start = 0); + + /** + Joins a QByteArray list with the given separator. + @param list The QByteArray list to join. + @param separator The separator. + */ + static QByteArray join(const QList &list, const QByteArray &separator); + + /** + Joins a QByteArray set with the given separator. + @param set The QByteArray set to join. + @param separator The separator. + */ + static QByteArray join(const QSet &set, const QByteArray &separator); + + /** + Same as parseString(), but with additional UTF-8 decoding of the result. + @param data Source data. + @param result Parsed string, quotation, literal marker, etc. are removed, + 'NIL' is transformed into an empty QString. UTF-8 decoding is applied.. + @param start Start parsing at this index. + */ + static int parseString(const QByteArray &data, QString &result, int start = 0); + + /** + Parses the next integer number from @p data starting at start and puts it into + @p result. The number of characters parsed is returned (this is not the parsed result!). + @param data Source data. + @param result Parsed integer number, invalid if ok is false. + @param ok Set to false if the parsing failed. + @param start Start parsing at this index. + */ + static int parseNumber(const QByteArray &data, qint64 &result, bool *ok = 0, int start = 0); + + /** + Quotes the given QByteArray. + @param data Source data. + */ + static QByteArray quote(const QByteArray &data); + + /** + Parse an IMAP sequence set. + @param data source data. + @param result The parse sequence set. + @param start start parsing at this index. + @return end position of parsing. + */ + static int parseSequenceSet(const QByteArray &data, ImapSet &result, int start = 0); + + /** + Parse an IMAP date/time value. + @param data source data. + @param dateTime The result date/time. + @param start Start parsing at this index. + @return end position of parsing. + */ + static int parseDateTime(const QByteArray &data, QDateTime &dateTime, int start = 0); + + /** + Split a versioned key of the form 'key[version]' into its components. + @param data The versioned key. + @param key The unversioned key. + @param version The version of the key or 0 if no version was set. + */ + static void splitVersionedKey(const QByteArray &data, QByteArray &key, int &version); + + /** + Constructs a new IMAP parser. + */ + ImapParser(); + + /** + Destroys an IMAP parser. + */ + ~ImapParser(); + + /** + Parses the given line. + @returns True if an IMAP message was parsed completely, false if more data is needed. + @todo read from a QIODevice directly to avoid an extra line buffer + */ + bool parseNextLine(const QByteArray &readBuffer); + + /** + Parses the given block of data. + Note: This currently only handles continuation blocks. + @param data The data to parse. + */ + void parseBlock(const QByteArray &data); + + /** + Returns the tag of the parsed message. + Only valid if parseNextLine() returned true. + */ + QByteArray tag() const; + + /** + Return the raw data of the parsed IMAP message. + Only valid if parseNextLine() returned true. + */ + QByteArray data() const; + + /** + Resets the internal state of the parser. Call before parsing + a new IMAP message. + */ + void reset(); + + /** + Returns true if the last parsed line contained a literal continuation, + ie. readiness for receiving literal data needs to be indicated. + */ + bool continuationStarted() const; + + /** + Returns the expected size of liteal data. + */ + qint64 continuationSize() const; + +private: + Q_DISABLE_COPY(ImapParser) + class Private; + Private *const d; +}; + +} + +#endif diff --git a/src/private/imapset.cpp b/src/private/imapset.cpp new file mode 100644 index 0000000..261fabb --- /dev/null +++ b/src/private/imapset.cpp @@ -0,0 +1,325 @@ +/* + 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 "imapset_p.h" +#include "datastream_p_p.h" + +#include +#include + +#include + +namespace Akonadi +{ + +class ImapInterval::Private : public QSharedData +{ +public: + Private() + : QSharedData() + , begin(0) + , end(0) + { + } + + Private(const Private &other) + : QSharedData(other) + { + begin = other.begin; + end = other.end; + } + + Id begin; + Id end; +}; + +class ImapSet::Private : public QSharedData +{ +public: + Private() + : QSharedData() + { + } + + Private(const Private &other) + : QSharedData(other) + { + intervals = other.intervals; + } + + ImapInterval::List intervals; +}; + +ImapInterval::ImapInterval() + : d(new Private) +{ +} + +ImapInterval::ImapInterval(const ImapInterval &other) + : d(other.d) +{ +} + +ImapInterval::ImapInterval(Id begin, Id end) + : d(new Private) +{ + d->begin = begin; + d->end = end; +} + +ImapInterval::~ImapInterval() +{ +} + +ImapInterval &ImapInterval::operator=(const ImapInterval &other) +{ + if (this != &other) { + d = other.d; + } + + return *this; +} + +bool ImapInterval::operator==(const ImapInterval &other) const +{ + return (d->begin == other.d->begin && d->end == other.d->end); +} + +ImapInterval::Id ImapInterval::size() const +{ + if (!d->begin && !d->end) { + return 0; + } + + return (d->end - d->begin + 1); +} + +bool ImapInterval::hasDefinedBegin() const +{ + return (d->begin != 0); +} + +ImapInterval::Id ImapInterval::begin() const +{ + return d->begin; +} + +bool ImapInterval::hasDefinedEnd() const +{ + return (d->end != 0); +} + +ImapInterval::Id ImapInterval::end() const +{ + if (hasDefinedEnd()) { + return d->end; + } + + return std::numeric_limits::max(); +} + +void ImapInterval::setBegin(Id value) +{ + Q_ASSERT(value >= 0); + Q_ASSERT(value <= d->end || !hasDefinedEnd()); + d->begin = value; +} + +void ImapInterval::setEnd(Id value) +{ + Q_ASSERT(value >= 0); + Q_ASSERT(value >= d->begin || !hasDefinedBegin()); + d->end = value; +} + +QByteArray Akonadi::ImapInterval::toImapSequence() const +{ + if (size() == 0) { + return QByteArray(); + } + + if (size() == 1) { + return QByteArray::number(d->begin); + } + + QByteArray rv; + rv += QByteArray::number(d->begin) + ':'; + + if (hasDefinedEnd()) { + rv += QByteArray::number(d->end); + } else { + rv += '*'; + } + + return rv; +} + +ImapSet::ImapSet() + : d(new Private) +{ +} + +ImapSet::ImapSet(Id id) + : d(new Private) +{ + add(QVector() << id); +} + +ImapSet::ImapSet(const QVector &ids) + : d(new Private) +{ + add(ids); +} + +ImapSet::ImapSet(const ImapInterval &interval) + :d (new Private) +{ + add(interval); +} + +ImapSet::ImapSet(const ImapSet &other) + : d(other.d) +{ +} + +ImapSet::~ImapSet() +{ +} + +ImapSet ImapSet::all() +{ + ImapSet set; + set.add(ImapInterval(1, 0)); + return set; +} + +ImapSet &ImapSet::operator=(const ImapSet &other) +{ + if (this != &other) { + d = other.d; + } + + return *this; +} + +bool ImapSet::operator==(const ImapSet &other) const +{ + return d->intervals == other.d->intervals; +} + +void ImapSet::add(const QVector &values) +{ + QVector vals = values; + qSort(vals); + for (int i = 0; i < vals.count(); ++i) { + const int begin = vals[i]; + Q_ASSERT(begin >= 0); + if (i == vals.count() - 1) { + d->intervals << ImapInterval(begin, begin); + break; + } + do { + ++i; + Q_ASSERT(vals[i] >= 0); + if (vals[i] != (vals[i - 1] + 1)) { + --i; + break; + } + } while (i < vals.count() - 1); + d->intervals << ImapInterval(begin, vals[i]); + } +} + +void ImapSet::add(const QSet &values) +{ + QVector v; + v.reserve(values.size()); + for (QSet::ConstIterator iter = values.constBegin(); iter != values.constEnd(); ++iter) { + v.push_back(*iter); + } + + add(v); +} + +void ImapSet::add(const ImapInterval &interval) +{ + d->intervals << interval; +} + +QByteArray ImapSet::toImapSequenceSet() const +{ + QByteArray rv; + for (auto iter = d->intervals.cbegin(), end = d->intervals.cend(); iter != end; ++iter) { + if (iter != d->intervals.cbegin()) { + rv += ","; + } + rv += iter->toImapSequence(); + } + + return rv; +} + +ImapInterval::List ImapSet::intervals() const +{ + return d->intervals; +} + +bool ImapSet::isEmpty() const +{ + return d->intervals.isEmpty() || (d->intervals.size() == 1 && d->intervals.at(0).size() == 0); +} + +Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::ImapInterval &interval) +{ + return stream << interval.d->begin + << interval.d->end; +} + +Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::ImapInterval &interval) +{ + return stream >> interval.d->begin + >> interval.d->end; +} + +Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::ImapSet &set) +{ + return stream << set.d->intervals; +} + +Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::ImapSet &set) +{ + return stream >> set.d->intervals; +} + +} // namespace Akonadi + +using namespace Akonadi; + + +QDebug operator<<(QDebug d, const Akonadi::ImapInterval &interval) +{ + d << interval.toImapSequence(); + return d; +} + +QDebug operator<<(QDebug d, const Akonadi::ImapSet &set) +{ + d << set.toImapSequenceSet(); + return d; +} diff --git a/src/private/imapset_p.h b/src/private/imapset_p.h new file mode 100644 index 0000000..d707fec --- /dev/null +++ b/src/private/imapset_p.h @@ -0,0 +1,239 @@ +/* + 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_IMAPSET_P_H +#define AKONADI_IMAPSET_P_H + +#include "akonadiprivate_export.h" + +#include +#include +#include +#include +#include + +namespace Akonadi { +namespace Protocol { +class DataStream; +} +} + +namespace Akonadi { + +/** + Represents a single interval in an ImapSet. + This class is implicitly shared. +*/ +class AKONADIPRIVATE_EXPORT ImapInterval +{ +public: + /** + * Describes the ids stored in the interval. + */ + typedef qint64 Id; + + /** + A list of ImapInterval objects. + */ + typedef QList List; + + /** + Constructs an interval that covers all positive numbers. + */ + ImapInterval(); + + /** + Copy constructor. + */ + ImapInterval(const ImapInterval &other); + + /** + Create a new interval. + @param begin The begin of the interval. + @param end Keep default (0) to just set the interval begin + */ + explicit ImapInterval(Id begin, Id end = 0); + + /** + Destructor. + */ + ~ImapInterval(); + + /** + Assignment operator. + */ + ImapInterval &operator=(const ImapInterval &other); + + /** + Comparison operator. + */ + bool operator==(const ImapInterval &other) const; + + /** + Returns the size of this interval. + Size is only defined for finite intervals. + */ + Id size() const; + + /** + Returns true if this interval has a defined begin. + */ + bool hasDefinedBegin() const; + + /** + Returns the begin of this interval. The value is the smallest value part of the interval. + Only valid if begin is defined. + */ + Id begin() const; + + /** + Returns true if this intercal has been defined. + */ + bool hasDefinedEnd() const; + + /** + Returns the end of this interval. This value is the largest value part of the interval. + Only valid if hasDefinedEnd() returned true. + */ + Id end() const; + + /** + Sets the begin of the interval. + */ + void setBegin(Id value); + + /** + Sets the end of this interval. + */ + void setEnd(Id value); + + /** + Converts this set into an IMAP compatible sequence. + */ + QByteArray toImapSequence() const; + +private: + class Private; + QSharedDataPointer d; + + friend Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::ImapInterval &interval); + friend Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::ImapInterval &interval); +}; + +/** + Represents a set of natural numbers (1->\f$\infty\f$) in a as compact as possible form. + Used to address Akonadi items via the IMAP protocol or in the database. + This class is implicitly shared. +*/ +class AKONADIPRIVATE_EXPORT ImapSet +{ +public: + /** + * Describes the ids stored in the set. + */ + typedef qint64 Id; + + /** + Constructs an empty set. + */ + ImapSet(); + + ImapSet(qint64 Id); + + ImapSet(const QVector &ids); + + ImapSet(const ImapInterval &interval); + + /** + Copy constructor. + */ + ImapSet(const ImapSet &other); + + /** + Destructor. + */ + ~ImapSet(); + + /** + * Returns ImapSet representing 1:* + * */ + static ImapSet all(); + + /** + Assignment operator. + */ + ImapSet &operator=(const ImapSet &other); + + bool operator==(const ImapSet &other) const; + + /** + Adds the given list of positive integer numbers to the set. + The list is sorted and split into as large as possible intervals. + No interval merging is performed. + @param values List of positive integer numbers in arbitrary order + */ + void add(const QVector &values); + + /** + * @overload + */ + void add(const QSet &values); + + /** + Adds the given ImapInterval to this set. + No interval merging is performed. + */ + void add(const ImapInterval &interval); + + /** + Returns a IMAP-compatible QByteArray representation of this set. + */ + QByteArray toImapSequenceSet() const; + + /** + Returns the intervals this set consists of. + */ + ImapInterval::List intervals() const; + + /** + Returns true if this set doesn't contains any values. + */ + bool isEmpty() const; + +private: + class Private; + QSharedDataPointer d; + + friend Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::ImapSet &set); + friend Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::ImapSet &set); +}; + +} + +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug d, const Akonadi::ImapInterval &interval); +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug d, const Akonadi::ImapSet &set); + +Q_DECLARE_TYPEINFO(Akonadi::ImapInterval, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(Akonadi::ImapSet, Q_MOVABLE_TYPE); + +Q_DECLARE_METATYPE(Akonadi::ImapInterval) +Q_DECLARE_METATYPE(Akonadi::ImapInterval::List) +Q_DECLARE_METATYPE(Akonadi::ImapSet) + +#endif diff --git a/src/private/instance.cpp b/src/private/instance.cpp new file mode 100644 index 0000000..fa51d36 --- /dev/null +++ b/src/private/instance.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "instance_p.h" + +#include + +using namespace Akonadi; + +QString Instance::sIdentifier = QString(); + +void Instance::loadIdentifier() +{ + sIdentifier = QString::fromUtf8(qgetenv("AKONADI_INSTANCE")); + if (sIdentifier.isNull()) { + // QString is null by default, which means it wasn't initialized + // yet. Set it to empty when it is initialized + sIdentifier = QStringLiteral(""); + } +} + +bool Instance::hasIdentifier() +{ + if (sIdentifier.isNull()) { + loadIdentifier(); + } + return !sIdentifier.isEmpty(); +} + +void Instance::setIdentifier(const QString &identifier) +{ + if (identifier.isNull()) { + qunsetenv("AKONADI_INSTANCE"); + sIdentifier = QStringLiteral(""); + } else { + sIdentifier = identifier; + qputenv("AKONADI_INSTANCE", identifier.toUtf8()); + } +} + +QString Instance::identifier() +{ + if (sIdentifier.isNull()) { + loadIdentifier(); + } + return sIdentifier; +} diff --git a/src/private/instance_p.h b/src/private/instance_p.h new file mode 100644 index 0000000..dd162bf --- /dev/null +++ b/src/private/instance_p.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef AKONADI_INSTANCE_H +#define AKONADI_INSTANCE_H + +#include "akonadiprivate_export.h" + +class QString; + +namespace Akonadi { + +class AKONADIPRIVATE_EXPORT Instance +{ +public: + static bool hasIdentifier(); + static void setIdentifier(const QString &identifier); + static QString identifier(); + +private: + static void loadIdentifier(); + + static QString sIdentifier; +}; +} + +#endif // AKONADI_INSTANCE_H diff --git a/src/private/protocol.cpp b/src/private/protocol.cpp new file mode 100644 index 0000000..b157d91 --- /dev/null +++ b/src/private/protocol.cpp @@ -0,0 +1,8611 @@ +/* + * Copyright (c) 2015 Daniel Vrátil + * + * 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 "protocol_p.h" +#include "scope_p.h" +#include "imapset_p.h" +#include "datastream_p_p.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#undef AKONADI_DECLARE_PRIVATE +#define AKONADI_DECLARE_PRIVATE(Class) \ +inline Class##Private* Class::d_func() {\ + return reinterpret_cast(d_ptr.data()); \ +} \ +inline const Class##Private* Class::d_func() const {\ + return reinterpret_cast(d_ptr.constData()); \ +} + +#define COMPARE(prop) \ + (prop == ((decltype(this)) other)->prop) + +namespace Akonadi { +namespace Protocol { + +int version() { + return 53; +} + +} +} + +QDebug operator<<(QDebug _dbg, Akonadi::Protocol::Command::Type type) +{ + QDebug dbg(_dbg.noquote()); + + switch (type) + { + case Akonadi::Protocol::Command::Invalid: + return dbg << "Invalid"; + + case Akonadi::Protocol::Command::Hello: + return dbg << "Hello"; + case Akonadi::Protocol::Command::Login: + return dbg << "Login"; + case Akonadi::Protocol::Command::Logout: + return dbg << "Logout"; + + case Akonadi::Protocol::Command::Transaction: + return dbg << "Transaction"; + + case Akonadi::Protocol::Command::CreateItem: + return dbg << "CreateItem"; + case Akonadi::Protocol::Command::CopyItems: + return dbg << "CopyItems"; + case Akonadi::Protocol::Command::DeleteItems: + return dbg << "DeleteItems"; + case Akonadi::Protocol::Command::FetchItems: + return dbg << "FetchItems"; + case Akonadi::Protocol::Command::LinkItems: + return dbg << "LinkItems"; + case Akonadi::Protocol::Command::ModifyItems: + return dbg << "ModifyItems"; + case Akonadi::Protocol::Command::MoveItems: + return dbg << "MoveItems"; + + case Akonadi::Protocol::Command::CreateCollection: + return dbg << "CreateCollection"; + case Akonadi::Protocol::Command::CopyCollection: + return dbg << "CopyCollection"; + case Akonadi::Protocol::Command::DeleteCollection: + return dbg << "DeleteCollection"; + case Akonadi::Protocol::Command::FetchCollections: + return dbg << "FetchCollections"; + case Akonadi::Protocol::Command::FetchCollectionStats: + return dbg << "FetchCollectionStats"; + case Akonadi::Protocol::Command::ModifyCollection: + return dbg << "ModifyCollection"; + case Akonadi::Protocol::Command::MoveCollection: + return dbg << "MoveCollection"; + + case Akonadi::Protocol::Command::Search: + return dbg << "Search"; + case Akonadi::Protocol::Command::SearchResult: + return dbg << "SearchResult"; + case Akonadi::Protocol::Command::StoreSearch: + return dbg << "StoreSearch"; + + case Akonadi::Protocol::Command::CreateTag: + return dbg << "CreateTag"; + case Akonadi::Protocol::Command::DeleteTag: + return dbg << "DeleteTag"; + case Akonadi::Protocol::Command::FetchTags: + return dbg << "FetchTags"; + case Akonadi::Protocol::Command::ModifyTag: + return dbg << "ModifyTag"; + + case Akonadi::Protocol::Command::FetchRelations: + return dbg << "FetchRelations"; + case Akonadi::Protocol::Command::ModifyRelation: + return dbg << "ModifyRelation"; + case Akonadi::Protocol::Command::RemoveRelations: + return dbg << "RemoveRelations"; + + case Akonadi::Protocol::Command::SelectResource: + return dbg << "SelectResource"; + + case Akonadi::Protocol::Command::StreamPayload: + return dbg << "StreamPayload"; + case Akonadi::Protocol::Command::ChangeNotification: + return dbg << "ChangeNotification"; + + case Akonadi::Protocol::Command::_ResponseBit: + Q_ASSERT(false); + return dbg; + } + + Q_ASSERT(false); + return dbg; +} + +namespace Akonadi +{ +namespace Protocol +{ + +class DebugBlock +{ +public: + DebugBlock(QDebug &dbg) + : mIndent(0) + , mDbg(dbg) + { + beginBlock(); + } + + ~DebugBlock() + { + endBlock(); + } + + void beginBlock(const QByteArray &name = QByteArray()) + { + mDbg << "\n"; + if (!name.isNull()) { + mBlocks.push(name.size() + 4); + mDbg << QStringLiteral(" ").repeated(mIndent) << name << ": { "; + mIndent += mBlocks.top(); + } else { + mBlocks.push(2); + mDbg << QStringLiteral(" ").repeated(mIndent) << "{ "; + } + mBlockInit.push(false); + } + + void endBlock() + { + mDbg << " }"; + mIndent -= mBlocks.pop(); + mBlockInit.pop(); + } + + template + void write(const char *name, const T &val) + { + if (mBlockInit.top()) { + mDbg.noquote() << QByteArray("\n"); + mDbg << QStringLiteral(" ").repeated(mIndent); + } else { + mBlockInit.top() = true; + } + + mDbg << name << ": \"" << val << "\""; + } + +private: + Q_DISABLE_COPY(DebugBlock) + QStack mBlocks; + QStack mBlockInit; + int mIndent; + QDebug &mDbg; +}; + +} +} + + +/******************************************************************************/ + + +namespace Akonadi +{ +namespace Protocol +{ + + +class CommandPrivate : public QSharedData +{ +public: + CommandPrivate(quint8 type) + : QSharedData() + , commandType(type) + {} + + CommandPrivate(const CommandPrivate &other) + : QSharedData(other) + , commandType(other.commandType) + {} + + virtual ~CommandPrivate() + {} + + virtual bool compare(const CommandPrivate *other) const + { + return typeid(*this) == typeid(*other) + && COMPARE(commandType); + } + + virtual DataStream &serialize(DataStream &stream) const + { + return stream << commandType; + } + + virtual DataStream &deserialize(DataStream &stream) + { + return stream >> commandType; + } + + virtual CommandPrivate *clone() const + { + return new CommandPrivate(*this); + } + + virtual void debugString(DebugBlock &blck) const + { + blck.write("Command", static_cast(commandType)); + } + + quint8 commandType; +}; + +} +} + +template <> +Akonadi::Protocol::CommandPrivate *QSharedDataPointer::clone() +{ + return d->clone(); +} + + +namespace Akonadi +{ +namespace Protocol +{ + + +AKONADI_DECLARE_PRIVATE(Command) + +Command::Command() + : d_ptr(new CommandPrivate(Invalid)) +{ +} + +Command::Command(CommandPrivate *dd) + : d_ptr(dd) +{ +} + +Command::Command(Command &&other) +{ + d_ptr.swap(other.d_ptr); +} + +Command::Command(const Command &other) + : d_ptr(other.d_ptr) +{ +} + +Command::~Command() +{ +} + +Command& Command::operator=(Command &&other) +{ + d_ptr.swap(other.d_ptr); + return *this; +} + +Command& Command::operator=(const Command &other) +{ + d_ptr = other.d_ptr; + return *this; +} + +bool Command::operator==(const Command &other) const +{ + return d_ptr == other.d_ptr || d_ptr->compare(other.d_ptr.constData()); +} + +bool Command::operator!=(const Command &other) const +{ + return d_ptr != other.d_ptr && !d_ptr->compare(other.d_ptr.constData()); +} + +Command::Type Command::type() const +{ + return static_cast(d_func()->commandType & ~_ResponseBit); +} + +bool Command::isValid() const +{ + return type() != Invalid; +} + +bool Command::isResponse() const +{ + return d_func()->commandType & _ResponseBit; +} + +QString Command::debugString() const +{ + QString out; + QDebug dbg(&out); + DebugBlock blck(dbg.noquote().nospace()); + d_func()->debugString(blck); + return out; +} + +QString Command::debugString(DebugBlock &blck) const +{ + d_func()->debugString(blck); + return QString(); +} + +DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::Command &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, Akonadi::Protocol::Command &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/******************************************************************************/ + + + + + +class ResponsePrivate : public CommandPrivate +{ +public: + ResponsePrivate(Command::Type type) + : CommandPrivate(type | Command::_ResponseBit) + , errorCode(0) + {} + + ResponsePrivate(const ResponsePrivate &other) + : CommandPrivate(other) + , errorMsg(other.errorMsg) + , errorCode(other.errorCode) + {} + + virtual ~ResponsePrivate() Q_DECL_OVERRIDE + {} + + virtual bool compare(const CommandPrivate *other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(errorMsg) + && COMPARE(errorCode); + } + + virtual DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << errorCode + << errorMsg; + } + + virtual DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> errorCode + >> errorMsg; + } + + virtual CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new ResponsePrivate(*this); + } + + virtual void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + blck.write("Response", static_cast(commandType & ~Command::_ResponseBit)); + blck.write("Error Code", errorCode); + blck.write("Error Msg", errorMsg); + } + + QString errorMsg; + int errorCode; +}; + + + + +AKONADI_DECLARE_PRIVATE(Response) + +Response::Response() + : Command(new ResponsePrivate(Protocol::Command::Invalid)) +{ +} + +Response::Response(ResponsePrivate *dd) + : Command(dd) +{ +} + +Response::Response(const Command &command) + : Command(command) +{ +} + +void Response::setError(int code, const QString &message) +{ + d_func()->errorCode = code; + d_func()->errorMsg = message; +} + +bool Response::isError() const +{ + return d_func()->errorCode; +} + +int Response::errorCode() const +{ + return d_func()->errorCode; +} + +QString Response::errorMessage() const +{ + return d_func()->errorMsg; +} + +DataStream &operator<<(DataStream &stream, const Response &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, Response &command) +{ + return command.d_func()->deserialize(stream); +} + + + + + +/******************************************************************************/ + + + + + +class FactoryPrivate +{ +public: + typedef Command (*CommandFactoryFunc)(); + typedef Response (*ResponseFactoryFunc)(); + + FactoryPrivate() + { + // Session management + registerType(); + registerType(); + registerType(); + + // Transactions + registerType(); + + // Items + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + + // Collections + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + + // Search + registerType(); + registerType(); + registerType(); + + // Tag + registerType(); + registerType(); + registerType(); + registerType(); + + // Relation + registerType(); + registerType(); + registerType(); + + // Resources + registerType(); + + // Other...? + registerType(); + registerType(); + } + + // clang has problem resolving the right qHash() overload for Command::Type, + // so use its underlying integer type instead + QHash::type, QPair> registrar; + +private: + template + static Command commandFactoryFunc() + { + return T(); + } + template + static Response responseFactoryFunc() + { + return T(); + } + + template + void registerType() { + CommandFactoryFunc cmdFunc = &commandFactoryFunc; + ResponseFactoryFunc respFunc = &responseFactoryFunc; + registrar.insert(T, qMakePair(cmdFunc, respFunc)); + } +}; + +Q_GLOBAL_STATIC(FactoryPrivate, sFactoryPrivate) + +Command Factory::command(Command::Type type) +{ + auto iter = sFactoryPrivate->registrar.constFind(type); + if (iter == sFactoryPrivate->registrar.constEnd()) { + return Command(); + } + return iter.value().first(); +} + +Response Factory::response(Command::Type type) +{ + auto iter = sFactoryPrivate->registrar.constFind(type); + if (iter == sFactoryPrivate->registrar.constEnd()) { + return Response(); + } + return iter.value().second(); +} + + + + + + +/******************************************************************************/ + + + +void serialize(QIODevice *device, const Command &command) +{ + DataStream stream(device); + stream << command; + +#if 0 + QLocalSocket *socket + if ((socket == qobject_cast(device))) { + socket->flush(); + } +#endif +} + +Command deserialize(QIODevice *device) +{ + DataStream stream(device); + + stream.waitForData(sizeof(Command::Type)); + Command::Type cmdType; + if (device->peek((char *) &cmdType, sizeof(Command::Type)) != sizeof(Command::Type)) { + throw ProtocolException("Failed to peek command type"); + } + + Command cmd; + if (cmdType & Command::_ResponseBit) { + cmd = Factory::response(Command::Type(cmdType & ~Command::_ResponseBit)); + } else { + cmd = Factory::command(cmdType); + } + + stream >> cmd; + return cmd; +} + + + + +/******************************************************************************/ + + + + +class FetchScopePrivate : public QSharedData +{ +public: + FetchScopePrivate() + : fetchFlags(FetchScope::None) + , ancestorDepth(Ancestor::NoAncestor) + {} + + FetchScope::FetchFlags fetchFlags; + QVector requestedParts; + QDateTime changedSince; + QSet tagFetchScope; + Ancestor::Depth ancestorDepth; +}; + + + + +FetchScope::FetchScope() + : d(new FetchScopePrivate) +{ +} + + +FetchScope::FetchScope(FetchScope &&other) +{ + d.swap(other.d); +} + +FetchScope::FetchScope(const FetchScope &other) + : d(other.d) +{ +} + +FetchScope::~FetchScope() +{ +} + +FetchScope &FetchScope::operator=(FetchScope &&other) +{ + d.swap(other.d); + return *this; +} + +FetchScope &FetchScope::operator=(const FetchScope &other) +{ + d = other.d; + return *this; +} + +bool FetchScope::operator==(const FetchScope &other) const +{ + return (d == other.d) + || (d->requestedParts == other.d->requestedParts + && d->changedSince == other.d->changedSince + && d->tagFetchScope == other.d->tagFetchScope + && d->ancestorDepth == other.d->ancestorDepth + && d->fetchFlags == other.d->fetchFlags); +} + +bool FetchScope::operator!=(const FetchScope &other) const +{ + return !(*this == other); +} + +void FetchScope::setRequestedParts(const QVector &requestedParts) +{ + d->requestedParts = requestedParts; +} + +QVector FetchScope::requestedParts() const +{ + return d->requestedParts; +} + +QVector FetchScope::requestedPayloads() const +{ + QVector rv; + std::copy_if(d->requestedParts.begin(), d->requestedParts.end(), + std::back_inserter(rv), + [](const QByteArray &ba) { return ba.startsWith("PLD:"); }); + return rv; +} + +void FetchScope::setChangedSince(const QDateTime &changedSince) +{ + d->changedSince = changedSince; +} + +QDateTime FetchScope::changedSince() const +{ + return d->changedSince; +} + +void FetchScope::setTagFetchScope(const QSet &tagFetchScope) +{ + d->tagFetchScope = tagFetchScope; +} + +QSet FetchScope::tagFetchScope() const +{ + return d->tagFetchScope; +} + +void FetchScope::setAncestorDepth(Ancestor::Depth depth) +{ + d->ancestorDepth = depth; +} + +Ancestor::Depth FetchScope::ancestorDepth() const +{ + return d->ancestorDepth; +} + +bool FetchScope::cacheOnly() const +{ + return d->fetchFlags & CacheOnly; +} + +bool FetchScope::checkCachedPayloadPartsOnly() const +{ + return d->fetchFlags & CheckCachedPayloadPartsOnly; +} +bool FetchScope::fullPayload() const +{ + return d->fetchFlags & FullPayload; +} +bool FetchScope::allAttributes() const +{ + return d->fetchFlags & AllAttributes; +} +bool FetchScope::fetchSize() const +{ + return d->fetchFlags & Size; +} +bool FetchScope::fetchMTime() const +{ + return d->fetchFlags & MTime; +} +bool FetchScope::fetchRemoteRevision() const +{ + return d->fetchFlags & RemoteRevision; +} +bool FetchScope::ignoreErrors() const +{ + return d->fetchFlags & IgnoreErrors; +} +bool FetchScope::fetchFlags() const +{ + return d->fetchFlags & Flags; +} +bool FetchScope::fetchRemoteId() const +{ + return d->fetchFlags & RemoteID; +} +bool FetchScope::fetchGID() const +{ + return d->fetchFlags & GID; +} +bool FetchScope::fetchTags() const +{ + return d->fetchFlags & Tags; +} +bool FetchScope::fetchRelations() const +{ + return d->fetchFlags & Relations; +} +bool FetchScope::fetchVirtualReferences() const +{ + return d->fetchFlags & VirtReferences; +} + +void FetchScope::setFetch(FetchFlags attributes, bool fetch) +{ + if (fetch) { + d->fetchFlags |= attributes; + if (attributes & FullPayload) { + if (!d->requestedParts.contains(AKONADI_PARAM_PLD_RFC822)) { + d->requestedParts << AKONADI_PARAM_PLD_RFC822; + } + } + } else { + d->fetchFlags &= ~attributes; + } +} + + +bool FetchScope::fetch(FetchFlags flags) const +{ + if (flags == None) { + return d->fetchFlags == None; + } else { + return d->fetchFlags & flags; + } +} + +void FetchScope::debugString(DebugBlock &blck) const +{ + blck.write("Fetch Flags", d->fetchFlags); + blck.write("Tag Fetch Scope", d->tagFetchScope); + blck.write("Changed Since", d->changedSince); + blck.write("Ancestor Depth", d->ancestorDepth); + blck.write("Requested Parts", d->requestedParts); +} + +DataStream &operator<<(DataStream &stream, const FetchScope &scope) +{ + return stream << scope.d->requestedParts + << scope.d->changedSince + << scope.d->tagFetchScope + << scope.d->ancestorDepth + << scope.d->fetchFlags; +} + +DataStream &operator>>(DataStream &stream, FetchScope &scope) +{ + return stream >> scope.d->requestedParts + >> scope.d->changedSince + >> scope.d->tagFetchScope + >> scope.d->ancestorDepth + >> scope.d->fetchFlags; +} + +/******************************************************************************/ + + + +class ScopeContextPrivate : public QSharedData +{ +public: + ScopeContextPrivate(ScopeContext::Type type = ScopeContext::Collection, const QVariant &ctx = QVariant()) + { + if (type == ScopeContext::Tag) { + tagCtx = ctx; + } else if (type == ScopeContext::Collection) { + collectionCtx = ctx; + } + } + + ScopeContextPrivate(const ScopeContextPrivate &other) + : QSharedData(other) + , collectionCtx(other.collectionCtx) + , tagCtx(other.tagCtx) + {} + + QVariant ctx(ScopeContext::Type type) const + { + switch (type) { + case ScopeContext::Collection: + return collectionCtx; + case ScopeContext::Tag: + return tagCtx; + case ScopeContext::Any: + return QVariant(); + } + return QVariant(); + } + + void setCtx(ScopeContext::Type type, const QVariant &val) + { + switch (type) { + case ScopeContext::Collection: + collectionCtx = val; + break; + case ScopeContext::Tag: + tagCtx = val; + break; + case ScopeContext::Any: + break; + } + } + + QVariant collectionCtx; + QVariant tagCtx; +}; + + +ScopeContext::ScopeContext() + : d(new ScopeContextPrivate) +{ +} + +ScopeContext::ScopeContext(Type type, qint64 id) + : d(new ScopeContextPrivate(type, id)) +{ +} + +ScopeContext::ScopeContext(Type type, const QString &rid) + : d(new ScopeContextPrivate(type, rid)) +{ +} + +ScopeContext::ScopeContext(const ScopeContext &other) + : d(other.d) +{ +} + +ScopeContext::ScopeContext(ScopeContext &&other) +{ + d.swap(other.d); +} + +ScopeContext::~ScopeContext() +{ +} + +ScopeContext &ScopeContext::operator=(const ScopeContext &other) +{ + d = other.d; + return *this; +} + +ScopeContext &ScopeContext::operator=(ScopeContext &&other) +{ + d.swap(other.d); + return *this; +} + +bool ScopeContext::operator==(const ScopeContext &other) const +{ + return d == other.d || + (d->collectionCtx == other.d->collectionCtx && + d->tagCtx == other.d->tagCtx); +} + +bool ScopeContext::operator!=(const ScopeContext &other) const +{ + return !(*this == other); +} + +bool ScopeContext::isEmpty() const +{ + return d->collectionCtx.isNull() && d->tagCtx.isNull(); +} + +void ScopeContext::setContext(Type type, qint64 id) +{ + d->setCtx(type, id); +} + +void ScopeContext::setContext(Type type, const QString &rid) +{ + d->setCtx(type, rid); +} + +void ScopeContext::clearContext(Type type) +{ + d->setCtx(type, QVariant()); +} + +bool ScopeContext::hasContextId(Type type) const +{ + return d->ctx(type).type() == QVariant::LongLong; +} + +qint64 ScopeContext::contextId(Type type) const +{ + return hasContextId(type) ? d->ctx(type).toLongLong() : 0; +} + +bool ScopeContext::hasContextRID(Type type) const +{ + return d->ctx(type).type() == QVariant::String; +} + +QString ScopeContext::contextRID(Type type) const +{ + return hasContextRID(type) ? d->ctx(type).toString() : QString(); +} + +void ScopeContext::debugString(DebugBlock &blck) const +{ + blck.write("Tag", d->tagCtx); + blck.write("Collection", d->collectionCtx); +} + +DataStream &operator<<(DataStream &stream, const ScopeContext &context) +{ + // We don't have a custom generic DataStream streaming operator for QVariant + // because it's very hard, esp. without access to QVariant private + // stuff, so we have have to decompose it manually here. + QVariant::Type vType = context.d->collectionCtx.type(); + stream << vType; + if (vType == QVariant::LongLong) { + stream << context.d->collectionCtx.toLongLong(); + } else if (vType == QVariant::String) { + stream << context.d->collectionCtx.toString(); + } + + vType = context.d->tagCtx.type(); + stream << vType; + if (vType == QVariant::LongLong) { + stream << context.d->tagCtx.toLongLong(); + } else if (vType == QVariant::String) { + stream << context.d->tagCtx.toString(); + } + + return stream; +} + +DataStream &operator>>(DataStream &stream, ScopeContext &context) +{ + QVariant::Type vType; + qint64 id; + QString rid; + + for (ScopeContext::Type type : { ScopeContext::Collection, ScopeContext::Tag }) { + stream >> vType; + if (vType == QVariant::LongLong) { + stream >> id; + context.setContext(type, id); + } else if (vType == QVariant::String) { + stream >> rid; + context.setContext(type, rid); + } + } + + return stream; +} + +#undef CTX + + +/******************************************************************************/ + + +class PartMetaDataPrivate : public QSharedData +{ +public: + PartMetaDataPrivate(const QByteArray &name = QByteArray(), qint64 size = 0, + int version = 0, bool external = false) + : QSharedData() + , name(name) + , size(size) + , version(version) + , external(external) + {} + + PartMetaDataPrivate(const PartMetaDataPrivate &other) + : QSharedData(other) + , name(other.name) + , size(other.size) + , version(other.version) + , external(other.external) + {} + + QByteArray name; + qint64 size; + int version; + bool external; +}; + + + + +PartMetaData::PartMetaData() + : d(new PartMetaDataPrivate) +{ +} + +PartMetaData::PartMetaData(const QByteArray &name, qint64 size, int version, bool external) + : d(new PartMetaDataPrivate(name, size, version, external)) +{ +} + +PartMetaData::PartMetaData(PartMetaData &&other) +{ + d.swap(other.d); +} + +PartMetaData::PartMetaData(const PartMetaData &other) +{ + d = other.d; +} + +PartMetaData::~PartMetaData() +{ +} + +PartMetaData &PartMetaData::operator=(PartMetaData &&other) +{ + d.swap(other.d); + return *this; +} + +PartMetaData &PartMetaData::operator=(const PartMetaData &other) +{ + d = other.d; + return *this; +} + +bool PartMetaData::operator==(const PartMetaData &other) const +{ + return (d == other.d) + || (d->name == other.d->name + && d->size == other.d->size + && d->version == other.d->version + && d->external == other.d->external); +} + +bool PartMetaData::operator!=(const PartMetaData &other) const +{ + return !(*this == other); +} + +bool PartMetaData::operator<(const PartMetaData &other) const +{ + return d->name < other.d->name; +} + +void PartMetaData::setName(const QByteArray &name) +{ + d->name = name; +} +QByteArray PartMetaData::name() const +{ + return d->name; +} + +void PartMetaData::setSize(qint64 size) +{ + d->size = size; +} +qint64 PartMetaData::size() const +{ + return d->size; +} + +void PartMetaData::setVersion(int version) +{ + d->version = version; +} +int PartMetaData::version() const +{ + return d->version; +} + +void PartMetaData::setIsExternal(bool external) +{ + d->external = external; +} +bool PartMetaData::isExternal() const +{ + return d->external; +} + +DataStream &operator<<(DataStream &stream, const PartMetaData &part) +{ + return stream << part.d->name + << part.d->size + << part.d->version + << part.d->external; +} + +DataStream &operator>>(DataStream &stream, PartMetaData &part) +{ + return stream >> part.d->name + >> part.d->size + >> part.d->version + >> part.d->external; +} + + + +/******************************************************************************/ + + + +class CachePolicyPrivate : public QSharedData +{ +public: + CachePolicyPrivate() + : syncOnDemand(false) + , inherit(true) + , interval(-1) + , cacheTimeout(-1) + {} + + bool syncOnDemand; + bool inherit; + QStringList localParts; + int interval; + int cacheTimeout; +}; + + + + +CachePolicy::CachePolicy() + : d(new CachePolicyPrivate) +{ +} + +CachePolicy::CachePolicy(CachePolicy &&other) +{ + d.swap(other.d); +} + +CachePolicy::CachePolicy(const CachePolicy &other) + : d(other.d) +{ +} + +CachePolicy::~CachePolicy() +{ +} + +CachePolicy &CachePolicy::operator=(CachePolicy &&other) +{ + d.swap(other.d); + return *this; +} + +CachePolicy &CachePolicy::operator=(const CachePolicy &other) +{ + d = other.d; + return *this; +} + +bool CachePolicy::operator==(const CachePolicy &other) const +{ + return (d == other.d) + || (d->localParts == other.d->localParts + && d->interval == other.d->interval + && d->cacheTimeout == other.d->cacheTimeout + && d->syncOnDemand == other.d->syncOnDemand + && d->inherit == other.d->inherit); +} + +bool CachePolicy::operator!=(const CachePolicy &other) const +{ + return !(*this == other); +} + +void CachePolicy::setInherit(bool inherit) +{ + d->inherit = inherit; +} +bool CachePolicy::inherit() const +{ + return d->inherit; +} + +void CachePolicy::setCheckInterval(int interval) +{ + d->interval = interval; +} +int CachePolicy::checkInterval() const +{ + return d->interval; +} + +void CachePolicy::setCacheTimeout(int timeout) +{ + d->cacheTimeout = timeout; +} +int CachePolicy::cacheTimeout() const +{ + return d->cacheTimeout; +} + +void CachePolicy::setSyncOnDemand(bool onDemand) +{ + d->syncOnDemand = onDemand; +} +bool CachePolicy::syncOnDemand() const +{ + return d->syncOnDemand; +} + +void CachePolicy::setLocalParts(const QStringList &localParts) +{ + d->localParts = localParts; +} +QStringList CachePolicy::localParts() const +{ + return d->localParts; +} + +void CachePolicy::debugString(DebugBlock &blck) const +{ + blck.write("Inherit", d->inherit); + blck.write("Interval", d->interval); + blck.write("Cache Timeout", d->cacheTimeout); + blck.write("Sync on Demand", d->syncOnDemand); + blck.write("Local Parts", d->localParts); +} + +DataStream &operator<<(DataStream &stream, const CachePolicy &policy) +{ + return stream << policy.d->inherit + << policy.d->interval + << policy.d->cacheTimeout + << policy.d->syncOnDemand + << policy.d->localParts; +} + +DataStream &operator>>(DataStream &stream, CachePolicy &policy) +{ + return stream >> policy.d->inherit + >> policy.d->interval + >> policy.d->cacheTimeout + >> policy.d->syncOnDemand + >> policy.d->localParts; +} + + + +/******************************************************************************/ + + + + +class AncestorPrivate : public QSharedData +{ +public: + AncestorPrivate(qint64 id = -1, const QString &remoteId = QString()) + : id(id) + , remoteId(remoteId) + {} + + qint64 id; + QString remoteId; + QString name; + Attributes attrs; +}; + + + + + +Ancestor::Ancestor() + : d(new AncestorPrivate) +{ +} + +Ancestor::Ancestor(qint64 id) + : d(new AncestorPrivate(id)) +{ +} + +Ancestor::Ancestor(qint64 id, const QString &remoteId) + : d(new AncestorPrivate(id, remoteId)) +{ +} + +Ancestor::Ancestor(Ancestor &&other) +{ + d.swap(other.d); +} + +Ancestor::Ancestor(const Ancestor &other) + : d(other.d) +{ +} + +Ancestor::~Ancestor() +{ +} + +Ancestor &Ancestor::operator=(Ancestor &&other) +{ + d.swap(other.d); + return *this; +} + +Ancestor &Ancestor::operator=(const Ancestor &other) +{ + d = other.d; + return *this; +} + +bool Ancestor::operator==(const Ancestor &other) const +{ + return (d == other.d) + || (d->id == other.d->id + && d->remoteId == other.d->remoteId + && d->name == other.d->name + && d->attrs == other.d->attrs); +} + +bool Ancestor::operator!=(const Ancestor &other) const +{ + return !(*this == other); +} + +void Ancestor::setId(qint64 id) +{ + d->id = id; +} +qint64 Ancestor::id() const +{ + return d->id; +} + +void Ancestor::setRemoteId(const QString &remoteId) +{ + d->remoteId = remoteId; +} +QString Ancestor::remoteId() const +{ + return d->remoteId; +} + +void Ancestor::setName(const QString &name) +{ + d->name = name; +} +QString Ancestor::name() const +{ + return d->name; +} + +void Ancestor::setAttributes(const Attributes &attributes) +{ + d->attrs = attributes; +} +Attributes Ancestor::attributes() const +{ + return d->attrs; +} + +void Ancestor::debugString(DebugBlock &blck) const +{ + blck.write("ID", d->id); + blck.write("Remote ID", d->remoteId); + blck.write("Name", d->name); + blck.write("Attributes", d->attrs); +} + +DataStream &operator<<(DataStream &stream, const Ancestor &ancestor) +{ + return stream << ancestor.d->id + << ancestor.d->remoteId + << ancestor.d->name + << ancestor.d->attrs; +} + +DataStream &operator>>(DataStream &stream, Ancestor &ancestor) +{ + return stream >> ancestor.d->id + >> ancestor.d->remoteId + >> ancestor.d->name + >> ancestor.d->attrs; +} + + + + +/******************************************************************************/ + + + + + +class HelloResponsePrivate : public ResponsePrivate +{ +public: + HelloResponsePrivate() + : ResponsePrivate(Command::Hello) + , protocol(0) + {} + HelloResponsePrivate(const QString &server, const QString &message, int protocol) + : ResponsePrivate(Command::Hello) + , server(server) + , message(message) + , protocol(protocol) + {} + + HelloResponsePrivate(const HelloResponsePrivate &other) + : ResponsePrivate(other) + , server(other.server) + , message(other.message) + , protocol(other.protocol) + {} + + bool compare(const CommandPrivate *other) const Q_DECL_OVERRIDE + { + return ResponsePrivate::compare(other) + && COMPARE(server) + && COMPARE(message) + && COMPARE(protocol); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return ResponsePrivate::serialize(stream) + << server + << message + << protocol; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return ResponsePrivate::deserialize(stream) + >> server + >> message + >> protocol; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + ResponsePrivate::debugString(blck); + blck.write("Server", server); + blck.write("Protocol Version", protocol); + blck.write("Message", message); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new HelloResponsePrivate(*this); + } + + QString server; + QString message; + int protocol; +}; + + + +#define checkCopyInvariant(_cmdType) \ + if (std::is_base_of::type>::value) { \ + assert(d_func()->commandType == Command::Invalid || d_func()->commandType == (_cmdType | Command::_ResponseBit)); \ + } else { \ + assert(d_func()->commandType == Command::Invalid || d_func()->commandType == _cmdType); \ + } + + +AKONADI_DECLARE_PRIVATE(HelloResponse) + +HelloResponse::HelloResponse(const QString &server, const QString &message, int protocol) + : Response(new HelloResponsePrivate(server, message, protocol)) +{ +} + +HelloResponse::HelloResponse() + : Response(new HelloResponsePrivate) +{ +} + +HelloResponse::HelloResponse(const Command &command) + : Response(command) +{ + checkCopyInvariant(Command::Hello); +} + +void HelloResponse::setServerName(const QString &server) +{ + d_func()->server = server; +} + +QString HelloResponse::serverName() const +{ + return d_func()->server; +} + +void HelloResponse::setMessage(const QString &message) +{ + d_func()->message = message; +} + +QString HelloResponse::message() const +{ + return d_func()->message; +} + +void HelloResponse::setProtocolVersion(int protocolVersion) +{ + d_func()->protocol = protocolVersion; +} + +int HelloResponse::protocolVersion() const +{ + return d_func()->protocol; +} + +DataStream &operator<<(DataStream &stream, const HelloResponse &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, HelloResponse &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/******************************************************************************/ + + + + +class LoginCommandPrivate : public CommandPrivate +{ +public: + LoginCommandPrivate(const QByteArray &sessionId = QByteArray(), + LoginCommand::SessionMode mode = LoginCommand::CommandMode) + : CommandPrivate(Command::Login) + , sessionId(sessionId) + , sessionMode(mode) + + {} + + LoginCommandPrivate(const LoginCommandPrivate &other) + : CommandPrivate(other) + , sessionId(other.sessionId) + , sessionMode(other.sessionMode) + {} + + bool compare(const CommandPrivate *other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(sessionId) + && COMPARE(sessionMode); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << sessionId + << sessionMode; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> sessionId + >> sessionMode; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Session ID", sessionId); + blck.write("Session mode", [this]() -> QString { + switch (sessionMode) { + case LoginCommand::CommandMode: + return QStringLiteral("CommandMode"); + case LoginCommand::NotificationBus: + return QStringLiteral("NotificationBus"); + } + Q_ASSERT(false); + return QString(); + }()); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new LoginCommandPrivate(*this); + } + + QByteArray sessionId; + LoginCommand::SessionMode sessionMode; +}; + + + + +AKONADI_DECLARE_PRIVATE(LoginCommand) + +LoginCommand::LoginCommand() + : Command(new LoginCommandPrivate) +{ +} + +LoginCommand::LoginCommand(const QByteArray &sessionId, SessionMode mode) + : Command(new LoginCommandPrivate(sessionId, mode)) +{ +} + +LoginCommand::LoginCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::Login); +} + +void LoginCommand::setSessionId(const QByteArray &sessionId) +{ + d_func()->sessionId = sessionId; +} + +QByteArray LoginCommand::sessionId() const +{ + return d_func()->sessionId; +} + +void LoginCommand::setSessionMode(SessionMode mode) +{ + d_func()->sessionMode = mode; +} + +LoginCommand::SessionMode LoginCommand::sessionMode() const +{ + return d_func()->sessionMode; +} + +DataStream &operator<<(DataStream &stream, const LoginCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, LoginCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/******************************************************************************/ + + + + +LoginResponse::LoginResponse() + : Response(new ResponsePrivate(Command::Login)) +{ +} + +LoginResponse::LoginResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::Login); +} + + + + +/******************************************************************************/ + + + + +LogoutCommand::LogoutCommand() + : Command(new CommandPrivate(Command::Logout)) +{ +} + +LogoutCommand::LogoutCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::Logout); +} + + + +/******************************************************************************/ + + + +LogoutResponse::LogoutResponse() + : Response(new ResponsePrivate(Command::Logout)) +{ +} + +LogoutResponse::LogoutResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::Logout); +} + + + + +/******************************************************************************/ + + + + +class TransactionCommandPrivate : public CommandPrivate +{ +public: + TransactionCommandPrivate(TransactionCommand::Mode mode = TransactionCommand::Invalid) + : CommandPrivate(Command::Transaction) + , mode(mode) + {} + + TransactionCommandPrivate(const TransactionCommandPrivate &other) + : CommandPrivate(other) + , mode(other.mode) + {} + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Mode", [this]() -> const char* { + switch (mode) { + case TransactionCommand::Begin: + return "Begin"; + case TransactionCommand::Commit: + return "Commit"; + case TransactionCommand::Rollback: + return "Rollback"; + default: + return "Invalid"; + } + }()); + } + + bool compare(const CommandPrivate *other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(mode); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << mode; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> mode; + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new TransactionCommandPrivate(*this); + } + + TransactionCommand::Mode mode; +}; + + + + +AKONADI_DECLARE_PRIVATE(TransactionCommand) + +TransactionCommand::TransactionCommand() + : Command(new TransactionCommandPrivate) +{ +} + +TransactionCommand::TransactionCommand(TransactionCommand::Mode mode) + : Command(new TransactionCommandPrivate(mode)) +{ +} + +TransactionCommand::TransactionCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::Transaction); +} + +void TransactionCommand::setMode(Mode mode) +{ + d_func()->mode = mode; +} + +TransactionCommand::Mode TransactionCommand::mode() const +{ + return d_func()->mode; +} + +DataStream &operator<<(DataStream &stream, const TransactionCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, TransactionCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/******************************************************************************/ + + + + +TransactionResponse::TransactionResponse() + : Response(new ResponsePrivate(Command::Transaction)) +{ +} + +TransactionResponse::TransactionResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::Transaction); +} + + + +/******************************************************************************/ + + + + + +class CreateItemCommandPrivate : public CommandPrivate +{ +public: + CreateItemCommandPrivate() + : CommandPrivate(Command::CreateItem) + , mergeMode(CreateItemCommand::None) + , itemSize(0) + {} + + CreateItemCommandPrivate(const CreateItemCommandPrivate &other) + : CommandPrivate(other) + , collection(other.collection) + , mimeType(other.mimeType) + , gid(other.gid) + , remoteId(other.remoteId) + , remoteRev(other.remoteRev) + , dateTime(other.dateTime) + , tags(other.tags) + , addedTags(other.addedTags) + , removedTags(other.removedTags) + , flags(other.flags) + , addedFlags(other.addedFlags) + , removedFlags(other.removedFlags) + , parts(other.parts) + , attributes(other.attributes) + , mergeMode(other.mergeMode) + , itemSize(other.itemSize) + {} + + bool compare(const CommandPrivate *other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(mergeMode) + && COMPARE(itemSize) + && COMPARE(collection) + && COMPARE(mimeType) + && COMPARE(gid) + && COMPARE(remoteId) + && COMPARE(remoteRev) + && COMPARE(dateTime) + && COMPARE(tags) + && COMPARE(addedTags) + && COMPARE(removedTags) + && COMPARE(flags) + && COMPARE(addedFlags) + && COMPARE(removedFlags) + && COMPARE(attributes) + && COMPARE(parts); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << mergeMode + << collection + << itemSize + << mimeType + << gid + << remoteId + << remoteRev + << dateTime + << flags + << addedFlags + << removedFlags + << tags + << addedTags + << removedTags + << attributes + << parts; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> mergeMode + >> collection + >> itemSize + >> mimeType + >> gid + >> remoteId + >> remoteRev + >> dateTime + >> flags + >> addedFlags + >> removedFlags + >> tags + >> addedTags + >> removedTags + >> attributes + >> parts; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Merge mode", [this]() { + QStringList mm; + if (mergeMode == CreateItemCommand::None) { + mm << QStringLiteral("None"); + } else { + if (mergeMode & CreateItemCommand::GID) { + mm << QStringLiteral("GID"); + } + if (mergeMode & CreateItemCommand::RemoteID) { + mm << QStringLiteral("Remote ID"); + } + if (mergeMode & CreateItemCommand::Silent) { + mm << QStringLiteral("Silent"); + } + } + return mm; + }()); + blck.write("Collection", collection); + blck.write("MimeType", mimeType); + blck.write("GID", gid); + blck.write("Remote ID", remoteId); + blck.write("Remote Revision", remoteRev); + blck.write("Size", itemSize); + blck.write("Time", dateTime); + blck.write("Tags", tags); + blck.write("Added Tags", addedTags); + blck.write("Removed Tags", removedTags); + blck.write("Flags", flags); + blck.write("Added Flags", addedFlags); + blck.write("Removed Flags", removedFlags); + blck.beginBlock("Attributes"); + for (auto iter = attributes.constBegin(); iter != attributes.constEnd(); ++iter) { + blck.write(iter.key().constData(), iter.value()); + } + blck.endBlock(); + blck.write("Parts", parts); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new CreateItemCommandPrivate(*this); + } + + Scope collection; + QString mimeType; + QString gid; + QString remoteId; + QString remoteRev; + QDateTime dateTime; + Scope tags; + Scope addedTags; + Scope removedTags; + QSet flags; + QSet addedFlags; + QSet removedFlags; + QSet parts; + Attributes attributes; + CreateItemCommand::MergeModes mergeMode; + qint64 itemSize; +}; + + + + +AKONADI_DECLARE_PRIVATE(CreateItemCommand) + +CreateItemCommand::CreateItemCommand() + : Command(new CreateItemCommandPrivate) +{ +} + +CreateItemCommand::CreateItemCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::CreateItem); +} + +void CreateItemCommand::setMergeModes(const MergeModes &mode) +{ + d_func()->mergeMode = mode; +} +CreateItemCommand::MergeModes CreateItemCommand::mergeModes() const +{ + return d_func()->mergeMode; +} + +void CreateItemCommand::setCollection(const Scope &collection) +{ + d_func()->collection = collection; +} +Scope CreateItemCommand::collection() const +{ + return d_func()->collection; +} + +void CreateItemCommand::setItemSize(qint64 size) +{ + d_func()->itemSize = size; +} +qint64 CreateItemCommand::itemSize() const +{ + return d_func()->itemSize; +} + +void CreateItemCommand::setMimeType(const QString &mimeType) +{ + d_func()->mimeType = mimeType; +} +QString CreateItemCommand::mimeType() const +{ + return d_func()->mimeType; +} + +void CreateItemCommand::setGID(const QString &gid) +{ + d_func()->gid = gid; +} +QString CreateItemCommand::gid() const +{ + return d_func()->gid; +} + +void CreateItemCommand::setRemoteId(const QString &remoteId) +{ + d_func()->remoteId = remoteId; +} +QString CreateItemCommand::remoteId() const +{ + return d_func()->remoteId; +} + +void CreateItemCommand::setRemoteRevision(const QString &remoteRevision) +{ + d_func()->remoteRev = remoteRevision; +} + +QString CreateItemCommand::remoteRevision() const +{ + return d_func()->remoteRev; +} + +void CreateItemCommand::setDateTime(const QDateTime &dateTime) +{ + d_func()->dateTime = dateTime; +} +QDateTime CreateItemCommand::dateTime() const +{ + return d_func()->dateTime; +} + +void CreateItemCommand::setFlags(const QSet &flags) +{ + d_func()->flags = flags; +} +QSet CreateItemCommand::flags() const +{ + return d_func()->flags; +} +void CreateItemCommand::setAddedFlags(const QSet &flags) +{ + d_func()->addedFlags = flags; +} +QSet CreateItemCommand::addedFlags() const +{ + return d_func()->addedFlags; +} +void CreateItemCommand::setRemovedFlags(const QSet &flags) +{ + d_func()->removedFlags = flags; +} +QSet CreateItemCommand::removedFlags() const +{ + return d_func()->removedFlags; +} + +void CreateItemCommand::setTags(const Scope &tags) +{ + d_func()->tags = tags; +} +Scope CreateItemCommand::tags() const +{ + return d_func()->tags; +} +void CreateItemCommand::setAddedTags(const Scope &tags) +{ + d_func()->addedTags = tags; +} +Scope CreateItemCommand::addedTags() const +{ + return d_func()->addedTags; +} +void CreateItemCommand::setRemovedTags(const Scope &tags) +{ + d_func()->removedTags = tags; +} +Scope CreateItemCommand::removedTags() const +{ + return d_func()->removedTags; +} +void CreateItemCommand::setAttributes(const Attributes &attrs) +{ + d_func()->attributes = attrs; +} +Attributes CreateItemCommand::attributes() const +{ + return d_func()->attributes; +} +void CreateItemCommand::setParts(const QSet &parts) +{ + d_func()->parts = parts; +} +QSet CreateItemCommand::parts() const +{ + return d_func()->parts; +} + +DataStream &operator<<(DataStream &stream, const CreateItemCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, CreateItemCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/******************************************************************************/ + + + + +CreateItemResponse::CreateItemResponse() + : Response(new ResponsePrivate(Command::CreateItem)) +{ +} + +CreateItemResponse::CreateItemResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::CreateItem); +} + + + + +/******************************************************************************/ + + + + +class CopyItemsCommandPrivate : public CommandPrivate +{ +public: + CopyItemsCommandPrivate() + : CommandPrivate(Command::CopyItems) + {} + CopyItemsCommandPrivate(const Scope &items, const Scope &dest) + : CommandPrivate(Command::CopyItems) + , items(items) + , dest(dest) + {} + CopyItemsCommandPrivate(const CopyItemsCommandPrivate &other) + : CommandPrivate(other) + , items(other.items) + , dest(other.dest) + {} + + bool compare(const CommandPrivate *other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(items) + && COMPARE(dest); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << items + << dest; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> items + >> dest; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Items", items); + blck.write("Destination", dest); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new CopyItemsCommandPrivate(*this); + } + + Scope items; + Scope dest; +}; + + + + +AKONADI_DECLARE_PRIVATE(CopyItemsCommand) + +CopyItemsCommand::CopyItemsCommand() + : Command(new CopyItemsCommandPrivate) +{ +} + +CopyItemsCommand::CopyItemsCommand(const Scope &items, const Scope &dest) + : Command(new CopyItemsCommandPrivate(items, dest)) +{ +} + +CopyItemsCommand::CopyItemsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::CopyItems); +} + +void CopyItemsCommand::setItems(const Scope &items) +{ + d_func()->items = items; +} + +Scope CopyItemsCommand::items() const +{ + return d_func()->items; +} + +void CopyItemsCommand::setDestination(const Scope &dest) +{ + d_func()->dest = dest; +} + +Scope CopyItemsCommand::destination() const +{ + return d_func()->dest; +} + +DataStream &operator<<(DataStream &stream, const CopyItemsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, CopyItemsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/******************************************************************************/ + + + + +CopyItemsResponse::CopyItemsResponse() + : Response(new ResponsePrivate(Command::CopyItems)) +{ +} + +CopyItemsResponse::CopyItemsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::CopyItems); +} + + + +/******************************************************************************/ + + + + +class DeleteItemsCommandPrivate : public CommandPrivate +{ +public: + DeleteItemsCommandPrivate(const Scope &items = Scope(), const ScopeContext &context = ScopeContext()) + : CommandPrivate(Command::DeleteItems) + , items(items) + , context(context) + {} + + bool compare(const CommandPrivate *other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(items) + && COMPARE(context); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << items + << context; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> items + >> context; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Items", items); + blck.beginBlock("Context"); + context.debugString(blck); + blck.endBlock(); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new DeleteItemsCommandPrivate(*this); + } + + Scope items; + ScopeContext context; +}; + + + + +AKONADI_DECLARE_PRIVATE(DeleteItemsCommand) + +DeleteItemsCommand::DeleteItemsCommand() + : Command(new DeleteItemsCommandPrivate) +{ +} + +DeleteItemsCommand::DeleteItemsCommand(const Scope &items, const ScopeContext &context) + : Command(new DeleteItemsCommandPrivate(items, context)) +{ +} + +DeleteItemsCommand::DeleteItemsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::DeleteItems); +} + +Scope DeleteItemsCommand::items() const +{ + return d_func()->items; +} + +ScopeContext DeleteItemsCommand::scopeContext() const +{ + return d_func()->context; +} + +DataStream &operator<<(DataStream &stream, const DeleteItemsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, DeleteItemsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/******************************************************************************/ + + + + +DeleteItemsResponse::DeleteItemsResponse() + : Response(new ResponsePrivate(Command::DeleteItems)) +{ +} + +DeleteItemsResponse::DeleteItemsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::DeleteItems); +} + + + + +/******************************************************************************/ + + + + + +class FetchRelationsCommandPrivate : public CommandPrivate +{ +public: + FetchRelationsCommandPrivate(qint64 left = -1, qint64 right = -1, qint64 side = -1, + const QVector &types = QVector(), + const QString &resource = QString()) + : CommandPrivate(Command::FetchRelations) + , left(left) + , right(right) + , side(side) + , types(types) + , resource(resource) + {} + FetchRelationsCommandPrivate(const FetchRelationsCommandPrivate &other) + : CommandPrivate(other) + , left(other.left) + , right(other.right) + , side(other.side) + , types(other.types) + , resource(other.resource) + {} + + bool compare(const CommandPrivate *other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(left) + && COMPARE(right) + && COMPARE(side) + && COMPARE(types) + && COMPARE(resource); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << left + << right + << side + << types + << resource; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> left + >> right + >> side + >> types + >> resource; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Left", left); + blck.write("Right", right); + blck.write("Side", side); + blck.write("Types", types); + blck.write("Resource", resource); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchRelationsCommandPrivate(*this); + } + + qint64 left; + qint64 right; + qint64 side; + QVector types; + QString resource; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchRelationsCommand) + +FetchRelationsCommand::FetchRelationsCommand() + : Command(new FetchRelationsCommandPrivate) +{ +} + +FetchRelationsCommand::FetchRelationsCommand(qint64 side, const QVector &types, + const QString &resource) + : Command(new FetchRelationsCommandPrivate(-1, -1, side, types, resource)) +{ +} + +FetchRelationsCommand::FetchRelationsCommand(qint64 left, qint64 right, + const QVector &types, + const QString &resource) + : Command(new FetchRelationsCommandPrivate(left, right, -1, types, resource)) +{ +} + +FetchRelationsCommand::FetchRelationsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::FetchRelations); +} + +void FetchRelationsCommand::setLeft(qint64 left) +{ + d_func()->left = left; +} +qint64 FetchRelationsCommand::left() const +{ + return d_func()->left; +} + +void FetchRelationsCommand::setRight(qint64 right) +{ + d_func()->right = right; +} +qint64 FetchRelationsCommand::right() const +{ + return d_func()->right; +} + +void FetchRelationsCommand::setSide(qint64 side) +{ + d_func()->side = side; +} +qint64 FetchRelationsCommand::side() const +{ + return d_func()->side; +} + +void FetchRelationsCommand::setTypes(const QVector &types) +{ + d_func()->types = types; +} +QVector FetchRelationsCommand::types() const +{ + return d_func()->types; +} + +void FetchRelationsCommand::setResource(const QString &resource) +{ + d_func()->resource = resource; +} +QString FetchRelationsCommand::resource() const +{ + return d_func()->resource; +} + +DataStream &operator<<(DataStream &stream, const FetchRelationsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchRelationsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/*****************************************************************************/ + + + + +class FetchRelationsResponsePrivate : public ResponsePrivate +{ +public: + FetchRelationsResponsePrivate(qint64 left = -1, const QByteArray &leftMimeType = QByteArray(), + qint64 right = -1, const QByteArray &rightMimeType = QByteArray(), + const QByteArray &type = QByteArray(), + const QByteArray &remoteId = QByteArray()) + : ResponsePrivate(Command::FetchRelations) + , left(left) + , leftMimeType(leftMimeType) + , right(right) + , rightMimeType(rightMimeType) + , type(type) + , remoteId(remoteId) + {} + FetchRelationsResponsePrivate(const FetchRelationsResponsePrivate &other) + : ResponsePrivate(other) + , left(other.left) + , leftMimeType(other.leftMimeType) + , right(other.right) + , rightMimeType(other.rightMimeType) + , type(other.type) + , remoteId(other.remoteId) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return ResponsePrivate::compare(other) + && COMPARE(left) + && COMPARE(leftMimeType) + && COMPARE(right) + && COMPARE(rightMimeType) + && COMPARE(type) + && COMPARE(remoteId); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return ResponsePrivate::serialize(stream) + << left + << leftMimeType + << right + << rightMimeType + << type + << remoteId; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return ResponsePrivate::deserialize(stream) + >> left + >> leftMimeType + >> right + >> rightMimeType + >> type + >> remoteId; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + ResponsePrivate::debugString(blck); + blck.write("Left", left); + blck.write("LeftMimeType", leftMimeType); + blck.write("Right", right); + blck.write("RightMimeType", rightMimeType); + blck.write("Type", type); + blck.write("Remote ID", remoteId); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchRelationsResponsePrivate(*this); + } + + qint64 left; + QByteArray leftMimeType; + qint64 right; + QByteArray rightMimeType; + QByteArray type; + QByteArray remoteId; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchRelationsResponse) + +FetchRelationsResponse::FetchRelationsResponse() + : Response(new FetchRelationsResponsePrivate) +{ +} + +FetchRelationsResponse::FetchRelationsResponse(qint64 left, const QByteArray &leftMimeType, + qint64 right, const QByteArray &rightMimeType, + const QByteArray &type, + const QByteArray &remoteId) + : Response(new FetchRelationsResponsePrivate(left, leftMimeType, right, rightMimeType, type, remoteId)) +{ +} + +FetchRelationsResponse::FetchRelationsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::FetchRelations); +} + +qint64 FetchRelationsResponse::left() const +{ + return d_func()->left; +} +QByteArray FetchRelationsResponse::leftMimeType() const +{ + return d_func()->leftMimeType; +} +qint64 FetchRelationsResponse::right() const +{ + return d_func()->right; +} +QByteArray FetchRelationsResponse::rightMimeType() const +{ + return d_func()->rightMimeType; +} +QByteArray FetchRelationsResponse::type() const +{ + return d_func()->type; +} +void FetchRelationsResponse::setRemoteId(const QByteArray &remoteId) +{ + d_func()->remoteId = remoteId; +} +QByteArray FetchRelationsResponse::remoteId() const +{ + return d_func()->remoteId; +} + +DataStream &operator<<(DataStream &stream, const FetchRelationsResponse &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchRelationsResponse &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/******************************************************************************/ + + + + +class FetchTagsCommandPrivate : public CommandPrivate +{ +public: + FetchTagsCommandPrivate(const Scope &scope = Scope()) + : CommandPrivate(Command::FetchTags) + , scope(scope) + , idOnly(false) + {} + FetchTagsCommandPrivate(const FetchTagsCommandPrivate &other) + : CommandPrivate(other) + , scope(other.scope) + , attributes(other.attributes) + , idOnly(other.idOnly) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(idOnly) + && COMPARE(scope) + && COMPARE(attributes); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << scope + << attributes + << idOnly; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> scope + >> attributes + >> idOnly; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Tags", scope); + blck.write("Attributes", attributes); + blck.write("ID only", idOnly); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchTagsCommandPrivate(*this); + } + + Scope scope; + QSet attributes; + bool idOnly; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchTagsCommand) + +FetchTagsCommand::FetchTagsCommand() + : Command(new FetchTagsCommandPrivate) +{ +} + +FetchTagsCommand::FetchTagsCommand(const Scope &scope) + : Command(new FetchTagsCommandPrivate(scope)) +{ +} + +FetchTagsCommand::FetchTagsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::FetchTags); +} + +Scope FetchTagsCommand::scope() const +{ + return d_func()->scope; +} + +void FetchTagsCommand::setAttributes(const QSet &attributes) +{ + d_func()->attributes = attributes; +} +QSet FetchTagsCommand::attributes() const +{ + return d_func()->attributes; +} + +void FetchTagsCommand::setIdOnly(bool idOnly) +{ + d_func()->idOnly = idOnly; +} +bool FetchTagsCommand::idOnly() const +{ + return d_func()->idOnly; +} + +DataStream &operator<<(DataStream &stream, const FetchTagsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchTagsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/*****************************************************************************/ + + + +class FetchTagsResponsePrivate : public ResponsePrivate +{ +public: + FetchTagsResponsePrivate(qint64 id = -1, const QByteArray &gid = QByteArray(), + const QByteArray &type = QByteArray(), + const QByteArray &remoteId = QByteArray(), + qint64 parentId = -1, + const Attributes &attrs = Attributes()) + : ResponsePrivate(Command::FetchTags) + , id(id) + , parentId(parentId) + , gid(gid) + , type(type) + , remoteId(remoteId) + , attributes(attrs) + {} + FetchTagsResponsePrivate(const FetchTagsResponsePrivate &other) + : ResponsePrivate(other) + , id(other.id) + , parentId(other.parentId) + , gid(other.gid) + , type(other.type) + , remoteId(other.remoteId) + , attributes(other.attributes) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return ResponsePrivate::compare(other) + && COMPARE(id) + && COMPARE(parentId) + && COMPARE(gid) + && COMPARE(type) + && COMPARE(remoteId) + && COMPARE(attributes); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return ResponsePrivate::serialize(stream) + << id + << parentId + << gid + << type + << remoteId + << attributes; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return ResponsePrivate::deserialize(stream) + >> id + >> parentId + >> gid + >> type + >> remoteId + >> attributes; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + ResponsePrivate::debugString(blck); + blck.write("ID", id); + blck.write("Parent ID", parentId); + blck.write("GID", gid); + blck.write("Type", type); + blck.write("Remote ID", remoteId); + blck.write("Attributes", attributes); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchTagsResponsePrivate(*this); + } + + qint64 id; + qint64 parentId; + QByteArray gid; + QByteArray type; + QByteArray remoteId; + Attributes attributes; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchTagsResponse) + +FetchTagsResponse::FetchTagsResponse() + : Response(new FetchTagsResponsePrivate) +{ +} + +FetchTagsResponse::FetchTagsResponse(qint64 id) + : Response(new FetchTagsResponsePrivate(id)) +{ +} + +FetchTagsResponse::FetchTagsResponse(qint64 id, const QByteArray &gid, const QByteArray &type, + const QByteArray &remoteId, + qint64 parentId, const Attributes &attrs) + : Response(new FetchTagsResponsePrivate(id, gid, type, remoteId, parentId, attrs)) +{ +} + +FetchTagsResponse::FetchTagsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::FetchTags); +} + +qint64 FetchTagsResponse::id() const +{ + return d_func()->id; +} + +void FetchTagsResponse::setParentId(qint64 parentId) +{ + d_func()->parentId = parentId; +} +qint64 FetchTagsResponse::parentId() const +{ + return d_func()->parentId; +} + +void FetchTagsResponse::setGid(const QByteArray &gid) +{ + d_func()->gid = gid; +} +QByteArray FetchTagsResponse::gid() const +{ + return d_func()->gid; +} + +void FetchTagsResponse::setType(const QByteArray &type) +{ + d_func()->type = type; +} +QByteArray FetchTagsResponse::type() const +{ + return d_func()->type; +} + +void FetchTagsResponse::setRemoteId(const QByteArray &remoteId) +{ + d_func()->remoteId = remoteId; +} +QByteArray FetchTagsResponse::remoteId() const +{ + return d_func()->remoteId; +} + +void FetchTagsResponse::setAttributes(const Attributes &attributes) +{ + d_func()->attributes = attributes; +} +Attributes FetchTagsResponse::attributes() const +{ + return d_func()->attributes; +} + +DataStream &operator<<(DataStream &stream, const FetchTagsResponse &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchTagsResponse &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/*****************************************************************************/ + + + + +class FetchItemsCommandPrivate : public CommandPrivate +{ +public: + FetchItemsCommandPrivate(const Scope &scope = Scope(), + const ScopeContext &context = ScopeContext(), + const FetchScope &fetchScope = FetchScope()) + : CommandPrivate(Command::FetchItems) + , scope(scope) + , scopeContext(context) + , fetchScope(fetchScope) + {} + + FetchItemsCommandPrivate(const FetchItemsCommandPrivate &other) + : CommandPrivate(other) + , scope(other.scope) + , scopeContext(other.scopeContext) + , fetchScope(other.fetchScope) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(scope) + && COMPARE(scopeContext) + && COMPARE(fetchScope); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << scope + << scopeContext + << fetchScope; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> scope + >> scopeContext + >> fetchScope; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Items", scope); + blck.beginBlock("Scope Context"); + scopeContext.debugString(blck); + blck.endBlock(); + blck.beginBlock("Fetch Scope"); + fetchScope.debugString(blck); + blck.endBlock(); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchItemsCommandPrivate(*this); + } + + Scope scope; + ScopeContext scopeContext; + FetchScope fetchScope; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchItemsCommand) + +FetchItemsCommand::FetchItemsCommand() + : Command(new FetchItemsCommandPrivate) +{ +} + +FetchItemsCommand::FetchItemsCommand(const Scope &scope, const FetchScope &fetchScope) + : Command(new FetchItemsCommandPrivate(scope, ScopeContext(), fetchScope)) +{ +} + +FetchItemsCommand::FetchItemsCommand(const Scope &scope, const ScopeContext &context, + const FetchScope &fetchScope) + : Command(new FetchItemsCommandPrivate(scope, context, fetchScope)) +{ +} + +FetchItemsCommand::FetchItemsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::FetchItems); +} + +Scope FetchItemsCommand::scope() const +{ + return d_func()->scope; +} + +ScopeContext FetchItemsCommand::scopeContext() const +{ + return d_func()->scopeContext; +} + +FetchScope FetchItemsCommand::fetchScope() const +{ + return d_func()->fetchScope; +} + +FetchScope &FetchItemsCommand::fetchScope() +{ + return d_func()->fetchScope; +} + +DataStream &operator<<(DataStream &stream, const FetchItemsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchItemsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/****************************************************************************/ + + + + + +class FetchItemsResponsePrivate : public ResponsePrivate +{ +public: + FetchItemsResponsePrivate(qint64 id = -1) + : ResponsePrivate(Command::FetchItems) + , id(id) + , collectionId(-1) + , size(0) + , revision(0) + {} + FetchItemsResponsePrivate(const FetchItemsResponsePrivate &other) + : ResponsePrivate(other) + , remoteId(other.remoteId) + , remoteRev(other.remoteRev) + , gid(other.gid) + , mimeType(other.mimeType) + , time(other.time) + , flags(other.flags) + , tags(other.tags) + , virtRefs(other.virtRefs) + , relations(other.relations) + , ancestors(other.ancestors) + , parts(other.parts) + , cachedParts(other.cachedParts) + , id(other.id) + , collectionId(other.collectionId) + , size(other.size) + , revision(other.revision) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return ResponsePrivate::compare(other) + && COMPARE(id) + && COMPARE(collectionId) + && COMPARE(size) + && COMPARE(revision) + && COMPARE(remoteId) + && COMPARE(remoteRev) + && COMPARE(gid) + && COMPARE(mimeType) + && COMPARE(time) + && COMPARE(flags) + && COMPARE(tags) + && COMPARE(virtRefs) + && COMPARE(relations) + && COMPARE(ancestors) + && COMPARE(parts) + && COMPARE(cachedParts); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return ResponsePrivate::serialize(stream) + << id + << revision + << collectionId + << remoteId + << remoteRev + << gid + << size + << mimeType + << time + << flags + << tags + << virtRefs + << relations + << ancestors + << parts + << cachedParts; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return ResponsePrivate::deserialize(stream) + >> id + >> revision + >> collectionId + >> remoteId + >> remoteRev + >> gid + >> size + >> mimeType + >> time + >> flags + >> tags + >> virtRefs + >> relations + >> ancestors + >> parts + >> cachedParts; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + ResponsePrivate::debugString(blck); + blck.write("ID", id); + blck.write("Revision", revision); + blck.write("Collection ID", collectionId); + blck.write("Remote ID", remoteId); + blck.write("Remote Revision", remoteRev); + blck.write("GID", gid); + blck.write("Size", size); + blck.write("Mimetype", mimeType); + blck.write("Time", time); + blck.write("Flags", flags); + blck.beginBlock("Tags"); + Q_FOREACH (const FetchTagsResponse &tag, tags) { + blck.beginBlock(); + tag.debugString(blck); + blck.endBlock(); + } + blck.endBlock(); + blck.beginBlock("Relations"); + Q_FOREACH (const FetchRelationsResponse &rel, relations) { + blck.beginBlock(); + rel.debugString(blck); + blck.endBlock(); + } + blck.endBlock(); + blck.write("Virtual References", virtRefs); + blck.beginBlock("Ancestors"); + Q_FOREACH (const Ancestor &anc, ancestors) { + blck.beginBlock(); + anc.debugString(blck); + blck.endBlock(); + } + blck.endBlock(); + blck.write("Cached Parts", cachedParts); + blck.beginBlock("Parts"); + Q_FOREACH (const StreamPayloadResponse &part, parts) { + blck.beginBlock(part.payloadName()); + blck.write("Size", part.metaData().size()); + blck.write("External", part.metaData().isExternal()); + blck.write("Version", part.metaData().version()); + blck.write("Data", part.data()); + blck.endBlock(); + } + blck.endBlock(); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchItemsResponsePrivate(*this); + } + + QString remoteId; + QString remoteRev; + QString gid; + QString mimeType; + QDateTime time; + QVector flags; + QVector tags; + QVector virtRefs; + QVector relations; + QVector ancestors; + QVector parts; + QVector cachedParts; + qint64 id; + qint64 collectionId; + qint64 size; + int revision; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchItemsResponse) + +FetchItemsResponse::FetchItemsResponse() + : Response(new FetchItemsResponsePrivate) +{ +} + +FetchItemsResponse::FetchItemsResponse(qint64 id) + : Response(new FetchItemsResponsePrivate(id)) +{ +} + +FetchItemsResponse::FetchItemsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::FetchItems); +} + +qint64 FetchItemsResponse::id() const +{ + return d_func()->id; +} + +void FetchItemsResponse::setRevision(int revision) +{ + d_func()->revision = revision; +} +int FetchItemsResponse::revision() const +{ + return d_func()->revision; +} + +void FetchItemsResponse::setParentId(qint64 parentId) +{ + d_func()->collectionId = parentId; +} +qint64 FetchItemsResponse::parentId() const +{ + return d_func()->collectionId; +} + +void FetchItemsResponse::setRemoteId(const QString &remoteId) +{ + d_func()->remoteId = remoteId; +} +QString FetchItemsResponse::remoteId() const +{ + return d_func()->remoteId; +} + +void FetchItemsResponse::setRemoteRevision(const QString &remoteRevision) +{ + d_func()->remoteRev = remoteRevision; +} +QString FetchItemsResponse::remoteRevision() const +{ + return d_func()->remoteRev; +} + +void FetchItemsResponse::setGid(const QString &gid) +{ + d_func()->gid = gid; +} +QString FetchItemsResponse::gid() const +{ + return d_func()->gid; +} + +void FetchItemsResponse::setSize(qint64 size) +{ + d_func()->size = size; +} +qint64 FetchItemsResponse::size() const +{ + return d_func()->size; +} + +void FetchItemsResponse::setMimeType(const QString &mimeType) +{ + d_func()->mimeType = mimeType; +} +QString FetchItemsResponse::mimeType() const +{ + return d_func()->mimeType; +} + +void FetchItemsResponse::setMTime(const QDateTime &mtime) +{ + d_func()->time = mtime; +} +QDateTime FetchItemsResponse::MTime() const +{ + return d_func()->time; +} + +void FetchItemsResponse::setFlags(const QVector &flags) +{ + d_func()->flags = flags; +} +QVector FetchItemsResponse::flags() const +{ + return d_func()->flags; +} + +void FetchItemsResponse::setTags(const QVector &tags) +{ + d_func()->tags = tags; +} +QVector FetchItemsResponse::tags() const +{ + return d_func()->tags; +} + +void FetchItemsResponse::setVirtualReferences(const QVector &refs) +{ + d_func()->virtRefs = refs; +} +QVector FetchItemsResponse::virtualReferences() const +{ + return d_func()->virtRefs; +} + +void FetchItemsResponse::setRelations(const QVector &relations) +{ + d_func()->relations = relations; +} +QVector FetchItemsResponse::relations() const +{ + return d_func()->relations; +} + +void FetchItemsResponse::setAncestors(const QVector &ancestors) +{ + d_func()->ancestors = ancestors; +} +QVector FetchItemsResponse::ancestors() const +{ + return d_func()->ancestors; +} + +void FetchItemsResponse::setParts(const QVector &parts) +{ + d_func()->parts = parts; +} +QVector FetchItemsResponse::parts() const +{ + return d_func()->parts; +} + +void FetchItemsResponse::setCachedParts(const QVector &cachedParts) +{ + d_func()->cachedParts = cachedParts; +} +QVector FetchItemsResponse::cachedParts() const +{ + return d_func()->cachedParts; +} + +DataStream &operator<<(DataStream &stream, const FetchItemsResponse &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchItemsResponse &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/*****************************************************************************/ + + + + +class LinkItemsCommandPrivate : public CommandPrivate +{ +public: + LinkItemsCommandPrivate(LinkItemsCommand::Action action = LinkItemsCommand::Link, + const Scope &items = Scope(), + const Scope &dest = Scope()) + : CommandPrivate(Command::LinkItems) + , items(items) + , dest(dest) + , action(action) + {} + LinkItemsCommandPrivate(const LinkItemsCommandPrivate &other) + : CommandPrivate(other) + , items(other.items) + , dest(other.dest) + , action(other.action) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(action) + && COMPARE(items) + && COMPARE(dest); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << action + << items + << dest; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> action + >> items + >> dest; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Action", (action == LinkItemsCommand::Link ? "Link" : "Unlink")); + blck.write("Items", items); + blck.write("Destination", dest); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new LinkItemsCommandPrivate(*this); + } + + Scope items; + Scope dest; + LinkItemsCommand::Action action; +}; + + + + +AKONADI_DECLARE_PRIVATE(LinkItemsCommand) + +LinkItemsCommand::LinkItemsCommand() + : Command(new LinkItemsCommandPrivate) +{ +} + +LinkItemsCommand::LinkItemsCommand(Action action, const Scope &items, const Scope &dest) + : Command(new LinkItemsCommandPrivate(action, items, dest)) +{ +} + +LinkItemsCommand::LinkItemsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::LinkItems); +} + +LinkItemsCommand::Action LinkItemsCommand::action() const +{ + return d_func()->action; +} +Scope LinkItemsCommand::items() const +{ + return d_func()->items; +} +Scope LinkItemsCommand::destination() const +{ + return d_func()->dest; +} + +DataStream &operator<<(DataStream &stream, const LinkItemsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, LinkItemsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/****************************************************************************/ + + + + +LinkItemsResponse::LinkItemsResponse() + : Response(new ResponsePrivate(Command::LinkItems)) +{ +} + +LinkItemsResponse::LinkItemsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::LinkItems); +} + + + + +/****************************************************************************/ + + + + +class ModifyItemsCommandPrivate : public CommandPrivate +{ +public: + ModifyItemsCommandPrivate(const Scope &items = Scope()) + : CommandPrivate(Command::ModifyItems) + , items(items) + , size(0) + , oldRevision(-1) + , dirty(true) + , invalidate(false) + , noResponse(false) + , notify(true) + , modifiedParts(ModifyItemsCommand::None) + {} + ModifyItemsCommandPrivate(const ModifyItemsCommandPrivate &other) + : CommandPrivate(other) + , items(other.items) + , flags(other.flags) + , addedFlags(other.addedFlags) + , removedFlags(other.removedFlags) + , tags(other.tags) + , addedTags(other.addedTags) + , removedTags(other.removedTags) + , remoteId(other.remoteId) + , remoteRev(other.remoteRev) + , gid(other.gid) + , removedParts(other.removedParts) + , parts(other.parts) + , attributes(other.attributes) + , size(other.size) + , oldRevision(other.oldRevision) + , dirty(other.dirty) + , invalidate(other.invalidate) + , noResponse(other.noResponse) + , notify(other.notify) + , modifiedParts(other.modifiedParts) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(modifiedParts) + && COMPARE(size) + && COMPARE(oldRevision) + && COMPARE(dirty) + && COMPARE(invalidate) + && COMPARE(noResponse) + && COMPARE(notify) + && COMPARE(items) + && COMPARE(flags) && COMPARE(addedFlags) && COMPARE(removedFlags) + && COMPARE(tags) && COMPARE(addedTags) && COMPARE(removedTags) + && COMPARE(remoteId) + && COMPARE(remoteRev) + && COMPARE(gid) + && COMPARE(removedParts) && COMPARE(parts) + && COMPARE(attributes); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + CommandPrivate::serialize(stream) + << items + << oldRevision + << modifiedParts + << dirty + << invalidate + << noResponse + << notify; + + if (modifiedParts & ModifyItemsCommand::Flags) { + stream << flags; + } + if (modifiedParts & ModifyItemsCommand::AddedFlags) { + stream << addedFlags; + } + if (modifiedParts & ModifyItemsCommand::RemovedFlags) { + stream << removedFlags; + } + if (modifiedParts & ModifyItemsCommand::Tags) { + stream << tags; + } + if (modifiedParts & ModifyItemsCommand::AddedTags) { + stream << addedTags; + } + if (modifiedParts & ModifyItemsCommand::RemovedTags) { + stream << removedTags; + } + if (modifiedParts & ModifyItemsCommand::RemoteID) { + stream << remoteId; + } + if (modifiedParts & ModifyItemsCommand::RemoteRevision) { + stream << remoteRev; + } + if (modifiedParts & ModifyItemsCommand::GID) { + stream << gid; + } + if (modifiedParts & ModifyItemsCommand::Size) { + stream << size; + } + if (modifiedParts & ModifyItemsCommand::Parts) { + stream << parts; + } + if (modifiedParts & ModifyItemsCommand::RemovedParts) { + stream << removedParts; + } + if (modifiedParts & ModifyItemsCommand::Attributes) { + stream << attributes; + } + return stream; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + CommandPrivate::deserialize(stream) + >> items + >> oldRevision + >> modifiedParts + >> dirty + >> invalidate + >> noResponse + >> notify; + + if (modifiedParts & ModifyItemsCommand::Flags) { + stream >> flags; + } + if (modifiedParts & ModifyItemsCommand::AddedFlags) { + stream >> addedFlags; + } + if (modifiedParts & ModifyItemsCommand::RemovedFlags) { + stream >> removedFlags; + } + if (modifiedParts & ModifyItemsCommand::Tags) { + stream >> tags; + } + if (modifiedParts & ModifyItemsCommand::AddedTags) { + stream >> addedTags; + } + if (modifiedParts & ModifyItemsCommand::RemovedTags) { + stream >> removedTags; + } + if (modifiedParts & ModifyItemsCommand::RemoteID) { + stream >> remoteId; + } + if (modifiedParts & ModifyItemsCommand::RemoteRevision) { + stream >> remoteRev; + } + if (modifiedParts & ModifyItemsCommand::GID) { + stream >> gid; + } + if (modifiedParts & ModifyItemsCommand::Size) { + stream >> size; + } + if (modifiedParts & ModifyItemsCommand::Parts) { + stream >> parts; + } + if (modifiedParts & ModifyItemsCommand::RemovedParts) { + stream >> removedParts; + } + if (modifiedParts & ModifyItemsCommand::Attributes) { + stream >> attributes; + } + return stream; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + QStringList mps; + if (modifiedParts & ModifyItemsCommand::Flags) { + mps << QStringLiteral("Flags"); + } + if (modifiedParts & ModifyItemsCommand::AddedFlags) { + mps << QStringLiteral("Added Flags"); + } + if (modifiedParts & ModifyItemsCommand::RemovedFlags) { + mps << QStringLiteral("Removed Flags"); + } + if (modifiedParts & ModifyItemsCommand::Tags) { + mps << QStringLiteral("Tags"); + } + if (modifiedParts & ModifyItemsCommand::AddedTags) { + mps << QStringLiteral("Added Tags"); + } + if (modifiedParts & ModifyItemsCommand::RemovedTags) { + mps << QStringLiteral("Removed Tags"); + } + if (modifiedParts & ModifyItemsCommand::RemoteID) { + mps << QStringLiteral("Remote ID"); + } + if (modifiedParts & ModifyItemsCommand::RemoteRevision) { + mps << QStringLiteral("Remote Revision"); + } + if (modifiedParts & ModifyItemsCommand::GID) { + mps << QStringLiteral("GID"); + } + if (modifiedParts & ModifyItemsCommand::Size) { + mps << QStringLiteral("Size"); + } + if (modifiedParts & ModifyItemsCommand::Parts) { + mps << QStringLiteral("Parts"); + } + if (modifiedParts & ModifyItemsCommand::RemovedParts) { + mps << QStringLiteral("Removed Parts"); + } + if (modifiedParts & ModifyItemsCommand::Attributes) { + mps << QStringLiteral("Attributes"); + } + + CommandPrivate::debugString(blck); + blck.write("Modified PartS", mps); + blck.write("Items", items); + blck.write("Old Revision", oldRevision); + blck.write("Dirty", dirty); + blck.write("Invalidate Cache", invalidate); + blck.write("No Response", noResponse); + blck.write("Notify", notify); + if (modifiedParts & ModifyItemsCommand::Flags) { + blck.write("Flags", flags); + } + if (modifiedParts & ModifyItemsCommand::AddedFlags) { + blck.write("Added Flags", addedFlags); + } + if (modifiedParts & ModifyItemsCommand::RemovedFlags) { + blck.write("Removed Flags", removedFlags); + } + if (modifiedParts & ModifyItemsCommand::Tags) { + blck.write("Tags", tags); + } + if (modifiedParts & ModifyItemsCommand::AddedTags) { + blck.write("Added Tags", addedTags); + } + if (modifiedParts & ModifyItemsCommand::RemovedTags) { + blck.write("Removed Tags", removedTags); + } + if (modifiedParts & ModifyItemsCommand::RemoteID) { + blck.write("Remote ID", remoteId); + } + if (modifiedParts & ModifyItemsCommand::RemoteRevision) { + blck.write("Remote Revision", remoteRev); + } + if (modifiedParts & ModifyItemsCommand::GID) { + blck.write("GID", gid); + } + if (modifiedParts & ModifyItemsCommand::Size) { + blck.write("Size", size); + } + if (modifiedParts & ModifyItemsCommand::Parts) { + blck.write("Parts", parts); + } + if (modifiedParts & ModifyItemsCommand::RemovedParts) { + blck.write("Removed Parts", removedParts); + } + if (modifiedParts & ModifyItemsCommand::Attributes) { + blck.beginBlock("Attributes"); + for (auto iter = attributes.constBegin(); iter != attributes.constEnd(); ++iter) { + blck.write(iter.key().constData(), iter.value()); + } + blck.endBlock(); + } + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new ModifyItemsCommandPrivate(*this); + } + + Scope items; + QSet flags; + QSet addedFlags; + QSet removedFlags; + Scope tags; + Scope addedTags; + Scope removedTags; + + QString remoteId; + QString remoteRev; + QString gid; + QSet removedParts; + QSet parts; + Attributes attributes; + qint64 size; + int oldRevision; + bool dirty; + bool invalidate; + bool noResponse; + bool notify; + + ModifyItemsCommand::ModifiedParts modifiedParts; +}; + + + + +AKONADI_DECLARE_PRIVATE(ModifyItemsCommand) + +ModifyItemsCommand::ModifyItemsCommand() + : Command(new ModifyItemsCommandPrivate) +{ +} + +ModifyItemsCommand::ModifyItemsCommand(const Scope &items) + : Command(new ModifyItemsCommandPrivate(items)) +{ +} + +ModifyItemsCommand::ModifyItemsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::ModifyItems); +} + +ModifyItemsCommand::ModifiedParts ModifyItemsCommand::modifiedParts() const +{ + return d_func()->modifiedParts; +} + +void ModifyItemsCommand::setItems(const Scope &items) +{ + d_func()->items = items; +} +Scope ModifyItemsCommand::items() const +{ + return d_func()->items; +} + +void ModifyItemsCommand::setOldRevision(int oldRevision) +{ + d_func()->oldRevision = oldRevision; +} +int ModifyItemsCommand::oldRevision() const +{ + return d_func()->oldRevision; +} + +void ModifyItemsCommand::setFlags(const QSet &flags) +{ + d_func()->modifiedParts |= Flags; + d_func()->flags = flags; +} +QSet ModifyItemsCommand::flags() const +{ + return d_func()->flags; +} + +void ModifyItemsCommand::setAddedFlags(const QSet &addedFlags) +{ + d_func()->modifiedParts |= AddedFlags; + d_func()->addedFlags = addedFlags; +} +QSet ModifyItemsCommand::addedFlags() const +{ + return d_func()->addedFlags; +} + +void ModifyItemsCommand::setRemovedFlags(const QSet &removedFlags) +{ + d_func()->modifiedParts |= RemovedFlags; + d_func()->removedFlags = removedFlags; +} +QSet ModifyItemsCommand::removedFlags() const +{ + return d_func()->removedFlags; +} + +void ModifyItemsCommand::setTags(const Scope &tags) +{ + d_func()->modifiedParts |= Tags; + d_func()->tags = tags; +} +Scope ModifyItemsCommand::tags() const +{ + return d_func()->tags; +} + +void ModifyItemsCommand::setAddedTags(const Scope &addedTags) +{ + d_func()->modifiedParts |= AddedTags; + d_func()->addedTags = addedTags; +} +Scope ModifyItemsCommand::addedTags() const +{ + return d_func()->addedTags; +} + +void ModifyItemsCommand::setRemovedTags(const Scope &removedTags) +{ + d_func()->modifiedParts |= RemovedTags; + d_func()->removedTags = removedTags; +} +Scope ModifyItemsCommand::removedTags() const +{ + return d_func()->removedTags; +} + +void ModifyItemsCommand::setRemoteId(const QString &remoteId) +{ + d_func()->modifiedParts |= RemoteID; + d_func()->remoteId = remoteId; +} +QString ModifyItemsCommand::remoteId() const +{ + return d_func()->remoteId; +} + +void ModifyItemsCommand::setRemoteRevision(const QString &remoteRevision) +{ + d_func()->modifiedParts |= RemoteRevision; + d_func()->remoteRev = remoteRevision; +} +QString ModifyItemsCommand::remoteRevision() const +{ + return d_func()->remoteRev; +} + +void ModifyItemsCommand::setGid(const QString &gid) +{ + d_func()->modifiedParts |= GID; + d_func()->gid = gid; +} +QString ModifyItemsCommand::gid() const +{ + return d_func()->gid; +} + +void ModifyItemsCommand::setDirty(bool dirty) +{ + d_func()->dirty = dirty; +} +bool ModifyItemsCommand::dirty() const +{ + return d_func()->dirty; +} + +void ModifyItemsCommand::setInvalidateCache(bool invalidate) +{ + d_func()->invalidate = invalidate; +} +bool ModifyItemsCommand::invalidateCache() const +{ + return d_func()->invalidate; +} + +void ModifyItemsCommand::setNoResponse(bool noResponse) +{ + d_func()->noResponse = noResponse; +} +bool ModifyItemsCommand::noResponse() const +{ + return d_func()->noResponse; +} + +void ModifyItemsCommand::setNotify(bool notify) +{ + d_func()->notify = notify; +} +bool ModifyItemsCommand::notify() const +{ + return d_func()->notify; +} + +void ModifyItemsCommand::setItemSize(qint64 size) +{ + d_func()->modifiedParts |= Size; + d_func()->size = size; +} +qint64 ModifyItemsCommand::itemSize() const +{ + return d_func()->size; +} + +void ModifyItemsCommand::setRemovedParts(const QSet &removedParts) +{ + d_func()->modifiedParts |= RemovedParts; + d_func()->removedParts = removedParts; +} +QSet ModifyItemsCommand::removedParts() const +{ + return d_func()->removedParts; +} + +void ModifyItemsCommand::setParts(const QSet &parts) +{ + d_func()->modifiedParts |= Parts; + d_func()->parts = parts; +} +QSet ModifyItemsCommand::parts() const +{ + return d_func()->parts; +} + +void ModifyItemsCommand::setAttributes(const Protocol::Attributes &attributes) +{ + d_func()->modifiedParts |= Attributes; + d_func()->attributes = attributes; +} +Attributes ModifyItemsCommand::attributes() const +{ + return d_func()->attributes; +} + +DataStream &operator<<(DataStream &stream, const ModifyItemsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, ModifyItemsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +class ModifyItemsResponsePrivate : public ResponsePrivate +{ +public: + ModifyItemsResponsePrivate(qint64 id = -1, int newRevision = -1, const QDateTime &modifyDt = QDateTime()) + : ResponsePrivate(Command::ModifyItems) + , id(id) + , newRevision(newRevision) + , modificationDt(modifyDt) + {} + ModifyItemsResponsePrivate(const ModifyItemsResponsePrivate &other) + : ResponsePrivate(other) + , id(other.id) + , newRevision(other.newRevision) + , modificationDt(other.modificationDt) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return ResponsePrivate::compare(other) + && COMPARE(id) + && COMPARE(newRevision) + && COMPARE(modificationDt); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return ResponsePrivate::serialize(stream) + << id + << newRevision + << modificationDt; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return ResponsePrivate::deserialize(stream) + >> id + >> newRevision + >> modificationDt; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + ResponsePrivate::debugString(blck); + blck.write("ID", id); + blck.write("New Revision", newRevision); + blck.write("Modification datetime", modificationDt); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new ModifyItemsResponsePrivate(*this); + } + + qint64 id; + int newRevision; + QDateTime modificationDt; +}; + + + + +AKONADI_DECLARE_PRIVATE(ModifyItemsResponse) + +ModifyItemsResponse::ModifyItemsResponse() + : Response(new ModifyItemsResponsePrivate) +{ +} + +ModifyItemsResponse::ModifyItemsResponse(qint64 id, int newRevision) + : Response(new ModifyItemsResponsePrivate(id, newRevision)) +{ +} + +ModifyItemsResponse::ModifyItemsResponse(const QDateTime &modificationDt) + : Response(new ModifyItemsResponsePrivate(-1, -1, modificationDt)) +{ +} + +ModifyItemsResponse::ModifyItemsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::ModifyItems); +} + +qint64 ModifyItemsResponse::id() const +{ + return d_func()->id; +} +int ModifyItemsResponse::newRevision() const +{ + return d_func()->newRevision; +} + +QDateTime ModifyItemsResponse::modificationDateTime() const +{ + return d_func()->modificationDt; +} + +DataStream &operator<<(DataStream &stream, const ModifyItemsResponse &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, ModifyItemsResponse &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + +class MoveItemsCommandPrivate : public CommandPrivate +{ +public: + MoveItemsCommandPrivate(const Scope &items = Scope(), + const ScopeContext &context = ScopeContext(), + const Scope &dest = Scope()) + : CommandPrivate(Command::MoveItems) + , items(items) + , dest(dest) + , context(context) + {} + MoveItemsCommandPrivate(const MoveItemsCommandPrivate &other) + : CommandPrivate(other) + , items(other.items) + , dest(other.dest) + , context(other.context) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(items) + && COMPARE(dest) + && COMPARE(context); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << items + << dest + << context; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> items + >> dest + >> context; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Items", items); + blck.beginBlock("Context"); + context.debugString(blck); + blck.endBlock(); + blck.write("Destination", dest); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new MoveItemsCommandPrivate(*this); + } + + Scope items; + Scope dest; + ScopeContext context; +}; + + + + +AKONADI_DECLARE_PRIVATE(MoveItemsCommand) + +MoveItemsCommand::MoveItemsCommand() + : Command(new MoveItemsCommandPrivate) +{ +} + +MoveItemsCommand::MoveItemsCommand(const Scope &items, const Scope &dest) + : Command(new MoveItemsCommandPrivate(items, ScopeContext(), dest)) +{ +} + +MoveItemsCommand::MoveItemsCommand(const Scope &items, const ScopeContext &ctx, + const Scope &dest) + : Command(new MoveItemsCommandPrivate(items, ctx, dest)) +{ +} + +MoveItemsCommand::MoveItemsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::MoveItems); +} + +Scope MoveItemsCommand::items() const +{ + return d_func()->items; +} + +ScopeContext MoveItemsCommand::itemsContext() const +{ + return d_func()->context; +} + +Scope MoveItemsCommand::destination() const +{ + return d_func()->dest; +} + +DataStream &operator<<(DataStream &stream, const MoveItemsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, MoveItemsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + +MoveItemsResponse::MoveItemsResponse() + : Response(new ResponsePrivate(Command::MoveItems)) +{ +} + +MoveItemsResponse::MoveItemsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::MoveItems); +} + + + +/****************************************************************************/ + + + + +class CreateCollectionCommandPrivate : public CommandPrivate +{ +public: + CreateCollectionCommandPrivate() + : CommandPrivate(Command::CreateCollection) + , sync(Tristate::Undefined) + , display(Tristate::Undefined) + , index(Tristate::Undefined) + , enabled(true) + , isVirtual(false) + {} + CreateCollectionCommandPrivate(const CreateCollectionCommandPrivate &other) + : CommandPrivate(other) + , parent(other.parent) + , name(other.name) + , remoteId(other.remoteId) + , remoteRev(other.remoteRev) + , mimeTypes(other.mimeTypes) + , cachePolicy(other.cachePolicy) + , attributes(other.attributes) + , sync(other.sync) + , display(other.display) + , index(other.index) + , enabled(other.enabled) + , isVirtual(other.isVirtual) + {} + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << parent + << name + << remoteId + << remoteRev + << mimeTypes + << cachePolicy + << attributes + << enabled + << sync + << display + << index + << isVirtual; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> parent + >> name + >> remoteId + >> remoteRev + >> mimeTypes + >> cachePolicy + >> attributes + >> enabled + >> sync + >> display + >> index + >> isVirtual; + } + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(sync) + && COMPARE(display) + && COMPARE(index) + && COMPARE(enabled) + && COMPARE(isVirtual) + && COMPARE(parent) + && COMPARE(name) + && COMPARE(remoteId) + && COMPARE(remoteRev) + && COMPARE(mimeTypes) + && COMPARE(cachePolicy) + && COMPARE(attributes); + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Name", name); + blck.write("Parent", parent); + blck.write("Remote ID", remoteId); + blck.write("Remote Revision", remoteRev); + blck.write("Mimetypes", mimeTypes); + blck.write("Sync", sync); + blck.write("Display", display); + blck.write("Index", index); + blck.write("Enabled", enabled); + blck.write("Virtual", isVirtual); + blck.beginBlock("CachePolicy"); + cachePolicy.debugString(blck); + blck.endBlock(); + blck.write("Attributes", attributes); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new CreateCollectionCommandPrivate(*this); + } + + Scope parent; + QString name; + QString remoteId; + QString remoteRev; + QStringList mimeTypes; + CachePolicy cachePolicy; + Attributes attributes; + Tristate sync; + Tristate display; + Tristate index; + bool enabled; + bool isVirtual; +}; + + + + +AKONADI_DECLARE_PRIVATE(CreateCollectionCommand) + +CreateCollectionCommand::CreateCollectionCommand() + : Command(new CreateCollectionCommandPrivate) +{ +} + +CreateCollectionCommand::CreateCollectionCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::CreateCollection); +} + +void CreateCollectionCommand::setParent(const Scope &parent) +{ + d_func()->parent = parent; +} +Scope CreateCollectionCommand::parent() const +{ + return d_func()->parent; +} + +void CreateCollectionCommand::setName(const QString &name) +{ + d_func()->name = name; +} +QString CreateCollectionCommand::name() const +{ + return d_func()->name; +} + +void CreateCollectionCommand::setRemoteId(const QString &remoteId) +{ + d_func()->remoteId = remoteId; +} +QString CreateCollectionCommand::remoteId() const +{ + return d_func()->remoteId; +} + +void CreateCollectionCommand::setRemoteRevision(const QString &remoteRevision) +{ + d_func()->remoteRev = remoteRevision; +} +QString CreateCollectionCommand::remoteRevision() const +{ + return d_func()->remoteRev; +} + +void CreateCollectionCommand::setMimeTypes(const QStringList &mimeTypes) +{ + d_func()->mimeTypes = mimeTypes; +} +QStringList CreateCollectionCommand::mimeTypes() const +{ + return d_func()->mimeTypes; +} + +void CreateCollectionCommand::setCachePolicy(const CachePolicy &cachePolicy) +{ + d_func()->cachePolicy = cachePolicy; +} +CachePolicy CreateCollectionCommand::cachePolicy() const +{ + return d_func()->cachePolicy; +} + +void CreateCollectionCommand::setAttributes(const Attributes &attributes) +{ + d_func()->attributes = attributes; +} +Attributes CreateCollectionCommand::attributes() const +{ + return d_func()->attributes; +} + +void CreateCollectionCommand::setIsVirtual(bool isVirtual) +{ + d_func()->isVirtual = isVirtual; +} +bool CreateCollectionCommand::isVirtual() const +{ + return d_func()->isVirtual; +} + +void CreateCollectionCommand::setEnabled(bool enabled) +{ + d_func()->enabled = enabled; +} +bool CreateCollectionCommand::enabled() const +{ + return d_func()->enabled; +} + +void CreateCollectionCommand::setSyncPref(Tristate sync) +{ + d_func()->sync = sync; +} +Tristate CreateCollectionCommand::syncPref() const +{ + return d_func()->sync; +} + +void CreateCollectionCommand::setDisplayPref(Tristate display) +{ + d_func()->display = display; +} +Tristate CreateCollectionCommand::displayPref() const +{ + return d_func()->display; +} + +void CreateCollectionCommand::setIndexPref(Tristate index) +{ + d_func()->index = index; +} +Tristate CreateCollectionCommand::indexPref() const +{ + return d_func()->index; +} + +DataStream &operator<<(DataStream &stream, const CreateCollectionCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, CreateCollectionCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/****************************************************************************/ + + + + +CreateCollectionResponse::CreateCollectionResponse() + : Response(new ResponsePrivate(Command::CreateCollection)) +{ +} + +CreateCollectionResponse::CreateCollectionResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::CreateCollection); +} + + + +/****************************************************************************/ + + + + +class CopyCollectionCommandPrivate : public CommandPrivate +{ +public: + CopyCollectionCommandPrivate(const Scope &collection = Scope(), + const Scope &dest = Scope()) + : CommandPrivate(Command::CopyCollection) + , collection(collection) + , dest(dest) + {} + CopyCollectionCommandPrivate(const CopyCollectionCommandPrivate &other) + : CommandPrivate(other) + , collection(other.collection) + , dest(other.dest) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(collection) + && COMPARE(dest); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << collection + << dest; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> collection + >> dest; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Collection", collection); + blck.write("Destination", dest); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new CopyCollectionCommandPrivate(*this); + } + + Scope collection; + Scope dest; +}; + + + + +AKONADI_DECLARE_PRIVATE(CopyCollectionCommand) + +CopyCollectionCommand::CopyCollectionCommand() + : Command(new CopyCollectionCommandPrivate) +{ +} + +CopyCollectionCommand::CopyCollectionCommand(const Scope &collection, + const Scope &destination) + : Command(new CopyCollectionCommandPrivate(collection, destination)) +{ +} + +CopyCollectionCommand::CopyCollectionCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::CopyCollection); +} + +Scope CopyCollectionCommand::collection() const +{ + return d_func()->collection; +} +Scope CopyCollectionCommand::destination() const +{ + return d_func()->dest; +} + +DataStream &operator<<(DataStream &stream, const CopyCollectionCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, CopyCollectionCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +CopyCollectionResponse::CopyCollectionResponse() + : Response(new ResponsePrivate(Command::CopyCollection)) +{ +} + +CopyCollectionResponse::CopyCollectionResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::CopyCollection); +} + + + +/****************************************************************************/ + + + + +class DeleteCollectionCommandPrivate : public CommandPrivate +{ +public: + DeleteCollectionCommandPrivate(const Scope &col = Scope()) + : CommandPrivate(Command::DeleteCollection) + , collection(col) + {} + DeleteCollectionCommandPrivate(const DeleteCollectionCommandPrivate &other) + : CommandPrivate(other) + , collection(other.collection) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(collection); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << collection; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> collection; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Collection", collection); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new DeleteCollectionCommandPrivate(*this); + } + + Scope collection; +}; + + + + +AKONADI_DECLARE_PRIVATE(DeleteCollectionCommand) + +DeleteCollectionCommand::DeleteCollectionCommand() + : Command(new DeleteCollectionCommandPrivate) +{ +} + +DeleteCollectionCommand::DeleteCollectionCommand(const Scope &collection) + : Command(new DeleteCollectionCommandPrivate(collection)) +{ +} + +DeleteCollectionCommand::DeleteCollectionCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::DeleteCollection); +} + +Scope DeleteCollectionCommand::collection() const +{ + return d_func()->collection; +} + +DataStream &operator<<(DataStream &stream, const DeleteCollectionCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, DeleteCollectionCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +DeleteCollectionResponse::DeleteCollectionResponse() + : Response(new ResponsePrivate(Command::DeleteCollection)) +{ +} + +DeleteCollectionResponse::DeleteCollectionResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::DeleteCollection); +} + + + +/****************************************************************************/ + + + + +class FetchCollectionStatsCommandPrivate : public CommandPrivate +{ +public: + FetchCollectionStatsCommandPrivate(const Scope &collection = Scope()) + : CommandPrivate(Command::FetchCollectionStats) + , collection(collection) + {} + FetchCollectionStatsCommandPrivate(const FetchCollectionStatsCommandPrivate &other) + : CommandPrivate(other) + , collection(other.collection) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(collection); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << collection; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> collection; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Collection", collection); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchCollectionStatsCommandPrivate(*this); + } + + Scope collection; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchCollectionStatsCommand) + +FetchCollectionStatsCommand::FetchCollectionStatsCommand() + : Command(new FetchCollectionStatsCommandPrivate) +{ +} + +FetchCollectionStatsCommand::FetchCollectionStatsCommand(const Scope &collection) + : Command(new FetchCollectionStatsCommandPrivate(collection)) +{ +} + +FetchCollectionStatsCommand::FetchCollectionStatsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::FetchCollectionStats); +} + +Scope FetchCollectionStatsCommand::collection() const +{ + return d_func()->collection; +} + +DataStream &operator<<(DataStream &stream, const FetchCollectionStatsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchCollectionStatsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +class FetchCollectionStatsResponsePrivate : public ResponsePrivate +{ +public: + FetchCollectionStatsResponsePrivate(qint64 count = -1, + qint64 unseen = -1, + qint64 size = -1) + : ResponsePrivate(Command::FetchCollectionStats) + , count(count) + , unseen(unseen) + , size(size) + {} + FetchCollectionStatsResponsePrivate(const FetchCollectionStatsResponsePrivate &other) + : ResponsePrivate(other) + , count(other.count) + , unseen(other.unseen) + , size(other.size) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return ResponsePrivate::compare(other) + && COMPARE(count) + && COMPARE(unseen) + && COMPARE(size); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return ResponsePrivate::serialize(stream) + << count + << unseen + << size; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return ResponsePrivate::deserialize(stream) + >> count + >> unseen + >> size; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + ResponsePrivate::debugString(blck); + blck.write("Count", count); + blck.write("Unseen", unseen); + blck.write("Size", size); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchCollectionStatsResponsePrivate(*this); + } + + qint64 count; + qint64 unseen; + qint64 size; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchCollectionStatsResponse) + +FetchCollectionStatsResponse::FetchCollectionStatsResponse() + : Response(new FetchCollectionStatsResponsePrivate) +{ +} + +FetchCollectionStatsResponse::FetchCollectionStatsResponse(qint64 count, + qint64 unseen, + qint64 size) + : Response(new FetchCollectionStatsResponsePrivate(count, unseen, size)) +{ +} + +FetchCollectionStatsResponse::FetchCollectionStatsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::FetchCollectionStats); +} + +qint64 FetchCollectionStatsResponse::count() const +{ + return d_func()->count; +} +qint64 FetchCollectionStatsResponse::unseen() const +{ + return d_func()->unseen; +} +qint64 FetchCollectionStatsResponse::size() const +{ + return d_func()->size; +} + +DataStream &operator<<(DataStream &stream, const FetchCollectionStatsResponse &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchCollectionStatsResponse &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/****************************************************************************/ + + + + +class FetchCollectionsCommandPrivate : public CommandPrivate +{ +public: + FetchCollectionsCommandPrivate(const Scope &collections = Scope()) + : CommandPrivate(Command::FetchCollections) + , collections(collections) + , depth(FetchCollectionsCommand::BaseCollection) + , ancestorsDepth(Ancestor::NoAncestor) + , enabled(false) + , sync(false) + , display(false) + , index(false) + , stats(false) + {} + FetchCollectionsCommandPrivate(const FetchCollectionsCommandPrivate &other) + : CommandPrivate(other) + , collections(other.collections) + , resource(other.resource) + , mimeTypes(other.mimeTypes) + , ancestorsAttributes(other.ancestorsAttributes) + , depth(other.depth) + , ancestorsDepth(other.ancestorsDepth) + , enabled(other.enabled) + , sync(other.sync) + , display(other.display) + , index(other.index) + , stats(other.stats) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(depth) + && COMPARE(ancestorsDepth) + && COMPARE(enabled) + && COMPARE(sync) + && COMPARE(display) + && COMPARE(index) + && COMPARE(stats) + && COMPARE(collections) + && COMPARE(resource) + && COMPARE(mimeTypes) + && COMPARE(ancestorsAttributes); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << collections + << resource + << mimeTypes + << depth + << ancestorsDepth + << ancestorsAttributes + << enabled + << sync + << display + << index + << stats; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> collections + >> resource + >> mimeTypes + >> depth + >> ancestorsDepth + >> ancestorsAttributes + >> enabled + >> sync + >> display + >> index + >> stats; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Collections", collections); + blck.write("Depth", depth); + blck.write("Resource", resource); + blck.write("Mimetypes", mimeTypes); + blck.write("Ancestors Depth", ancestorsDepth); + blck.write("Ancestors Attributes", ancestorsAttributes); + blck.write("Enabled", enabled); + blck.write("Sync", sync); + blck.write("Display", display); + blck.write("Index", index); + blck.write("Status", stats); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchCollectionsCommandPrivate(*this); + } + + Scope collections; + QString resource; + QStringList mimeTypes; + QSet ancestorsAttributes; + FetchCollectionsCommand::Depth depth; + Ancestor::Depth ancestorsDepth; + bool enabled; + bool sync; + bool display; + bool index; + bool stats; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchCollectionsCommand) + +FetchCollectionsCommand::FetchCollectionsCommand() + : Command(new FetchCollectionsCommandPrivate) +{ +} + +FetchCollectionsCommand::FetchCollectionsCommand(const Scope &collections) + : Command(new FetchCollectionsCommandPrivate(collections)) +{ +} + +FetchCollectionsCommand::FetchCollectionsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::FetchCollections); +} + +Scope FetchCollectionsCommand::collections() const +{ + return d_func()->collections; +} + +void FetchCollectionsCommand::setDepth(Depth depth) +{ + d_func()->depth = depth; +} +FetchCollectionsCommand::Depth FetchCollectionsCommand::depth() const +{ + return d_func()->depth; +} + +void FetchCollectionsCommand::setResource(const QString &resourceId) +{ + d_func()->resource = resourceId; +} +QString FetchCollectionsCommand::resource() const +{ + return d_func()->resource; +} + +void FetchCollectionsCommand::setMimeTypes(const QStringList &mimeTypes) +{ + d_func()->mimeTypes = mimeTypes; +} +QStringList FetchCollectionsCommand::mimeTypes() const +{ + return d_func()->mimeTypes; +} + +void FetchCollectionsCommand::setAncestorsDepth(Ancestor::Depth depth) +{ + d_func()->ancestorsDepth = depth; +} +Ancestor::Depth FetchCollectionsCommand::ancestorsDepth() const +{ + return d_func()->ancestorsDepth; +} + +void FetchCollectionsCommand::setAncestorsAttributes(const QSet &attributes) +{ + d_func()->ancestorsAttributes = attributes; +} +QSet FetchCollectionsCommand::ancestorsAttributes() const +{ + return d_func()->ancestorsAttributes; +} + +void FetchCollectionsCommand::setEnabled(bool enabled) +{ + d_func()->enabled = enabled; +} +bool FetchCollectionsCommand::enabled() const +{ + return d_func()->enabled; +} + +void FetchCollectionsCommand::setSyncPref(bool sync) +{ + d_func()->sync = sync; +} +bool FetchCollectionsCommand::syncPref() const +{ + return d_func()->sync; +} + +void FetchCollectionsCommand::setDisplayPref(bool display) +{ + d_func()->display = display; +} +bool FetchCollectionsCommand::displayPref() const +{ + return d_func()->display; +} + +void FetchCollectionsCommand::setIndexPref(bool index) +{ + d_func()->index = index; +} +bool FetchCollectionsCommand::indexPref() const +{ + return d_func()->index; +} + +void FetchCollectionsCommand::setFetchStats(bool stats) +{ + d_func()->stats = stats; +} +bool FetchCollectionsCommand::fetchStats() const +{ + return d_func()->stats; +} + +DataStream &operator<<(DataStream &stream, const FetchCollectionsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchCollectionsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + +class FetchCollectionsResponsePrivate : public ResponsePrivate +{ +public: + FetchCollectionsResponsePrivate(qint64 id = -1) + : ResponsePrivate(Command::FetchCollections) + , id(id) + , parentId(-1) + , display(Tristate::Undefined) + , sync(Tristate::Undefined) + , index(Tristate::Undefined) + , isVirtual(false) + , referenced(false) + , enabled(true) + {} + FetchCollectionsResponsePrivate(const FetchCollectionsResponsePrivate &other) + : ResponsePrivate(other) + , name(other.name) + , remoteId(other.remoteId) + , remoteRev(other.remoteRev) + , resource(other.resource) + , mimeTypes(other.mimeTypes) + , stats(other.stats) + , searchQuery(other.searchQuery) + , searchCols(other.searchCols) + , ancestors(other.ancestors) + , cachePolicy(other.cachePolicy) + , attributes(other.attributes) + , id(other.id) + , parentId(other.parentId) + , display(other.display) + , sync(other.sync) + , index(other.index) + , isVirtual(other.isVirtual) + , referenced(other.referenced) + , enabled(other.enabled) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return ResponsePrivate::compare(other) + && COMPARE(id) + && COMPARE(parentId) + && COMPARE(display) && COMPARE(sync) && COMPARE(index) + && COMPARE(isVirtual) && COMPARE(referenced) && COMPARE(enabled) + && COMPARE(name) + && COMPARE(remoteId) && COMPARE(remoteRev) + && COMPARE(resource) + && COMPARE(mimeTypes) + && COMPARE(stats) + && COMPARE(searchQuery) && COMPARE(searchCols) + && COMPARE(ancestors) + && COMPARE(cachePolicy) + && COMPARE(attributes); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return ResponsePrivate::serialize(stream) + << id + << parentId + << name + << mimeTypes + << remoteId + << remoteRev + << resource + << stats + << searchQuery + << searchCols + << ancestors + << cachePolicy + << attributes + << display + << sync + << index + << isVirtual + << referenced + << enabled; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return ResponsePrivate::deserialize(stream) + >> id + >> parentId + >> name + >> mimeTypes + >> remoteId + >> remoteRev + >> resource + >> stats + >> searchQuery + >> searchCols + >> ancestors + >> cachePolicy + >> attributes + >> display + >> sync + >> index + >> isVirtual + >> referenced + >> enabled; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + ResponsePrivate::debugString(blck); + blck.write("ID", id); + blck.write("Name", name); + blck.write("Parent ID", parentId); + blck.write("Remote ID", remoteId); + blck.write("Remote Revision", remoteRev); + blck.write("Resource", resource); + blck.write("Mimetypes", mimeTypes); + blck.beginBlock("Statistics"); + blck.write("Count", stats.count()); + blck.write("Unseen", stats.unseen()); + blck.write("Size", stats.size()); + blck.endBlock(); + blck.write("Search Query", searchQuery); + blck.write("Search Collections", searchCols); + blck.beginBlock("Cache Policy"); + cachePolicy.debugString(blck); + blck.endBlock(); + blck.beginBlock("Ancestors"); + Q_FOREACH (const Ancestor &anc, ancestors) { + blck.beginBlock(); + anc.debugString(blck); + blck.endBlock(); + } + blck.endBlock(); + blck.write("Attributes", attributes); + blck.write("Display", display); + blck.write("Sync", sync); + blck.write("Index", index); + blck.write("Enabled", enabled); + blck.write("Virtual", isVirtual); + blck.write("Referenced", referenced); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new FetchCollectionsResponsePrivate(*this); + } + + QString name; + QString remoteId; + QString remoteRev; + QString resource; + QStringList mimeTypes; + FetchCollectionStatsResponse stats; + QString searchQuery; + QVector searchCols; + QVector ancestors; + CachePolicy cachePolicy; + Attributes attributes; + qint64 id; + qint64 parentId; + Tristate display; + Tristate sync; + Tristate index; + bool isVirtual; + bool referenced; + bool enabled; +}; + + + + +AKONADI_DECLARE_PRIVATE(FetchCollectionsResponse) + +FetchCollectionsResponse::FetchCollectionsResponse() + : Response(new FetchCollectionsResponsePrivate) +{ +} + +FetchCollectionsResponse::FetchCollectionsResponse(qint64 id) + : Response(new FetchCollectionsResponsePrivate(id)) +{ +} + +FetchCollectionsResponse::FetchCollectionsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::FetchCollections); +} + +qint64 FetchCollectionsResponse::id() const +{ + return d_func()->id; +} + +void FetchCollectionsResponse::setParentId(qint64 parentId) +{ + d_func()->parentId = parentId; +} +qint64 FetchCollectionsResponse::parentId() const +{ + return d_func()->parentId; +} + +void FetchCollectionsResponse::setName(const QString &name) +{ + d_func()->name = name; +} +QString FetchCollectionsResponse::name() const +{ + return d_func()->name; +} + +void FetchCollectionsResponse::setMimeTypes(const QStringList &mimeTypes) +{ + d_func()->mimeTypes = mimeTypes; +} +QStringList FetchCollectionsResponse::mimeTypes() const +{ + return d_func()->mimeTypes; +} + +void FetchCollectionsResponse::setRemoteId(const QString &remoteId) +{ + d_func()->remoteId = remoteId; +} +QString FetchCollectionsResponse::remoteId() const +{ + return d_func()->remoteId; +} + +void FetchCollectionsResponse::setRemoteRevision(const QString &remoteRevision) +{ + d_func()->remoteRev = remoteRevision; +} +QString FetchCollectionsResponse::remoteRevision() const +{ + return d_func()->remoteRev; +} + +void FetchCollectionsResponse::setResource(const QString &resourceId) +{ + d_func()->resource = resourceId; +} +QString FetchCollectionsResponse::resource() const +{ + return d_func()->resource; +} + +void FetchCollectionsResponse::setStatistics(const FetchCollectionStatsResponse &stats) +{ + d_func()->stats = stats; +} +FetchCollectionStatsResponse FetchCollectionsResponse::statistics() const +{ + return d_func()->stats; +} + +void FetchCollectionsResponse::setSearchQuery(const QString &query) +{ + d_func()->searchQuery = query; +} +QString FetchCollectionsResponse::searchQuery() const +{ + return d_func()->searchQuery; +} + +void FetchCollectionsResponse::setSearchCollections(const QVector &searchCols) +{ + d_func()->searchCols = searchCols; +} +QVector FetchCollectionsResponse::searchCollections() const +{ + return d_func()->searchCols; +} + +void FetchCollectionsResponse::setAncestors(const QVector &ancestors) +{ + d_func()->ancestors = ancestors; +} +QVector FetchCollectionsResponse::ancestors() const +{ + return d_func()->ancestors; +} + +void FetchCollectionsResponse::setCachePolicy(const CachePolicy &cachePolicy) +{ + d_func()->cachePolicy = cachePolicy; +} +CachePolicy FetchCollectionsResponse::cachePolicy() const +{ + return d_func()->cachePolicy; +} + +CachePolicy &FetchCollectionsResponse::cachePolicy() +{ + return d_func()->cachePolicy; +} + +void FetchCollectionsResponse::setAttributes(const Attributes &attrs) +{ + d_func()->attributes = attrs; +} +Attributes FetchCollectionsResponse::attributes() const +{ + return d_func()->attributes; +} + +void FetchCollectionsResponse::setEnabled(bool enabled) +{ + d_func()->enabled = enabled; +} +bool FetchCollectionsResponse::enabled() const +{ + return d_func()->enabled; +} + +void FetchCollectionsResponse::setDisplayPref(Tristate display) +{ + d_func()->display = display; +} +Tristate FetchCollectionsResponse::displayPref() const +{ + return d_func()->display; +} + +void FetchCollectionsResponse::setSyncPref(Tristate sync) +{ + d_func()->sync = sync; +} +Tristate FetchCollectionsResponse::syncPref() const +{ + return d_func()->sync; +} + +void FetchCollectionsResponse::setIndexPref(Tristate index) +{ + d_func()->index = index; +} +Tristate FetchCollectionsResponse::indexPref() const +{ + return d_func()->index; +} + +void FetchCollectionsResponse::setReferenced(bool referenced) +{ + d_func()->referenced = referenced; +} +bool FetchCollectionsResponse::referenced() const +{ + return d_func()->referenced; +} + +void FetchCollectionsResponse::setIsVirtual(bool isVirtual) +{ + d_func()->isVirtual = isVirtual; +} +bool FetchCollectionsResponse::isVirtual() const +{ + return d_func()->isVirtual; +} + +DataStream &operator<<(DataStream &stream, const FetchCollectionsResponse &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, FetchCollectionsResponse &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +class ModifyCollectionCommandPrivate : public CommandPrivate +{ +public: + ModifyCollectionCommandPrivate(const Scope &collection = Scope()) + : CommandPrivate(Command::ModifyCollection) + , collection(collection) + , parentId(-1) + , sync(Tristate::Undefined) + , display(Tristate::Undefined) + , index(Tristate::Undefined) + , enabled(true) + , referenced(false) + , persistentSearchRemote(false) + , persistentSearchRecursive(false) + , modifiedParts(ModifyCollectionCommand::None) + {} + ModifyCollectionCommandPrivate(const ModifyCollectionCommandPrivate &other) + : CommandPrivate(other) + , collection(other.collection) + , mimeTypes(other.mimeTypes) + , cachePolicy(other.cachePolicy) + , name(other.name) + , remoteId(other.remoteId) + , remoteRev(other.remoteRev) + , persistentSearchQuery(other.persistentSearchQuery) + , persistentSearchCols(other.persistentSearchCols) + , removedAttributes(other.removedAttributes) + , attributes(other.attributes) + , parentId(other.parentId) + , sync(other.sync) + , display(other.display) + , index(other.index) + , enabled(other.enabled) + , referenced(other.referenced) + , persistentSearchRemote(other.persistentSearchRemote) + , persistentSearchRecursive(other.persistentSearchRecursive) + , modifiedParts(other.modifiedParts) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(modifiedParts) + && COMPARE(parentId) + && COMPARE(sync) && COMPARE(display) && COMPARE(index) + && COMPARE(enabled) && COMPARE(referenced) + && COMPARE(persistentSearchRemote) && COMPARE(persistentSearchRecursive) + && COMPARE(collection) + && COMPARE(mimeTypes) + && COMPARE(cachePolicy) + && COMPARE(name) + && COMPARE(remoteId) && COMPARE(remoteRev) + && COMPARE(persistentSearchQuery) && COMPARE(persistentSearchCols) + && COMPARE(removedAttributes) && COMPARE(attributes); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + CommandPrivate::serialize(stream) + << collection + << modifiedParts; + + if (modifiedParts & ModifyCollectionCommand::Name) { + stream << name; + } + if (modifiedParts & ModifyCollectionCommand::RemoteID) { + stream << remoteId; + } + if (modifiedParts & ModifyCollectionCommand::RemoteRevision) { + stream << remoteRev; + } + if (modifiedParts & ModifyCollectionCommand::ParentID) { + stream << parentId; + } + if (modifiedParts & ModifyCollectionCommand::MimeTypes) { + stream << mimeTypes; + } + if (modifiedParts & ModifyCollectionCommand::CachePolicy) { + stream << cachePolicy; + } + if (modifiedParts & ModifyCollectionCommand::PersistentSearch) { + stream << persistentSearchQuery + << persistentSearchCols + << persistentSearchRemote + << persistentSearchRecursive; + } + if (modifiedParts & ModifyCollectionCommand::RemovedAttributes) { + stream << removedAttributes; + } + if (modifiedParts & ModifyCollectionCommand::Attributes) { + stream << attributes; + } + if (modifiedParts & ModifyCollectionCommand::ListPreferences) { + stream << enabled + << sync + << display + << index; + } + if (modifiedParts & ModifyCollectionCommand::Referenced) { + stream << referenced; + } + return stream; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + CommandPrivate::deserialize(stream) + >> collection + >> modifiedParts; + + if (modifiedParts & ModifyCollectionCommand::Name) { + stream >> name; + } + if (modifiedParts & ModifyCollectionCommand::RemoteID) { + stream >> remoteId; + } + if (modifiedParts & ModifyCollectionCommand::RemoteRevision) { + stream >> remoteRev; + } + if (modifiedParts & ModifyCollectionCommand::ParentID) { + stream >> parentId; + } + if (modifiedParts & ModifyCollectionCommand::MimeTypes) { + stream >> mimeTypes; + } + if (modifiedParts & ModifyCollectionCommand::CachePolicy) { + stream >> cachePolicy; + } + if (modifiedParts & ModifyCollectionCommand::PersistentSearch) { + stream >> persistentSearchQuery + >> persistentSearchCols + >> persistentSearchRemote + >> persistentSearchRecursive; + } + if (modifiedParts & ModifyCollectionCommand::RemovedAttributes) { + stream >> removedAttributes; + } + if (modifiedParts & ModifyCollectionCommand::Attributes) { + stream >> attributes; + } + if (modifiedParts & ModifyCollectionCommand::ListPreferences) { + stream >> enabled + >> sync + >> display + >> index; + } + if (modifiedParts & ModifyCollectionCommand::Referenced) { + stream >> referenced; + } + return stream; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + QStringList mps; + + if (modifiedParts & ModifyCollectionCommand::Name) { + mps << QStringLiteral("Name"); + } + if (modifiedParts & ModifyCollectionCommand::RemoteID) { + mps << QStringLiteral("Remote ID"); + } + if (modifiedParts & ModifyCollectionCommand::RemoteRevision) { + mps << QStringLiteral("Remote Revision"); + } + if (modifiedParts & ModifyCollectionCommand::ParentID) { + mps << QStringLiteral("Parent ID"); + } + if (modifiedParts & ModifyCollectionCommand::MimeTypes) { + mps << QStringLiteral("Mimetypes"); + } + if (modifiedParts & ModifyCollectionCommand::CachePolicy) { + mps << QStringLiteral("Cache Policy"); + } + if (modifiedParts & ModifyCollectionCommand::PersistentSearch) { + mps << QStringLiteral("Persistent Search"); + } + if (modifiedParts & ModifyCollectionCommand::RemovedAttributes) { + mps << QStringLiteral("Remote Attributes"); + } + if (modifiedParts & ModifyCollectionCommand::Attributes) { + mps << QStringLiteral("Attributes"); + } + if (modifiedParts & ModifyCollectionCommand::ListPreferences) { + mps << QStringLiteral("List Preferences"); + } + if (modifiedParts & ModifyCollectionCommand::Referenced) { + mps << QStringLiteral("Referenced"); + } + + CommandPrivate::debugString(blck); + blck.write("Collection", collection); + blck.write("Modified Parts", mps); + if (modifiedParts & ModifyCollectionCommand::Name) { + blck.write("Name", name); + } + if (modifiedParts & ModifyCollectionCommand::RemoteID) { + blck.write("Remote ID", remoteId); + } + if (modifiedParts & ModifyCollectionCommand::RemoteRevision) { + blck.write("Remote Revision", remoteRev); + } + if (modifiedParts & ModifyCollectionCommand::ParentID) { + blck.write("Parent ID", parentId); + } + if (modifiedParts & ModifyCollectionCommand::MimeTypes) { + blck.write("Mimetypes", mimeTypes); + } + if (modifiedParts & ModifyCollectionCommand::CachePolicy) { + blck.beginBlock("Cache Policy"); + cachePolicy.debugString(blck); + blck.endBlock(); + } + if (modifiedParts & ModifyCollectionCommand::PersistentSearch) { + blck.beginBlock("Persistent Search"); + blck.write("Query", persistentSearchQuery); + blck.write("Cols", persistentSearchCols); + blck.write("Remote", persistentSearchRemote); + blck.write("Recursive", persistentSearchRecursive); + blck.endBlock(); + } + if (modifiedParts & ModifyCollectionCommand::RemovedAttributes) { + blck.write("Removed Attributes", removedAttributes); + } + if (modifiedParts & ModifyCollectionCommand::Attributes) { + blck.write("Attributes", attributes); + } + if (modifiedParts & ModifyCollectionCommand::ListPreferences) { + blck.write("Sync", sync); + blck.write("Display", display); + blck.write("Index", index); + blck.write("Enabled", enabled); + } + if (modifiedParts & ModifyCollectionCommand::Referenced) { + blck.write("Referenced", referenced); + } + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new ModifyCollectionCommandPrivate(*this); + } + + Scope collection; + QStringList mimeTypes; + Protocol::CachePolicy cachePolicy; + QString name; + QString remoteId; + QString remoteRev; + QString persistentSearchQuery; + QVector persistentSearchCols; + QSet removedAttributes; + Attributes attributes; + qint64 parentId; + Tristate sync; + Tristate display; + Tristate index; + bool enabled; + bool referenced; + bool persistentSearchRemote; + bool persistentSearchRecursive; + + ModifyCollectionCommand::ModifiedParts modifiedParts; +}; + + + + +AKONADI_DECLARE_PRIVATE(ModifyCollectionCommand) + +ModifyCollectionCommand::ModifyCollectionCommand() + : Command(new ModifyCollectionCommandPrivate) +{ +} + +ModifyCollectionCommand::ModifyCollectionCommand(const Scope &collection) + : Command(new ModifyCollectionCommandPrivate(collection)) +{ +} + +ModifyCollectionCommand::ModifyCollectionCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::ModifyCollection); +} + +Scope ModifyCollectionCommand::collection() const +{ + return d_func()->collection; +} + +ModifyCollectionCommand::ModifiedParts ModifyCollectionCommand::modifiedParts() const +{ + return d_func()->modifiedParts; +} + +void ModifyCollectionCommand::setParentId(qint64 parentId) +{ + d_func()->parentId = parentId; +} +qint64 ModifyCollectionCommand::parentId() const +{ + return d_func()->parentId; +} + +void ModifyCollectionCommand::setMimeTypes(const QStringList &mimeTypes) +{ + d_func()->modifiedParts |= MimeTypes; + d_func()->modifiedParts |= PersistentSearch; + d_func()->mimeTypes = mimeTypes; +} +QStringList ModifyCollectionCommand::mimeTypes() const +{ + return d_func()->mimeTypes; +} + +void ModifyCollectionCommand::setCachePolicy(const Protocol::CachePolicy &cachePolicy) +{ + d_func()->modifiedParts |= CachePolicy; + d_func()->cachePolicy = cachePolicy; +} +Protocol::CachePolicy ModifyCollectionCommand::cachePolicy() const +{ + return d_func()->cachePolicy; +} + +void ModifyCollectionCommand::setName(const QString &name) +{ + d_func()->modifiedParts |= Name; + d_func()->name = name; +} +QString ModifyCollectionCommand::name() const +{ + return d_func()->name; +} + +void ModifyCollectionCommand::setRemoteId(const QString &remoteId) +{ + d_func()->modifiedParts |= RemoteID; + d_func()->remoteId = remoteId; +} +QString ModifyCollectionCommand::remoteId() const +{ + return d_func()->remoteId; +} + +void ModifyCollectionCommand::setRemoteRevision(const QString &remoteRevision) +{ + d_func()->modifiedParts |= RemoteRevision; + d_func()->remoteRev = remoteRevision; +} +QString ModifyCollectionCommand::remoteRevision() const +{ + return d_func()->remoteRev; +} + +void ModifyCollectionCommand::setPersistentSearchQuery(const QString &query) +{ + d_func()->modifiedParts |= PersistentSearch; + d_func()->persistentSearchQuery = query; +} +QString ModifyCollectionCommand::persistentSearchQuery() const +{ + return d_func()->persistentSearchQuery; +} + +void ModifyCollectionCommand::setPersistentSearchCollections(const QVector &cols) +{ + d_func()->modifiedParts |= PersistentSearch; + d_func()->persistentSearchCols = cols; +} +QVector ModifyCollectionCommand::persistentSearchCollections() const +{ + return d_func()->persistentSearchCols; +} + +void ModifyCollectionCommand::setPersistentSearchRemote(bool remote) +{ + d_func()->modifiedParts |= PersistentSearch; + d_func()->persistentSearchRemote = remote; +} +bool ModifyCollectionCommand::persistentSearchRemote() const +{ + return d_func()->persistentSearchRemote; +} + +void ModifyCollectionCommand::setPersistentSearchRecursive(bool recursive) +{ + d_func()->modifiedParts |= PersistentSearch; + d_func()->persistentSearchRecursive = recursive; +} +bool ModifyCollectionCommand::persistentSearchRecursive() const +{ + return d_func()->persistentSearchRecursive; +} + +void ModifyCollectionCommand::setRemovedAttributes(const QSet &removedAttrs) +{ + d_func()->modifiedParts |= RemovedAttributes; + d_func()->removedAttributes = removedAttrs; +} +QSet ModifyCollectionCommand::removedAttributes() const +{ + return d_func()->removedAttributes; +} + +void ModifyCollectionCommand::setAttributes(const Protocol::Attributes &attributes) +{ + d_func()->modifiedParts |= Attributes; + d_func()->attributes = attributes; +} +Attributes ModifyCollectionCommand::attributes() const +{ + return d_func()->attributes; +} + +void ModifyCollectionCommand::setEnabled(bool enabled) +{ + d_func()->modifiedParts |= ListPreferences; + d_func()->enabled = enabled; +} +bool ModifyCollectionCommand::enabled() const +{ + return d_func()->enabled; +} + +void ModifyCollectionCommand::setSyncPref(Tristate sync) +{ + d_func()->modifiedParts |= ListPreferences; + d_func()->sync = sync; +} +Tristate ModifyCollectionCommand::syncPref() const +{ + return d_func()->sync; +} + +void ModifyCollectionCommand::setDisplayPref(Tristate display) +{ + d_func()->modifiedParts |= ListPreferences; + d_func()->display = display; +} +Tristate ModifyCollectionCommand::displayPref() const +{ + return d_func()->display; +} + +void ModifyCollectionCommand::setIndexPref(Tristate index) +{ + d_func()->modifiedParts |= ListPreferences; + d_func()->index = index; +} +Tristate ModifyCollectionCommand::indexPref() const +{ + return d_func()->index; +} + +void ModifyCollectionCommand::setReferenced(bool referenced) +{ + d_func()->modifiedParts |= Referenced; + d_func()->referenced = referenced; +} +bool ModifyCollectionCommand::referenced() const +{ + return d_func()->referenced; +} + +DataStream &operator<<(DataStream &stream, const ModifyCollectionCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, ModifyCollectionCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/****************************************************************************/ + + + +ModifyCollectionResponse::ModifyCollectionResponse() + : Response(new ResponsePrivate(Command::ModifyCollection)) +{ +} + +ModifyCollectionResponse::ModifyCollectionResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::ModifyCollection); +} + + + +/****************************************************************************/ + + + + +class MoveCollectionCommandPrivate : public CommandPrivate +{ +public: + MoveCollectionCommandPrivate(const Scope &collection = Scope(), + const Scope &dest = Scope()) + : CommandPrivate(Command::MoveCollection) + , collection(collection) + , dest(dest) + {} + MoveCollectionCommandPrivate(const MoveCollectionCommandPrivate &other) + : CommandPrivate(other) + , collection(other.collection) + , dest(other.dest) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(collection) + && COMPARE(dest); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << collection + << dest; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> collection + >> dest; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Collection", collection); + blck.write("Destination", dest); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new MoveCollectionCommandPrivate(*this); + } + + Scope collection; + Scope dest; +}; + + + + +AKONADI_DECLARE_PRIVATE(MoveCollectionCommand) + +MoveCollectionCommand::MoveCollectionCommand() + : Command(new MoveCollectionCommandPrivate) +{ +} + +MoveCollectionCommand::MoveCollectionCommand(const Scope &collection, + const Scope &destination) + : Command(new MoveCollectionCommandPrivate(collection, destination)) +{ +} + +MoveCollectionCommand::MoveCollectionCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::MoveCollection); +} + +Scope MoveCollectionCommand::collection() const +{ + return d_func()->collection; +} +Scope MoveCollectionCommand::destination() const +{ + return d_func()->dest; +} + +DataStream &operator<<(DataStream &stream, const MoveCollectionCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, MoveCollectionCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +MoveCollectionResponse::MoveCollectionResponse() + : Response(new ResponsePrivate(Command::MoveCollection)) +{ +} + +MoveCollectionResponse::MoveCollectionResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::MoveCollection); +} + + + +/****************************************************************************/ + + + + +class SearchCommandPrivate : public CommandPrivate +{ +public: + SearchCommandPrivate() + : CommandPrivate(Command::Search) + , recursive(false) + , remote(false) + {} + SearchCommandPrivate(const SearchCommandPrivate &other) + : CommandPrivate(other) + , mimeTypes(other.mimeTypes) + , collections(other.collections) + , query(other.query) + , fetchScope(other.fetchScope) + , recursive(other.recursive) + , remote(other.remote) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(recursive) && COMPARE(remote) + && COMPARE(mimeTypes) + && COMPARE(collections) + && COMPARE(query) + && COMPARE(fetchScope); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << mimeTypes + << collections + << query + << fetchScope + << recursive + << remote; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> mimeTypes + >> collections + >> query + >> fetchScope + >> recursive + >> remote; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Query", query); + blck.write("Collections", collections); + blck.write("Mimetypes", mimeTypes); + blck.beginBlock("Fetch Scope"); + fetchScope.debugString(blck); + blck.endBlock(); + blck.write("Recursive", recursive); + blck.write("Remote", remote); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new SearchCommandPrivate(*this); + } + + QStringList mimeTypes; + QVector collections; + QString query; + FetchScope fetchScope; + bool recursive; + bool remote; +}; + + + + +AKONADI_DECLARE_PRIVATE(SearchCommand) + +SearchCommand::SearchCommand() + : Command(new SearchCommandPrivate) +{ +} + +SearchCommand::SearchCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::Search); +} + +void SearchCommand::setMimeTypes(const QStringList &mimeTypes) +{ + d_func()->mimeTypes = mimeTypes; +} +QStringList SearchCommand::mimeTypes() const +{ + return d_func()->mimeTypes; +} + +void SearchCommand::setCollections(const QVector &collections) +{ + d_func()->collections = collections; +} +QVector SearchCommand::collections() const +{ + return d_func()->collections; +} + +void SearchCommand::setQuery(const QString &query) +{ + d_func()->query = query; +} +QString SearchCommand::query() const +{ + return d_func()->query; +} + +void SearchCommand::setFetchScope(const FetchScope &fetchScope) +{ + d_func()->fetchScope = fetchScope; +} +FetchScope SearchCommand::fetchScope() const +{ + return d_func()->fetchScope; +} + +void SearchCommand::setRecursive(bool recursive) +{ + d_func()->recursive = recursive; +} +bool SearchCommand::recursive() const +{ + return d_func()->recursive; +} + +void SearchCommand::setRemote(bool remote) +{ + d_func()->remote = remote; +} +bool SearchCommand::remote() const +{ + return d_func()->remote; +} + +DataStream &operator<<(DataStream &stream, const SearchCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, SearchCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + +SearchResponse::SearchResponse() + : Response(new ResponsePrivate(Command::Search)) +{ +} + +SearchResponse::SearchResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::Search); +} + + + +/****************************************************************************/ + + + + +class SearchResultCommandPrivate : public CommandPrivate +{ +public: + SearchResultCommandPrivate(const QByteArray &searchId = QByteArray(), + qint64 collectionId = -1, + const Scope &result = Scope()) + : CommandPrivate(Command::SearchResult) + , searchId(searchId) + , result(result) + , collectionId(collectionId) + {} + SearchResultCommandPrivate(const SearchResultCommandPrivate &other) + : CommandPrivate(other) + , searchId(other.searchId) + , result(other.result) + , collectionId(other.collectionId) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(searchId) + && COMPARE(collectionId) + && COMPARE(result); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << searchId + << collectionId + << result; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> searchId + >> collectionId + >> result; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Search ID", searchId); + blck.write("Collection ID", collectionId); + blck.write("Result", result); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new SearchResultCommandPrivate(*this); + } + + QByteArray searchId; + Scope result; + qint64 collectionId; +}; + + + + +AKONADI_DECLARE_PRIVATE(SearchResultCommand) + +SearchResultCommand::SearchResultCommand() + : Command(new SearchResultCommandPrivate) +{ +} + +SearchResultCommand::SearchResultCommand(const QByteArray &searchId, + qint64 collectionId, + const Scope &result) + : Command(new SearchResultCommandPrivate(searchId, collectionId, result)) +{ +} + +SearchResultCommand::SearchResultCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::SearchResult); +} + +QByteArray SearchResultCommand::searchId() const +{ + return d_func()->searchId; +} + +qint64 SearchResultCommand::collectionId() const +{ + return d_func()->collectionId; +} + +Scope SearchResultCommand::result() const +{ + return d_func()->result; +} + +DataStream &operator<<(DataStream &stream, const SearchResultCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, SearchResultCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/****************************************************************************/ + + + + +SearchResultResponse::SearchResultResponse() + : Response(new ResponsePrivate(Command::SearchResult)) +{ +} + +SearchResultResponse::SearchResultResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::SearchResult); +} + + + +/****************************************************************************/ + + + + +class StoreSearchCommandPrivate : public CommandPrivate +{ +public: + StoreSearchCommandPrivate() + : CommandPrivate(Command::StoreSearch) + , remote(false) + , recursive(false) + {} + StoreSearchCommandPrivate(const StoreSearchCommandPrivate &other) + : CommandPrivate(other) + , name(other.name) + , query(other.query) + , mimeTypes(other.mimeTypes) + , queryCols(other.queryCols) + , remote(other.remote) + , recursive(other.recursive) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(remote) && COMPARE(recursive) + && COMPARE(name) + && COMPARE(query) + && COMPARE(mimeTypes) + && COMPARE(queryCols); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << name + << query + << mimeTypes + << queryCols + << remote + << recursive; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> name + >> query + >> mimeTypes + >> queryCols + >> remote + >> recursive; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Name", name); + blck.write("Query", query); + blck.write("Mimetypes", mimeTypes); + blck.write("Query Collections", queryCols); + blck.write("Remote", remote); + blck.write("Recursive", recursive); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new StoreSearchCommandPrivate(*this); + } + + QString name; + QString query; + QStringList mimeTypes; + QVector queryCols; + bool remote; + bool recursive; +}; + + + + +AKONADI_DECLARE_PRIVATE(StoreSearchCommand) + +StoreSearchCommand::StoreSearchCommand() + : Command(new StoreSearchCommandPrivate) +{ +} + +StoreSearchCommand::StoreSearchCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::StoreSearch); +} + +void StoreSearchCommand::setName(const QString &name) +{ + d_func()->name = name; +} +QString StoreSearchCommand::name() const +{ + return d_func()->name; +} + +void StoreSearchCommand::setQuery(const QString &query) +{ + d_func()->query = query; +} +QString StoreSearchCommand::query() const +{ + return d_func()->query; +} + +void StoreSearchCommand::setMimeTypes(const QStringList &mimeTypes) +{ + d_func()->mimeTypes = mimeTypes; +} +QStringList StoreSearchCommand::mimeTypes() const +{ + return d_func()->mimeTypes; +} + +void StoreSearchCommand::setQueryCollections(const QVector &queryCols) +{ + d_func()->queryCols = queryCols; +} +QVector StoreSearchCommand::queryCollections() const +{ + return d_func()->queryCols; +} + +void StoreSearchCommand::setRemote(bool remote) +{ + d_func()->remote = remote; +} +bool StoreSearchCommand::remote() const +{ + return d_func()->remote; +} + +void StoreSearchCommand::setRecursive(bool recursive) +{ + d_func()->recursive = recursive; +} +bool StoreSearchCommand::recursive() const +{ + return d_func()->recursive; +} + +DataStream &operator<<(DataStream &stream, const StoreSearchCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, StoreSearchCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +StoreSearchResponse::StoreSearchResponse() + : Response(new ResponsePrivate(Command::StoreSearch)) +{ +} + +StoreSearchResponse::StoreSearchResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::StoreSearch); +} + + + +/****************************************************************************/ + + + + +class CreateTagCommandPrivate : public CommandPrivate +{ +public: + CreateTagCommandPrivate() + : CommandPrivate(Command::CreateTag) + , parentId(-1) + , merge(false) + {} + CreateTagCommandPrivate(const CreateTagCommandPrivate &other) + : CommandPrivate(other) + , gid(other.gid) + , remoteId(other.remoteId) + , type(other.type) + , attributes(other.attributes) + , parentId(other.parentId) + , merge(other.merge) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(parentId) + && COMPARE(merge) + && COMPARE(gid) + && COMPARE(remoteId) + && COMPARE(type) + && COMPARE(attributes); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << gid + << remoteId + << type + << attributes + << parentId + << merge; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> gid + >> remoteId + >> type + >> attributes + >> parentId + >> merge; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Merge", merge); + blck.write("GID", gid); + blck.write("Remote ID", remoteId); + blck.write("Parent ID", parentId); + blck.write("Type", type); + blck.write("Attributes", attributes); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new CreateTagCommandPrivate(*this); + } + + QByteArray gid; + QByteArray remoteId; + QByteArray type; + Attributes attributes; + qint64 parentId; + bool merge; +}; + + + + +AKONADI_DECLARE_PRIVATE(CreateTagCommand) + +CreateTagCommand::CreateTagCommand() + : Command(new CreateTagCommandPrivate) +{ +} + +CreateTagCommand::CreateTagCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::CreateTag); +} + +void CreateTagCommand::setGid(const QByteArray &gid) +{ + d_func()->gid = gid; +} +QByteArray CreateTagCommand::gid() const +{ + return d_func()->gid; +} + +void CreateTagCommand::setRemoteId(const QByteArray &remoteId) +{ + d_func()->remoteId = remoteId; +} +QByteArray CreateTagCommand::remoteId() const +{ + return d_func()->remoteId; +} + +void CreateTagCommand::setType(const QByteArray &type) +{ + d_func()->type = type; +} +QByteArray CreateTagCommand::type() const +{ + return d_func()->type; +} + +void CreateTagCommand::setParentId(qint64 parentId) +{ + d_func()->parentId = parentId; +} +qint64 CreateTagCommand::parentId() const +{ + return d_func()->parentId; +} + +void CreateTagCommand::setMerge(bool merge) +{ + d_func()->merge = merge; +} +bool CreateTagCommand::merge() const +{ + return d_func()->merge; +} + +void CreateTagCommand::setAttributes(const Attributes &attributes) +{ + d_func()->attributes = attributes; +} +Attributes CreateTagCommand::attributes() const +{ + return d_func()->attributes; +} + +DataStream &operator<<(DataStream &stream, const CreateTagCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, CreateTagCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +CreateTagResponse::CreateTagResponse() + : Response(new ResponsePrivate(Command::CreateTag)) +{ +} + +CreateTagResponse::CreateTagResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::CreateTag); +} + + + + +/****************************************************************************/ + + + + +class DeleteTagCommandPrivate : public CommandPrivate +{ +public: + DeleteTagCommandPrivate(const Scope &tag = Scope()) + : CommandPrivate(Command::DeleteTag) + , tag(tag) + {} + DeleteTagCommandPrivate(const DeleteTagCommandPrivate &other) + : CommandPrivate(other) + , tag(other.tag) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(tag); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << tag; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> tag; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Tag", tag); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new DeleteTagCommandPrivate(*this); + } + + Scope tag; +}; + + + + +AKONADI_DECLARE_PRIVATE(DeleteTagCommand) + +DeleteTagCommand::DeleteTagCommand() + : Command(new DeleteTagCommandPrivate) +{ +} + +DeleteTagCommand::DeleteTagCommand(const Scope &tag) + : Command(new DeleteTagCommandPrivate(tag)) +{ +} + +DeleteTagCommand::DeleteTagCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::DeleteTag); +} + +Scope DeleteTagCommand::tag() const +{ + return d_func()->tag; +} + +DataStream &operator<<(DataStream &stream, const DeleteTagCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, DeleteTagCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +DeleteTagResponse::DeleteTagResponse() + : Response(new ResponsePrivate(Command::DeleteTag)) +{ +} + +DeleteTagResponse::DeleteTagResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::DeleteTag); +} + + + + +/****************************************************************************/ + + + + + +class ModifyTagCommandPrivate : public CommandPrivate +{ +public: + ModifyTagCommandPrivate(qint64 tagId = -1) + : CommandPrivate(Command::ModifyTag) + , tagId(tagId) + , parentId(-1) + , modifiedParts(ModifyTagCommand::None) + {} + ModifyTagCommandPrivate(const ModifyTagCommandPrivate &other) + : CommandPrivate(other) + , type(other.type) + , remoteId(other.remoteId) + , removedAttributes(other.removedAttributes) + , attributes(other.attributes) + , tagId(other.tagId) + , parentId(other.parentId) + , modifiedParts(other.modifiedParts) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(modifiedParts) + && COMPARE(parentId) + && COMPARE(tagId) + && COMPARE(type) + && COMPARE(remoteId) + && COMPARE(removedAttributes); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + CommandPrivate::serialize(stream) + << tagId + << modifiedParts; + if (modifiedParts & ModifyTagCommand::ParentId) { + stream << parentId; + } + if (modifiedParts & ModifyTagCommand::Type) { + stream << type; + } + if (modifiedParts & ModifyTagCommand::RemoteId) { + stream << remoteId; + } + if (modifiedParts & ModifyTagCommand::RemovedAttributes) { + stream << removedAttributes; + } + if (modifiedParts & ModifyTagCommand::Attributes) { + stream << attributes; + } + return stream; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + CommandPrivate::deserialize(stream) + >> tagId + >> modifiedParts; + if (modifiedParts & ModifyTagCommand::ParentId) { + stream >> parentId; + } + if (modifiedParts & ModifyTagCommand::Type) { + stream >> type; + } + if (modifiedParts & ModifyTagCommand::RemoteId) { + stream >> remoteId; + } + if (modifiedParts & ModifyTagCommand::RemovedAttributes) { + stream >> removedAttributes; + } + if (modifiedParts & ModifyTagCommand::Attributes) { + stream >> attributes; + } + return stream; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + QStringList mps; + if (modifiedParts & ModifyTagCommand::ParentId) { + mps << QStringLiteral("Parent ID"); + } + if (modifiedParts & ModifyTagCommand::Type) { + mps << QStringLiteral("Type"); + } + if (modifiedParts & ModifyTagCommand::RemoteId) { + mps << QStringLiteral("Remote ID"); + } + if (modifiedParts & ModifyTagCommand::RemovedAttributes) { + mps << QStringLiteral("Removed Attributes"); + } + if (modifiedParts & ModifyTagCommand::Attributes) { + mps << QStringLiteral("Attributes"); + } + + CommandPrivate::debugString(blck); + blck.write("Tag ID", tagId); + blck.write("Modified Parts", mps); + if (modifiedParts & ModifyTagCommand::ParentId) { + blck.write("Parent ID", parentId); + } + if (modifiedParts & ModifyTagCommand::Type) { + blck.write("Type", type); + } + if (modifiedParts & ModifyTagCommand::RemoteId) { + blck.write("Remote ID", remoteId); + } + if (modifiedParts & ModifyTagCommand::RemovedAttributes) { + blck.write("Removed Attributes", removedAttributes); + } + if (modifiedParts & ModifyTagCommand::Attributes) { + blck.write("Attributes", attributes); + } + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new ModifyTagCommandPrivate(*this); + } + + QByteArray type; + QByteArray remoteId; + QSet removedAttributes; + Attributes attributes; + qint64 tagId; + qint64 parentId; + ModifyTagCommand::ModifiedParts modifiedParts; +}; + + + + +AKONADI_DECLARE_PRIVATE(ModifyTagCommand) + +ModifyTagCommand::ModifyTagCommand() + : Command(new ModifyTagCommandPrivate) +{ +} + +ModifyTagCommand::ModifyTagCommand(qint64 id) + : Command(new ModifyTagCommandPrivate(id)) +{ +} + +ModifyTagCommand::ModifyTagCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::ModifyTag); +} + +qint64 ModifyTagCommand::tagId() const +{ + return d_func()->tagId; +} + +ModifyTagCommand::ModifiedParts ModifyTagCommand::modifiedParts() const +{ + return d_func()->modifiedParts; +} + +void ModifyTagCommand::setParentId(qint64 parentId) +{ + d_func()->modifiedParts |= ParentId; + d_func()->parentId = parentId; +} +qint64 ModifyTagCommand::parentId() const +{ + return d_func()->parentId; +} + +void ModifyTagCommand::setType(const QByteArray &type) +{ + d_func()->modifiedParts |= Type; + d_func()->type = type; +} +QByteArray ModifyTagCommand::type() const +{ + return d_func()->type; +} + +void ModifyTagCommand::setRemoteId(const QByteArray &remoteId) +{ + d_func()->modifiedParts |= RemoteId; + d_func()->remoteId = remoteId; +} +QByteArray ModifyTagCommand::remoteId() const +{ + return d_func()->remoteId; +} + +void ModifyTagCommand::setRemovedAttributes(const QSet &removed) +{ + d_func()->modifiedParts |= RemovedAttributes; + d_func()->removedAttributes = removed; +} +QSet ModifyTagCommand::removedAttributes() const +{ + return d_func()->removedAttributes; +} + +void ModifyTagCommand::setAttributes(const Protocol::Attributes &attributes) +{ + d_func()->modifiedParts |= Attributes; + d_func()->attributes = attributes; +} +Attributes ModifyTagCommand::attributes() const +{ + return d_func()->attributes; +} + +DataStream &operator<<(DataStream &stream, const ModifyTagCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, ModifyTagCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/****************************************************************************/ + + + + +ModifyTagResponse::ModifyTagResponse() + : Response(new ResponsePrivate(Command::ModifyTag)) +{ +} + +ModifyTagResponse::ModifyTagResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::ModifyTag); +} + + + + +/****************************************************************************/ + + + + +class ModifyRelationCommandPrivate : public CommandPrivate +{ +public: + ModifyRelationCommandPrivate(qint64 left = -1, qint64 right = -1, + const QByteArray &type = QByteArray(), + const QByteArray &remoteId = QByteArray()) + : CommandPrivate(Command::ModifyRelation) + , type(type) + , remoteId(remoteId) + , left(left) + , right(right) + {} + ModifyRelationCommandPrivate(const ModifyRelationCommandPrivate &other) + : CommandPrivate(other) + , type(other.type) + , remoteId(other.remoteId) + , left(other.left) + , right(other.right) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(left) + && COMPARE(right) + && COMPARE(type) + && COMPARE(remoteId); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << left + << right + << type + << remoteId; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> left + >> right + >> type + >> remoteId; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Left", left); + blck.write("Right", right); + blck.write("Type", type); + blck.write("Remote ID", remoteId); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new ModifyRelationCommandPrivate(*this); + } + + QByteArray type; + QByteArray remoteId; + qint64 left; + qint64 right; +}; + + + + +AKONADI_DECLARE_PRIVATE(ModifyRelationCommand) + +ModifyRelationCommand::ModifyRelationCommand() + : Command(new ModifyRelationCommandPrivate) +{ +} + +ModifyRelationCommand::ModifyRelationCommand(qint64 left, qint64 right, + const QByteArray &type, + const QByteArray &remoteId) + : Command(new ModifyRelationCommandPrivate(left, right, type, remoteId)) +{ +} + +ModifyRelationCommand::ModifyRelationCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::ModifyRelation); +} + +void ModifyRelationCommand::setLeft(qint64 left) +{ + d_func()->left = left; +} +qint64 ModifyRelationCommand::left() const +{ + return d_func()->left; +} + +void ModifyRelationCommand::setRight(qint64 right) +{ + d_func()->right = right; +} +qint64 ModifyRelationCommand::right() const +{ + return d_func()->right; +} + +void ModifyRelationCommand::setType(const QByteArray &type) +{ + d_func()->type = type; +} +QByteArray ModifyRelationCommand::type() const +{ + return d_func()->type; +} + +void ModifyRelationCommand::setRemoteId(const QByteArray &remoteId) +{ + d_func()->remoteId = remoteId; +} +QByteArray ModifyRelationCommand::remoteId() const +{ + return d_func()->remoteId; +} + +DataStream &operator<<(DataStream &stream, const ModifyRelationCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, ModifyRelationCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +ModifyRelationResponse::ModifyRelationResponse() + : Response(new ResponsePrivate(Command::ModifyRelation)) +{ +} + +ModifyRelationResponse::ModifyRelationResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::ModifyRelation); +} + + + +/****************************************************************************/ + + + + +class RemoveRelationsCommandPrivate : public CommandPrivate +{ +public: + RemoveRelationsCommandPrivate(qint64 left = -1, qint64 right = -1, + const QByteArray &type = QByteArray()) + : CommandPrivate(Command::RemoveRelations) + , left(left) + , right(right) + , type(type) + {} + RemoveRelationsCommandPrivate(const RemoveRelationsCommandPrivate &other) + : CommandPrivate(other) + , left(other.left) + , right(other.right) + , type(other.type) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(left) + && COMPARE(right) + && COMPARE(type); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << left + << right + << type; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> left + >> right + >> type; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Left", left); + blck.write("Right", right); + blck.write("Type", type); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new RemoveRelationsCommandPrivate(*this); + } + + qint64 left; + qint64 right; + QByteArray type; +}; + + + + +AKONADI_DECLARE_PRIVATE(RemoveRelationsCommand) + +RemoveRelationsCommand::RemoveRelationsCommand() + : Command(new RemoveRelationsCommandPrivate) +{ +} + +RemoveRelationsCommand::RemoveRelationsCommand(qint64 left, qint64 right, const QByteArray &type) + : Command(new RemoveRelationsCommandPrivate(left, right, type)) +{ +} + +RemoveRelationsCommand::RemoveRelationsCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::RemoveRelations); +} + +void RemoveRelationsCommand::setLeft(qint64 left) +{ + d_func()->left = left; +} +qint64 RemoveRelationsCommand::left() const +{ + return d_func()->left; +} + +void RemoveRelationsCommand::setRight(qint64 right) +{ + d_func()->right = right; +} +qint64 RemoveRelationsCommand::right() const +{ + return d_func()->right; +} + +void RemoveRelationsCommand::setType(const QByteArray &type) +{ + d_func()->type = type; +} +QByteArray RemoveRelationsCommand::type() const +{ + return d_func()->type; +} + +DataStream &operator<<(DataStream &stream, const RemoveRelationsCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, RemoveRelationsCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +RemoveRelationsResponse::RemoveRelationsResponse() + : Response(new ResponsePrivate(Command::RemoveRelations)) +{ +} + +RemoveRelationsResponse::RemoveRelationsResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::RemoveRelations); +} + + + +/****************************************************************************/ + + + + +class SelectResourceCommandPrivate : public CommandPrivate +{ +public: + SelectResourceCommandPrivate(const QString &resourceId = QString()) + : CommandPrivate(Command::SelectResource) + , resourceId(resourceId) + {} + SelectResourceCommandPrivate(const SelectResourceCommandPrivate &other) + : CommandPrivate(other) + , resourceId(other.resourceId) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(resourceId); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << resourceId; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> resourceId; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Resource ID", resourceId); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new SelectResourceCommandPrivate(*this); + } + + QString resourceId; +}; + + + + +AKONADI_DECLARE_PRIVATE(SelectResourceCommand) + +SelectResourceCommand::SelectResourceCommand() + : Command(new SelectResourceCommandPrivate) +{ +} + +SelectResourceCommand::SelectResourceCommand(const QString &resourceId) + : Command(new SelectResourceCommandPrivate(resourceId)) +{ +} + +SelectResourceCommand::SelectResourceCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::SelectResource); +} + +QString SelectResourceCommand::resourceId() const +{ + return d_func()->resourceId; +} + +DataStream &operator<<(DataStream &stream, const SelectResourceCommand &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, SelectResourceCommand &command) +{ + return command.d_func()->deserialize(stream); +} + + + +/****************************************************************************/ + + + + +SelectResourceResponse::SelectResourceResponse() + : Response(new ResponsePrivate(Command::SelectResource)) +{ +} + +SelectResourceResponse::SelectResourceResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::SelectResource); +} + + + +/****************************************************************************/ + + + + +class StreamPayloadCommandPrivate : public CommandPrivate +{ +public: + StreamPayloadCommandPrivate(const QByteArray &name = QByteArray(), + StreamPayloadCommand::Request request = StreamPayloadCommand::MetaData, + const QString &dest = QString()) + : CommandPrivate(Command::StreamPayload) + , payloadName(name) + , dest(dest) + , request(request) + {} + StreamPayloadCommandPrivate(const StreamPayloadCommandPrivate &other) + : CommandPrivate(other) + , payloadName(other.payloadName) + , dest(other.dest) + , request(other.request) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(request) + && COMPARE(payloadName) + && COMPARE(dest); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << payloadName + << request + << dest; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> payloadName + >> request + >> dest; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + CommandPrivate::debugString(blck); + blck.write("Payload Name", payloadName); + blck.write("Request", request); + blck.write("Destination", dest); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new StreamPayloadCommandPrivate(*this); + } + + QByteArray payloadName; + QString dest; + StreamPayloadCommand::Request request; +}; + + + + +AKONADI_DECLARE_PRIVATE(StreamPayloadCommand) + +StreamPayloadCommand::StreamPayloadCommand() + : Command(new StreamPayloadCommandPrivate) +{ +} + +StreamPayloadCommand::StreamPayloadCommand(const QByteArray &name, Request request, + const QString &dest) + : Command(new StreamPayloadCommandPrivate(name, request, dest)) +{ +} + +StreamPayloadCommand::StreamPayloadCommand(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::StreamPayload); +} + +void StreamPayloadCommand::setPayloadName(const QByteArray &name) +{ + d_func()->payloadName = name; +} +QByteArray StreamPayloadCommand::payloadName() const +{ + return d_func()->payloadName; +} + +void StreamPayloadCommand::setRequest(Request request) +{ + d_func()->request = request; +} +StreamPayloadCommand::Request StreamPayloadCommand::request() const +{ + return d_func()->request; +} + +void StreamPayloadCommand::setDestination(const QString &dest) +{ + d_func()->dest = dest; +} +QString StreamPayloadCommand::destination() const +{ + return d_func()->dest; +} + +DataStream &operator<<(DataStream &stream, const StreamPayloadCommand &command) +{ + return command.d_func()->serialize(stream); + return stream; +} + +DataStream &operator>>(DataStream &stream, StreamPayloadCommand &command) +{ + return command.d_func()->deserialize(stream); + return stream; +} + + + + +/****************************************************************************/ + + + + +class StreamPayloadResponsePrivate : public ResponsePrivate +{ +public: + StreamPayloadResponsePrivate(const QByteArray &payloadName = QByteArray(), + const PartMetaData &metaData = PartMetaData(), + const QByteArray &data = QByteArray()) + : ResponsePrivate(Command::StreamPayload) + , payloadName(payloadName) + , data(data) + , metaData(metaData) + {} + StreamPayloadResponsePrivate(const StreamPayloadResponsePrivate &other) + : ResponsePrivate(other) + , payloadName(other.payloadName) + , data(other.data) + , metaData(other.metaData) + {} + + bool compare(const CommandPrivate* other) const Q_DECL_OVERRIDE + { + return ResponsePrivate::compare(other) + && COMPARE(metaData) + && COMPARE(payloadName) + && COMPARE(data); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return ResponsePrivate::serialize(stream) + << payloadName + << metaData + << data; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return ResponsePrivate::deserialize(stream) + >> payloadName + >> metaData + >> data; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + ResponsePrivate::debugString(blck); + blck.beginBlock("MetaData"); + blck.write("Name", metaData.name()); + blck.write("Size", metaData.size()); + blck.write("Version", metaData.version()); + blck.endBlock(); + blck.write("Data", data); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new StreamPayloadResponsePrivate(*this); + } + + QByteArray payloadName; + QByteArray data; + PartMetaData metaData; +}; + + + + +AKONADI_DECLARE_PRIVATE(StreamPayloadResponse) + +StreamPayloadResponse::StreamPayloadResponse() + : Response(new StreamPayloadResponsePrivate) +{ +} + +StreamPayloadResponse::StreamPayloadResponse(const QByteArray &payloadName, + const PartMetaData &metaData) + : Response(new StreamPayloadResponsePrivate(payloadName, metaData)) +{ +} + +StreamPayloadResponse::StreamPayloadResponse(const QByteArray &payloadName, + const QByteArray &data) + : Response(new StreamPayloadResponsePrivate(payloadName, PartMetaData(), data)) +{ +} + +StreamPayloadResponse::StreamPayloadResponse(const QByteArray &payloadName, + const PartMetaData &metaData, + const QByteArray &data) + : Response(new StreamPayloadResponsePrivate(payloadName, metaData, data)) +{ +} + +StreamPayloadResponse::StreamPayloadResponse(const Command &other) + : Response(other) +{ + checkCopyInvariant(Command::StreamPayload); +} + +void StreamPayloadResponse::setPayloadName(const QByteArray &payloadName) +{ + d_func()->payloadName = payloadName; +} +QByteArray StreamPayloadResponse::payloadName() const +{ + return d_func()->payloadName; +} +void StreamPayloadResponse::setMetaData(const PartMetaData &metaData) +{ + d_func()->metaData = metaData; +} +PartMetaData StreamPayloadResponse::metaData() const +{ + return d_func()->metaData; +} +void StreamPayloadResponse::setData(const QByteArray &data) +{ + d_func()->data = data; +} +QByteArray StreamPayloadResponse::data() const +{ + return d_func()->data; +} + +DataStream &operator<<(DataStream &stream, const StreamPayloadResponse &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, StreamPayloadResponse &command) +{ + return command.d_func()->deserialize(stream); +} + + + + +/******************************************************************************/ + + + + +class ChangeNotificationPrivate : public CommandPrivate +{ +public: + ChangeNotificationPrivate() + : CommandPrivate(Command::ChangeNotification) + , parentCollection(-1) + , parentDestCollection(-1) + , type(ChangeNotification::InvalidType) + , operation(ChangeNotification::InvalidOp) + {} + + ChangeNotificationPrivate(const ChangeNotificationPrivate &other) + : CommandPrivate(other) + , sessionId(other.sessionId) + , items(other.items) + , resource(other.resource) + , destResource(other.destResource) + , parts(other.parts) + , addedFlags(other.addedFlags) + , removedFlags(other.removedFlags) + , addedTags(other.addedTags) + , removedTags(other.removedTags) + , metadata(other.metadata) + , parentCollection(other.parentCollection) + , parentDestCollection(other.parentDestCollection) + , type(other.type) + , operation(other.operation) + {} + + bool compareWithoutOpAndParts(const ChangeNotificationPrivate *other) const + { + return type == other->type + && items == other->items + && sessionId == other->sessionId + && resource == other->resource + && destResource == other->destResource + && parentCollection == other->parentCollection + && parentDestCollection == other->parentDestCollection; + } + + bool compare(const CommandPrivate *other) const Q_DECL_OVERRIDE + { + return CommandPrivate::compare(other) + && COMPARE(operation) + && COMPARE(parts) + && COMPARE(addedFlags) + && COMPARE(removedFlags) + && COMPARE(addedTags) + && COMPARE(removedTags) + && compareWithoutOpAndParts(static_cast(other)); + } + + CommandPrivate *clone() const Q_DECL_OVERRIDE + { + return new ChangeNotificationPrivate(*this); + } + + DataStream &serialize(DataStream &stream) const Q_DECL_OVERRIDE + { + return CommandPrivate::serialize(stream) + << type + << operation + << sessionId + << items + << resource + << destResource + << parts + << addedFlags + << removedFlags + << addedTags + << removedTags + << parentCollection + << parentDestCollection; + } + + DataStream &deserialize(DataStream &stream) Q_DECL_OVERRIDE + { + return CommandPrivate::deserialize(stream) + >> type + >> operation + >> sessionId + >> items + >> resource + >> destResource + >> parts + >> addedFlags + >> removedFlags + >> addedTags + >> removedTags + >> parentCollection + >> parentDestCollection; + } + + void debugString(DebugBlock &blck) const Q_DECL_OVERRIDE + { + blck.write("Type", [this]() -> QString { + switch (type) { + case ChangeNotification::Items: + return QStringLiteral("Items"); + case ChangeNotification::Collections: + return QStringLiteral("Collections"); + case ChangeNotification::Tags: + return QStringLiteral("Tags"); + case ChangeNotification::Relations: + return QStringLiteral("Relations"); + case ChangeNotification::InvalidType: + return QStringLiteral("*INVALID TYPE*"); + } + Q_ASSERT(false); + return QString(); + }()); + blck.write("Operation", [this]() -> QString { + switch (operation) { + case ChangeNotification::Add: + return QStringLiteral("Add"); + case ChangeNotification::Modify: + return QStringLiteral("Modify"); + case ChangeNotification::ModifyFlags: + return QStringLiteral("ModifyFlags"); + case ChangeNotification::ModifyTags: + return QStringLiteral("ModifyTags"); + case ChangeNotification::ModifyRelations: + return QStringLiteral("ModifyRelations"); + case ChangeNotification::Move: + return QStringLiteral("Move"); + case ChangeNotification::Remove: + return QStringLiteral("Remove"); + case ChangeNotification::Link: + return QStringLiteral("Link"); + case ChangeNotification::Unlink: + return QStringLiteral("Unlink"); + case ChangeNotification::Subscribe: + return QStringLiteral("Subscribe"); + case ChangeNotification::Unsubscribe: + return QStringLiteral("Unsubscribe"); + case ChangeNotification::InvalidOp: + return QStringLiteral("*INVALID OPERATION*"); + } + Q_ASSERT(false); + return QString(); + }()); + blck.beginBlock("Items"); + Q_FOREACH (const ChangeNotification::Entity &item, items) { + blck.beginBlock(); + blck.write("ID", item.id); + blck.write("RemoteID", item.remoteId); + blck.write("Remote Revision", item.remoteRevision); + blck.write("Mime Type", item.mimeType); + blck.endBlock(); + } + blck.endBlock(); + blck.write("Session", sessionId); + blck.write("Resource", resource); + blck.write("Destination Resource", destResource); + blck.write("Parent Collection", parentCollection); + blck.write("Parent Destination Collection", parentDestCollection); + blck.write("Parts", parts); + blck.write("Added Flags", addedFlags); + blck.write("Removed Flags", removedFlags); + blck.write("Added Tags", addedTags); + blck.write("Removed Tags", removedTags); + } + + + + QByteArray sessionId; + QMap items; + QByteArray resource; + QByteArray destResource; + QSet parts; + QSet addedFlags; + QSet removedFlags; + QSet addedTags; + QSet removedTags; + + // For internal use only: Akonadi server can add some additional information + // that might be useful when evaluating the notification for example, but + // it is never transferred to clients + QVector metadata; + + qint64 parentCollection; + qint64 parentDestCollection; + ChangeNotification::Type type; + ChangeNotification::Operation operation; +}; + +AKONADI_DECLARE_PRIVATE(ChangeNotification) + + +ChangeNotification::ChangeNotification() + : Command(new ChangeNotificationPrivate) +{ +} + +ChangeNotification::ChangeNotification(const Command &other) + : Command(other) +{ + checkCopyInvariant(Command::ChangeNotification); +} + +bool ChangeNotification::isValid() const +{ + Q_D(const ChangeNotification); + return d->commandType == Command::ChangeNotification + && d->type != InvalidType + && d->operation != InvalidOp; +} + +void ChangeNotification::addEntity(Id id, const QString &remoteId, const QString &remoteRevision, const QString &mimeType) +{ + d_func()->items.insert(id, Entity(id, remoteId, remoteRevision, mimeType)); +} + +void ChangeNotification::setEntities(const QVector &entities) +{ + Q_D(ChangeNotification); + clearEntities(); + Q_FOREACH (const Entity &entity, entities) { + d->items.insert(entity.id, entity); + } +} + +void ChangeNotification::clearEntities() +{ + d_func()->items.clear(); +} + +QMap ChangeNotification::entities() const +{ + return d_func()->items; +} + +ChangeNotification::Entity ChangeNotification::entity(const Id id) const +{ + return d_func()->items.value(id); +} + +QList ChangeNotification::uids() const +{ + return d_func()->items.keys(); +} + +QByteArray ChangeNotification::sessionId() const +{ + return d_func()->sessionId; +} + +void ChangeNotification::setSessionId(const QByteArray &sessionId) +{ + d_func()->sessionId = sessionId; +} + +ChangeNotification::Type ChangeNotification::type() const +{ + return d_func()->type; +} + +void ChangeNotification::setType(Type type) +{ + d_func()->type = type; +} + +ChangeNotification::Operation ChangeNotification::operation() const +{ + return d_func()->operation; +} + +void ChangeNotification::setOperation(Operation operation) +{ + d_func()->operation = operation; +} + +QByteArray ChangeNotification::resource() const +{ + return d_func()->resource; +} + +void ChangeNotification::setResource(const QByteArray &resource) +{ + d_func()->resource = resource; +} + +qint64 ChangeNotification::parentCollection() const +{ + return d_func()->parentCollection; +} + +qint64 ChangeNotification::parentDestCollection() const +{ + return d_func()->parentDestCollection; +} + +void ChangeNotification::setParentCollection(Id parent) +{ + d_func()->parentCollection = parent; +} + +void ChangeNotification::setParentDestCollection(Id parent) +{ + d_func()->parentDestCollection = parent; +} + +void ChangeNotification::setDestinationResource(const QByteArray &destResource) +{ + d_func()->destResource = destResource; +} + +QByteArray ChangeNotification::destinationResource() const +{ + return d_func()->destResource; +} + +QSet ChangeNotification::itemParts() const +{ + return d_func()->parts; +} + +void ChangeNotification::setItemParts(const QSet &parts) +{ + d_func()->parts = parts; +} + +QSet ChangeNotification::addedFlags() const +{ + return d_func()->addedFlags; +} + +void ChangeNotification::setAddedFlags(const QSet &addedFlags) +{ + d_func()->addedFlags = addedFlags; +} + +QSet ChangeNotification::removedFlags() const +{ + return d_func()->removedFlags; +} + +void ChangeNotification::setRemovedFlags(const QSet &removedFlags) +{ + d_func()->removedFlags = removedFlags; +} + +QSet ChangeNotification::addedTags() const +{ + return d_func()->addedTags; +} + +void ChangeNotification::setAddedTags(const QSet &addedTags) +{ + d_func()->addedTags = addedTags; +} + +QSet ChangeNotification::removedTags() const +{ + return d_func()->removedTags; +} + +void ChangeNotification::setRemovedTags(const QSet &removedTags) +{ + d_func()->removedTags = removedTags; +} + +void ChangeNotification::addMetadata(const QByteArray &metadata) +{ + d_func()->metadata.append(metadata); +} + +void ChangeNotification::removeMetadata(const QByteArray &metadata) +{ + d_func()->metadata.removeAll(metadata); +} + +QVector ChangeNotification::metadata() const +{ + return d_func()->metadata; +} + +bool ChangeNotification::appendAndCompress(ChangeNotification::List &list, const ChangeNotification &msg) +{ + //It is likely that compressable notifications are within the last few notifications, so avoid searching a list that is potentially huge + static const int maxCompressionSearchLength = 10; + int searchCounter = 0; + // There are often multiple Collection Modify notifications in the queue, + // so we optimize for this case. + if (msg.type() == ChangeNotification::Collections && msg.operation() == ChangeNotification::Modify) { + typename List::Iterator iter, begin; + // We are iterating from end, since there's higher probability of finding + // matching notification + for (iter = list.end(), begin = list.begin(); iter != begin; ) { + --iter; + if (msg.d_func()->compareWithoutOpAndParts((*iter).d_func())) { + // both are modifications, merge them together and drop the new one + if (msg.operation() == ChangeNotification::Modify && iter->operation() == ChangeNotification::Modify) { + iter->setItemParts(iter->itemParts() + msg.itemParts()); + return false; + } + + // we found Add notification, which means we can drop this modification + if (iter->operation() == ChangeNotification::Add) { + return false; + } + } + searchCounter++; + if (searchCounter >= maxCompressionSearchLength) { + break; + } + } + } + + // All other cases are just append, as the compression becomes too expensive in large batches + list.append(msg); + return true; +} + +DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ChangeNotification::Entity &entity) +{ + return stream << entity.id + << entity.remoteId + << entity.remoteRevision + << entity.mimeType; +} + +DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ChangeNotification::Entity &entity) +{ + return stream >> entity.id + >> entity.remoteId + >> entity.remoteRevision + >> entity.mimeType; +} + +DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ChangeNotification &command) +{ + return command.d_func()->serialize(stream); +} + +DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ChangeNotification &command) +{ + return command.d_func()->deserialize(stream); +} + +} // namespace Protocol +} // namespace Akonadi diff --git a/src/private/protocol_exception_p.h b/src/private/protocol_exception_p.h new file mode 100644 index 0000000..54858a1 --- /dev/null +++ b/src/private/protocol_exception_p.h @@ -0,0 +1,49 @@ +/* + Copyright (c) 2015 Daniel Vrátil + + 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_PROTOCOLEXCEPTION_P_H +#define AKONADI_PROTOCOLEXCEPTION_P_H + +#include "akonadiprivate_export.h" + +#include + +#include + +namespace Akonadi { + +class AKONADIPRIVATE_EXPORT ProtocolException : public std::exception +{ +public: + ProtocolException(const char *what) + : std::exception() + , mWhat(what) + {} + + const char *what() const throw() + { + return mWhat.constData(); + } + +private: + QByteArray mWhat; +}; +} // namespace Akonadi + +#endif diff --git a/src/private/protocol_p.h b/src/private/protocol_p.h new file mode 100644 index 0000000..8d9197a --- /dev/null +++ b/src/private/protocol_p.h @@ -0,0 +1,2287 @@ +/* + Copyright (c) 2007 Volker Krause + Copyright (c) 2015 Daniel Vrátil + + 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_PROTOCOL_P_H +#define AKONADI_PROTOCOL_P_H + +#include "akonadiprivate_export.h" + + +#include +#include +#include +#include + +#include "tristate_p.h" + +class DataStream; +class QString; +class QByteArray; +template class QMap; +template class QSet; +class QDateTime; + +namespace Akonadi +{ +class Scope; +} + +/** + @file protocol_p.h Shared constants used in the communication protocol between + the Akonadi server and its clients. +*/ + +namespace Akonadi +{ + +namespace Server +{ +class NotificationSource; +class NotificationCollector; +} + +namespace Protocol +{ + +// NOTE: Q_DECLARE_PRIVATE does not work with QSharedDataPointer when T is incomplete +#ifndef AKONADI_DECLARE_PRIVATE +#define AKONADI_DECLARE_PRIVATE(Class) \ +Class##Private* d_func(); \ +const Class##Private* d_func() const; \ +friend class Class##Private; +#endif + +typedef QMap Attributes; +class Factory; + +AKONADIPRIVATE_EXPORT int version(); + +class DataStream; +class DebugBlock; +class CommandPrivate; +class AKONADIPRIVATE_EXPORT Command +{ +public: + enum Type : quint8 { + Invalid = 0, + + // Session management + Hello = 1, + Login, + Logout, + + // Transactions + Transaction = 10, + + // Items + CreateItem = 20, + CopyItems, + DeleteItems, + FetchItems, + LinkItems, + ModifyItems, + MoveItems, + + // Collections + CreateCollection = 40, + CopyCollection, + DeleteCollection, + FetchCollections, + FetchCollectionStats, + ModifyCollection, + MoveCollection, + + // Search + Search = 60, + SearchResult, + StoreSearch, + + // Tag + CreateTag = 70, + DeleteTag, + FetchTags, + ModifyTag, + + // Relation + FetchRelations = 80, + ModifyRelation, + RemoveRelations, + + // Resources + SelectResource = 90, + + // Other + StreamPayload = 100, + ChangeNotification, + + _ResponseBit = 0x80 // reserved + }; + + explicit Command(); + Command(Command &&other); + Command(const Command &other); + ~Command(); + + Command &operator=(Command &&other); + Command &operator=(const Command &other); + + bool operator==(const Command &other) const; + bool operator!=(const Command &other) const; + + Type type() const; + bool isValid() const; + bool isResponse() const; + + QString debugString() const; + QString debugString(DebugBlock &blck) const; + +protected: + explicit Command(CommandPrivate *dd); + + QSharedDataPointer d_ptr; + AKONADI_DECLARE_PRIVATE(Command) + +private: + friend class Factory; + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::Command &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::Command &command); +}; + + + + + +class ResponsePrivate; +class AKONADIPRIVATE_EXPORT Response : public Command +{ +public: + explicit Response(); + explicit Response(const Command &other); + + void setError(int code, const QString &message); + bool isError() const; + + int errorCode() const; + QString errorMessage() const; + +protected: + explicit Response(ResponsePrivate *dd); + AKONADI_DECLARE_PRIVATE(Response) + +private: + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::Response &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::Response &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT Factory +{ +public: + static Command command(Command::Type type); + static Response response(Command::Type type); +}; + + + +AKONADIPRIVATE_EXPORT void serialize(QIODevice *device, const Command &command); +AKONADIPRIVATE_EXPORT Command deserialize(QIODevice *device); + + + + + + +class AncestorPrivate; +class AKONADIPRIVATE_EXPORT Ancestor +{ +public: + enum Depth : uchar { + NoAncestor, + ParentAncestor, + AllAncestors + }; + + explicit Ancestor(); + explicit Ancestor(qint64 id); + Ancestor(qint64 id, const QString &remoteId); + Ancestor(Ancestor &&other); + Ancestor(const Ancestor &other); + ~Ancestor(); + + Ancestor &operator=(Ancestor &&other); + Ancestor &operator=(const Ancestor &other); + + bool operator==(const Ancestor &other) const; + bool operator!=(const Ancestor &other) const; + + void setId(qint64 id); + qint64 id() const; + + void setRemoteId(const QString &remoteId); + QString remoteId() const; + + void setName(const QString &name); + QString name() const; + + void setAttributes(const Attributes &attrs); + Attributes attributes() const; + + void debugString(DebugBlock &blck) const; +private: + QSharedDataPointer d; + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::Ancestor &ancestor); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::Ancestor &ancestor); +}; + + + + + +class FetchScopePrivate; +class AKONADIPRIVATE_EXPORT FetchScope +{ +public: + enum FetchFlag : int { + None = 0, + CacheOnly = 1 << 0, + CheckCachedPayloadPartsOnly = 1 << 1, + FullPayload = 1 << 2, + AllAttributes = 1 << 3, + Size = 1 << 4, + MTime = 1 << 5, + RemoteRevision = 1 << 6, + IgnoreErrors = 1 << 7, + Flags = 1 << 8, + RemoteID = 1 << 9, + GID = 1 << 10, + Tags = 1 << 11, + Relations = 1 << 12, + VirtReferences = 1 << 13 + }; + Q_DECLARE_FLAGS(FetchFlags, FetchFlag) + + explicit FetchScope(); + FetchScope(FetchScope &&other); + FetchScope(const FetchScope &other); + ~FetchScope(); + + FetchScope &operator=(FetchScope &&other); + FetchScope &operator=(const FetchScope &other); + + bool operator==(const FetchScope &other) const; + bool operator!=(const FetchScope &other) const; + + void setRequestedParts(const QVector &requestedParts); + QVector requestedParts() const; + QVector requestedPayloads() const; + + void setChangedSince(const QDateTime &changedSince); + QDateTime changedSince() const; + + void setTagFetchScope(const QSet &tagFetchScope); + QSet tagFetchScope() const; + + void setAncestorDepth(Ancestor::Depth depth); + Ancestor::Depth ancestorDepth() const; + + bool cacheOnly() const; + bool checkCachedPayloadPartsOnly() const; + bool fullPayload() const; + bool allAttributes() const; + bool fetchSize() const; + bool fetchMTime() const; + bool fetchRemoteRevision() const; + bool ignoreErrors() const; + bool fetchFlags() const; + bool fetchRemoteId() const; + bool fetchGID() const; + bool fetchTags() const; + bool fetchRelations() const; + bool fetchVirtualReferences() const; + + void setFetch(FetchFlags attributes, bool fetch = true); + bool fetch(FetchFlags flags) const; + + void debugString(DebugBlock &blck) const; +private: + QSharedDataPointer d; + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchScope &scope); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchScope &scope); +}; + + + + +class ScopeContextPrivate; +class AKONADIPRIVATE_EXPORT ScopeContext +{ +public: + enum Type : uchar { + Any = 0, + Collection, + Tag + }; + + explicit ScopeContext(); + ScopeContext(Type type, qint64 id); + ScopeContext(Type type, const QString &id); + ScopeContext(const ScopeContext &other); + ScopeContext(ScopeContext &&other); + ~ScopeContext(); + + ScopeContext &operator=(const ScopeContext &other); + ScopeContext &operator=(ScopeContext &&other); + + bool operator==(const ScopeContext &other) const; + bool operator!=(const ScopeContext &other) const; + + bool isEmpty() const; + + void setContext(Type type, qint64 id); + void setContext(Type type, const QString &id); + void clearContext(Type type); + + bool hasContextId(Type type) const; + qint64 contextId(Type type) const; + + bool hasContextRID(Type type) const; + QString contextRID(Type type) const; + + void debugString(DebugBlock &blck) const; +private: + QSharedDataPointer d; + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ScopeContext &context); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ScopeContext &context); +}; + + + +class PartMetaDataPrivate; +class AKONADIPRIVATE_EXPORT PartMetaData +{ +public: + explicit PartMetaData(); + PartMetaData(const QByteArray &name, qint64 size, int version = 0, + bool external = false); + PartMetaData(PartMetaData &&other); + PartMetaData(const PartMetaData &other); + ~PartMetaData(); + + PartMetaData &operator=(PartMetaData &&other); + PartMetaData &operator=(const PartMetaData &other); + + bool operator==(const PartMetaData &other) const; + bool operator!=(const PartMetaData &other) const; + + bool operator<(const PartMetaData &other) const; + + void setName(const QByteArray &name); + QByteArray name() const; + + void setSize(qint64 size); + qint64 size() const; + + void setVersion(int version); + int version() const; + + void setIsExternal(bool isExternal); + bool isExternal() const; + +private: + QSharedDataPointer d; + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::PartMetaData &part); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::PartMetaData &part); +}; + + + + +class CachePolicyPrivate; +class AKONADIPRIVATE_EXPORT CachePolicy +{ +public: + explicit CachePolicy(); + CachePolicy(CachePolicy &&other); + CachePolicy(const CachePolicy &other); + ~CachePolicy(); + + CachePolicy &operator=(CachePolicy &&other); + CachePolicy &operator=(const CachePolicy &other); + + bool operator==(const CachePolicy &other) const; + bool operator!=(const CachePolicy &other) const; + + void setInherit(bool inherit); + bool inherit() const; + + void setCheckInterval(int interval); + int checkInterval() const; + + void setCacheTimeout(int timeout); + int cacheTimeout() const; + + void setSyncOnDemand(bool onDemand); + bool syncOnDemand() const; + + void setLocalParts(const QStringList &parts); + QStringList localParts() const; + + void debugString(DebugBlock &blck) const; +private: + QSharedDataPointer d; + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::CachePolicy &policy); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::CachePolicy &policy); +}; + + + +class HelloResponsePrivate; +class AKONADIPRIVATE_EXPORT HelloResponse : public Response +{ +public: + explicit HelloResponse(); + explicit HelloResponse(const Command &command); + HelloResponse(const QString &server, const QString &message, int protocol); + + void setServerName(const QString &server); + QString serverName() const; + + void setMessage(const QString &message); + QString message() const; + + void setProtocolVersion(int protocolVersion); + int protocolVersion() const; + +private: + AKONADI_DECLARE_PRIVATE(HelloResponse) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::HelloResponse &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::HelloResponse &command); +}; + + + + +class LoginCommandPrivate; +class AKONADIPRIVATE_EXPORT LoginCommand : public Command +{ +public: + enum SessionMode : uchar { + CommandMode = 0, + NotificationBus + }; + + explicit LoginCommand(); + explicit LoginCommand(const QByteArray &sessionId, SessionMode mode = CommandMode); + LoginCommand(const Command &command); + + void setSessionId(const QByteArray &sessionId); + QByteArray sessionId() const; + + void setSessionMode(SessionMode mode); + SessionMode sessionMode() const; + +private: + AKONADI_DECLARE_PRIVATE(LoginCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::LoginCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::LoginCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT LoginResponse : public Response +{ +public: + explicit LoginResponse(); + LoginResponse(const Command &command); +}; + + + + +class AKONADIPRIVATE_EXPORT LogoutCommand : public Command +{ +public: + explicit LogoutCommand(); + LogoutCommand(const Command &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT LogoutResponse : public Response +{ +public: + explicit LogoutResponse(); + LogoutResponse(const Command &command); +}; + + + + +class TransactionCommandPrivate; +class AKONADIPRIVATE_EXPORT TransactionCommand : public Command +{ +public: + enum Mode : uchar { + Invalid = 0, + Begin, + Commit, + Rollback + }; + + explicit TransactionCommand(); + explicit TransactionCommand(Mode mode); + TransactionCommand(const Command &command); + + void setMode(Mode mode); + Mode mode() const; + +private: + AKONADI_DECLARE_PRIVATE(TransactionCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::TransactionCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::TransactionCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT TransactionResponse : public Response +{ +public: + explicit TransactionResponse(); + TransactionResponse(const Command &command); +}; + + + + +class CreateItemCommandPrivate; +class AKONADIPRIVATE_EXPORT CreateItemCommand : public Command +{ +public: + enum MergeMode : uchar { + None = 0, + GID = 1, + RemoteID = 2, + Silent = 4 + }; + Q_DECLARE_FLAGS(MergeModes, MergeMode) + + explicit CreateItemCommand(); + explicit CreateItemCommand(const Command &command); + + void setMergeModes(const MergeModes &mode); + MergeModes mergeModes() const; + + void setCollection(const Scope &collection); + Scope collection() const; + + void setItemSize(qint64 size); + qint64 itemSize() const; + + void setMimeType(const QString &mimeType); + QString mimeType() const; + + void setGID(const QString &gid); + QString gid() const; + + void setRemoteId(const QString &remoteId); + QString remoteId() const; + + void setRemoteRevision(const QString &remoteRevision); + QString remoteRevision() const; + + void setDateTime(const QDateTime &dateTime); + QDateTime dateTime() const; + + void setFlags(const QSet &flags); + QSet flags() const; + void setAddedFlags(const QSet &flags); + QSet addedFlags() const; + void setRemovedFlags(const QSet &flags); + QSet removedFlags() const; + + void setTags(const Scope &tags); + Scope tags() const; + void setAddedTags(const Scope &tags); + Scope addedTags() const; + void setRemovedTags(const Scope &tags); + Scope removedTags() const; + + void setAttributes(const Attributes &attributes); + Attributes attributes() const; + + void setParts(const QSet &parts); + QSet parts() const; + +private: + AKONADI_DECLARE_PRIVATE(CreateItemCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::CreateItemCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::CreateItemCommand &command); +}; + + + + +class AKONADIPRIVATE_EXPORT CreateItemResponse : public Response +{ +public: + explicit CreateItemResponse(); + CreateItemResponse(const Command &command); +}; + + + +class CopyItemsCommandPrivate; +class AKONADIPRIVATE_EXPORT CopyItemsCommand : public Command +{ +public: + explicit CopyItemsCommand(); + explicit CopyItemsCommand(const Scope &items, const Scope &destination); + CopyItemsCommand(const Command &command); + + void setItems(const Scope &items); + Scope items() const; + void setDestination(const Scope &destination); + Scope destination() const; + +private: + AKONADI_DECLARE_PRIVATE(CopyItemsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::CopyItemsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::CopyItemsCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT CopyItemsResponse : public Response +{ +public: + explicit CopyItemsResponse(); + CopyItemsResponse(const Command &command); +}; + + + + +class DeleteItemsCommandPrivate; +class AKONADIPRIVATE_EXPORT DeleteItemsCommand : public Command +{ +public: + explicit DeleteItemsCommand(); + explicit DeleteItemsCommand(const Scope &scope, const ScopeContext &context = ScopeContext()); + DeleteItemsCommand(const Command &command); + + ScopeContext scopeContext() const; + Scope items() const; + +private: + AKONADI_DECLARE_PRIVATE(DeleteItemsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::DeleteItemsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::DeleteItemsCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT DeleteItemsResponse : public Response +{ +public: + explicit DeleteItemsResponse(); + DeleteItemsResponse(const Command &command); +}; + + + + + +class FetchRelationsCommandPrivate; +class AKONADIPRIVATE_EXPORT FetchRelationsCommand : public Command +{ +public: + explicit FetchRelationsCommand(); + FetchRelationsCommand(qint64 side, const QVector &types = QVector(), + const QString &resource = QString()); + FetchRelationsCommand(qint64 left, qint64 right, const QVector &types = QVector(), + const QString &resource = QString()); + FetchRelationsCommand(const Command &command); + + void setLeft(qint64 left); + qint64 left() const; + + void setRight(qint64 right); + qint64 right() const; + + void setSide(qint64 side); + qint64 side() const; + + void setTypes(const QVector &types); + QVector types() const; + + void setResource(const QString &resource); + QString resource() const; + +private: + AKONADI_DECLARE_PRIVATE(FetchRelationsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchRelationsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchRelationsCommand &command); +}; + + + + +class FetchRelationsResponsePrivate; +class AKONADIPRIVATE_EXPORT FetchRelationsResponse : public Response +{ +public: + explicit FetchRelationsResponse(); + explicit FetchRelationsResponse(qint64 left, const QByteArray &leftMimeType, qint64 right, const QByteArray &rightMimeType, const QByteArray &type, + const QByteArray &remoteId = QByteArray()); + FetchRelationsResponse(const Command &command); + + qint64 left() const; + QByteArray leftMimeType() const; + qint64 right() const; + QByteArray rightMimeType() const; + QByteArray type() const; + + void setRemoteId(const QByteArray &remoteId); + QByteArray remoteId() const; + +private: + AKONADI_DECLARE_PRIVATE(FetchRelationsResponse) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchRelationsResponse &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchRelationsResponse &command); +}; + + + + +class FetchTagsCommandPrivate; +class AKONADIPRIVATE_EXPORT FetchTagsCommand : public Command +{ +public: + explicit FetchTagsCommand(); + explicit FetchTagsCommand(const Scope &scope); + FetchTagsCommand(const Command &command); + + Scope scope() const; + + void setAttributes(const QSet &attributes); + QSet attributes() const; + + void setIdOnly(bool idOnly); + bool idOnly() const; + +private: + AKONADI_DECLARE_PRIVATE(FetchTagsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchTagsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchTagsCommand &command); +}; + + + + +class FetchTagsResponsePrivate; +class AKONADIPRIVATE_EXPORT FetchTagsResponse : public Response +{ +public: + explicit FetchTagsResponse(); + explicit FetchTagsResponse(qint64 id); + FetchTagsResponse(qint64 id, const QByteArray &gid, const QByteArray &type, + const QByteArray &remoteId = QByteArray(), + qint64 parentId = 0, + const Attributes &attrs = Attributes()); + FetchTagsResponse(const Command &command); + + qint64 id() const; + + void setParentId(qint64 parentId); + qint64 parentId() const; + + void setGid(const QByteArray &gid); + QByteArray gid() const; + + void setType(const QByteArray &type); + QByteArray type() const; + + void setRemoteId(const QByteArray &remoteId); + QByteArray remoteId() const; + + void setAttributes(const Attributes &attributes); + Attributes attributes() const; + +private: + AKONADI_DECLARE_PRIVATE(FetchTagsResponse) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchTagsResponse &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchTagsResponse &command); +}; + + + + +class FetchItemsCommandPrivate; +class AKONADIPRIVATE_EXPORT FetchItemsCommand : public Command +{ +public: + explicit FetchItemsCommand(); + explicit FetchItemsCommand(const Scope &scope, const FetchScope &fetchScope = FetchScope()); + explicit FetchItemsCommand(const Scope &scope, const ScopeContext &ctx, + const FetchScope &fetchScope = FetchScope()); + FetchItemsCommand(const Command &command); + + Scope scope() const; + ScopeContext scopeContext() const; + FetchScope fetchScope() const; + FetchScope &fetchScope(); + +private: + AKONADI_DECLARE_PRIVATE(FetchItemsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchItemsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchItemsCommand &command); +}; + + + +class StreamPayloadResponse; +class FetchItemsResponsePrivate; +class AKONADIPRIVATE_EXPORT FetchItemsResponse : public Response +{ +public: + explicit FetchItemsResponse(); + explicit FetchItemsResponse(qint64 id); + FetchItemsResponse(const Command &command); + + qint64 id() const; + + void setRevision(int revision); + int revision() const; + + void setParentId(qint64 parent); + qint64 parentId() const; + + void setRemoteId(const QString &remoteId); + QString remoteId() const; + + void setRemoteRevision(const QString &remoteRev); + QString remoteRevision() const; + + void setGid(const QString &gid); + QString gid() const; + + void setSize(qint64 size); + qint64 size() const; + + void setMimeType(const QString &mimeType); + QString mimeType() const; + + void setMTime(const QDateTime &mtime); + QDateTime MTime() const; + + void setFlags(const QVector &flags); + QVector flags() const; + + void setTags(const QVector &tags); + QVector tags() const; + + void setVirtualReferences(const QVector &virtRefs); + QVector virtualReferences() const; + + void setRelations(const QVector &relations); + QVector relations() const; + + void setAncestors(const QVector &ancestors); + QVector ancestors() const; + + void setParts(const QVector &parts); + QVector parts() const; + + void setCachedParts(const QVector &cachedParts); + QVector cachedParts() const; + +private: + AKONADI_DECLARE_PRIVATE(FetchItemsResponse) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchItemsResponse &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchItemsResponse &command); +}; + + + + + +class LinkItemsCommandPrivate; +class AKONADIPRIVATE_EXPORT LinkItemsCommand : public Command +{ +public: + enum Action : bool { + Link, + Unlink + }; + + explicit LinkItemsCommand(); + explicit LinkItemsCommand(Action action, const Scope &items, const Scope &dest); + LinkItemsCommand(const Command &command); + + Action action() const; + Scope items() const; + Scope destination() const; + +private: + AKONADI_DECLARE_PRIVATE(LinkItemsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::LinkItemsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::LinkItemsCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT LinkItemsResponse : public Response +{ +public: + explicit LinkItemsResponse(); + LinkItemsResponse(const Command &command); +}; + + + + +class ModifyItemsCommandPrivate; +class AKONADIPRIVATE_EXPORT ModifyItemsCommand : public Command +{ +public: + enum ModifiedPart { + None = 0, + Flags = 1 << 0, + AddedFlags = 1 << 2, + RemovedFlags = 1 << 3, + Tags = 1 << 4, + AddedTags = 1 << 5, + RemovedTags = 1 << 6, + RemoteID = 1 << 7, + RemoteRevision = 1 << 8, + GID = 1 << 9, + Size = 1 << 10, + Parts = 1 << 11, + RemovedParts = 1 << 12, + Attributes = 1 << 13 + }; + Q_DECLARE_FLAGS(ModifiedParts, ModifiedPart) + + explicit ModifyItemsCommand(); + explicit ModifyItemsCommand(const Scope &scope); + ModifyItemsCommand(const Command &command); + + ModifiedParts modifiedParts() const; + + void setItems(const Scope &scope); + Scope items() const; + + void setOldRevision(int oldRevision); + int oldRevision() const; + + void setFlags(const QSet &flags); + QSet flags() const; + void setAddedFlags(const QSet &flags); + QSet addedFlags() const; + void setRemovedFlags(const QSet &flags); + QSet removedFlags() const; + + void setTags(const Scope &tags); + Scope tags() const; + void setAddedTags(const Scope &tags); + Scope addedTags() const; + void setRemovedTags(const Scope &tags); + Scope removedTags() const; + + void setRemoteId(const QString &remoteId); + QString remoteId() const; + + void setGid(const QString &gid); + QString gid() const; + + void setRemoteRevision(const QString &remoteRevision); + QString remoteRevision() const; + + void setDirty(bool dirty); + bool dirty() const; + + void setInvalidateCache(bool invalidate); + bool invalidateCache() const; + + void setNoResponse(bool noResponse); + bool noResponse() const; + + void setNotify(bool notify); + bool notify() const; + + void setItemSize(qint64 size); + qint64 itemSize() const; + + void setRemovedParts(const QSet &removedParts); + QSet removedParts() const; + + void setParts(const QSet &parts); + QSet parts() const; + + void setAttributes(const Protocol::Attributes &attributes); + Protocol::Attributes attributes() const; +private: + AKONADI_DECLARE_PRIVATE(ModifyItemsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ModifyItemsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ModifyItemsCommand &command); +}; + + + + +class ModifyItemsResponsePrivate; +class AKONADIPRIVATE_EXPORT ModifyItemsResponse : public Response +{ +public: + explicit ModifyItemsResponse(); + ModifyItemsResponse(qint64 id, int newRevision); + ModifyItemsResponse(const QDateTime &modificationDT); + ModifyItemsResponse(const Command &command); + + qint64 id() const; + int newRevision() const; + QDateTime modificationDateTime() const; + +private: + AKONADI_DECLARE_PRIVATE(ModifyItemsResponse) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ModifyItemsResponse &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ModifyItemsResponse &command); +}; + + + + +class MoveItemsCommandPrivate; +class AKONADIPRIVATE_EXPORT MoveItemsCommand : public Command +{ +public: + explicit MoveItemsCommand(); + explicit MoveItemsCommand(const Scope &items, const Scope &dest); + explicit MoveItemsCommand(const Scope &items, const ScopeContext &context, const Scope &dest); + MoveItemsCommand(const Command &command); + + Scope items() const; + ScopeContext itemsContext() const; + Scope destination() const; + +private: + AKONADI_DECLARE_PRIVATE(MoveItemsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::MoveItemsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::MoveItemsCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT MoveItemsResponse : public Response +{ +public: + explicit MoveItemsResponse(); + MoveItemsResponse(const Command &command); +}; + + + + +class CreateCollectionCommandPrivate; +class AKONADIPRIVATE_EXPORT CreateCollectionCommand : public Command +{ +public: + explicit CreateCollectionCommand(); + CreateCollectionCommand(const Command &command); + + void setParent(const Scope &scope); + Scope parent() const; + + void setName(const QString &name); + QString name() const; + + void setRemoteId(const QString &remoteId); + QString remoteId() const; + + void setRemoteRevision(const QString &remoteRevision); + QString remoteRevision() const; + + void setMimeTypes(const QStringList &mimeTypes); + QStringList mimeTypes() const; + + void setCachePolicy(const CachePolicy &cachePolicy); + CachePolicy cachePolicy() const; + + void setAttributes(const Attributes &attributes); + Attributes attributes() const; + + void setIsVirtual(bool isVirtual); + bool isVirtual() const; + + void setEnabled(bool enabled); + bool enabled() const; + + void setSyncPref(Tristate sync); + Tristate syncPref() const; + + void setDisplayPref(Tristate display); + Tristate displayPref() const; + + void setIndexPref(Tristate index); + Tristate indexPref() const; + +private: + AKONADI_DECLARE_PRIVATE(CreateCollectionCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::CreateCollectionCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::CreateCollectionCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT CreateCollectionResponse : public Response +{ +public: + explicit CreateCollectionResponse(); + CreateCollectionResponse(const Command &command); +}; + + + + +class CopyCollectionCommandPrivate; +class AKONADIPRIVATE_EXPORT CopyCollectionCommand : public Command +{ +public: + explicit CopyCollectionCommand(); + explicit CopyCollectionCommand(const Scope &collection, const Scope &dest); + CopyCollectionCommand(const Command &command); + + Scope collection() const; + Scope destination() const; + +private: + AKONADI_DECLARE_PRIVATE(CopyCollectionCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::CopyCollectionCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::CopyCollectionCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT CopyCollectionResponse : public Response +{ +public: + explicit CopyCollectionResponse(); + CopyCollectionResponse(const Command &command); +}; + + + + +class DeleteCollectionCommandPrivate; +class AKONADIPRIVATE_EXPORT DeleteCollectionCommand : public Command +{ +public: + explicit DeleteCollectionCommand(); + explicit DeleteCollectionCommand(const Scope &scope); + DeleteCollectionCommand(const Command &command); + + Scope collection() const; + +private: + AKONADI_DECLARE_PRIVATE(DeleteCollectionCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::DeleteCollectionCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::DeleteCollectionCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT DeleteCollectionResponse : public Response +{ +public: + explicit DeleteCollectionResponse(); + DeleteCollectionResponse(const Command &command); +}; + + + + +class FetchCollectionStatsCommandPrivate; +class AKONADIPRIVATE_EXPORT FetchCollectionStatsCommand : public Command +{ +public: + explicit FetchCollectionStatsCommand(); + explicit FetchCollectionStatsCommand(const Scope &col); + FetchCollectionStatsCommand(const Command &command); + + Scope collection() const; + +private: + AKONADI_DECLARE_PRIVATE(FetchCollectionStatsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchCollectionStatsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchCollectionStatsCommand &command); +}; + + + + + +class FetchCollectionStatsResponsePrivate; +class AKONADIPRIVATE_EXPORT FetchCollectionStatsResponse : public Response +{ +public: + explicit FetchCollectionStatsResponse(); + explicit FetchCollectionStatsResponse(qint64 count, qint64 unseen, qint64 size); + FetchCollectionStatsResponse(const Command &command); + + qint64 count() const; + qint64 unseen() const; + qint64 size() const; + +private: + AKONADI_DECLARE_PRIVATE(FetchCollectionStatsResponse) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchCollectionStatsResponse &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchCollectionStatsResponse &command); +}; + + + + +class FetchCollectionsCommandPrivate; +class AKONADIPRIVATE_EXPORT FetchCollectionsCommand : public Command +{ +public: + enum Depth : uchar { + BaseCollection, + ParentCollection, + AllCollections + }; + + explicit FetchCollectionsCommand(); + explicit FetchCollectionsCommand(const Scope &scope); + FetchCollectionsCommand(const Command &command); + + Scope collections() const; + + void setDepth(Depth depth); + Depth depth() const; + + void setResource(const QString &resourceId); + QString resource() const; + + void setMimeTypes(const QStringList &mimeTypes); + QStringList mimeTypes() const; + + void setAncestorsDepth(Ancestor::Depth depth); + Ancestor::Depth ancestorsDepth() const; + + void setAncestorsAttributes(const QSet &attributes); + QSet ancestorsAttributes() const; + + void setEnabled(bool enabled); + bool enabled() const; + + void setSyncPref(bool sync); + bool syncPref() const; + + void setDisplayPref(bool display); + bool displayPref() const; + + void setIndexPref(bool index); + bool indexPref() const; + + void setFetchStats(bool stats); + bool fetchStats() const; + +private: + AKONADI_DECLARE_PRIVATE(FetchCollectionsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchCollectionsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchCollectionsCommand &command); +}; + + + + + +class FetchCollectionsResponsePrivate; +class AKONADIPRIVATE_EXPORT FetchCollectionsResponse : public Response +{ +public: + explicit FetchCollectionsResponse(); + explicit FetchCollectionsResponse(qint64 id); + FetchCollectionsResponse(const Command &command); + + qint64 id() const; + + void setParentId(qint64 id); + qint64 parentId() const; + + void setName(const QString &name); + QString name() const; + + void setMimeTypes(const QStringList &mimeType); + QStringList mimeTypes() const; + + void setRemoteId(const QString &remoteId); + QString remoteId() const; + + void setRemoteRevision(const QString &remoteRev); + QString remoteRevision() const; + + void setResource(const QString &resourceId); + QString resource() const; + + void setStatistics(const FetchCollectionStatsResponse &stats); + FetchCollectionStatsResponse statistics() const; + + void setSearchQuery(const QString &searchQuery); + QString searchQuery() const; + + void setSearchCollections(const QVector &searchCols); + QVector searchCollections() const; + + void setAncestors(const QVector &ancestors); + QVector ancestors() const; + + void setCachePolicy(const CachePolicy &cachePolicy); + CachePolicy cachePolicy() const; + CachePolicy &cachePolicy(); + + void setAttributes(const Attributes &attrs); + Attributes attributes() const; + + void setEnabled(bool enabled); + bool enabled() const; + + void setDisplayPref(Tristate displayPref); + Tristate displayPref() const; + + void setSyncPref(Tristate syncPref); + Tristate syncPref() const; + + void setIndexPref(Tristate indexPref); + Tristate indexPref() const; + + void setReferenced(bool ref); + bool referenced() const; + + void setIsVirtual(bool isVirtual); + bool isVirtual() const; + +private: + AKONADI_DECLARE_PRIVATE(FetchCollectionsResponse) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::FetchCollectionsResponse &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::FetchCollectionsResponse &command); +}; + + + + + +class ModifyCollectionCommandPrivate; +class AKONADIPRIVATE_EXPORT ModifyCollectionCommand : public Command +{ +public: + enum ModifiedPart { + None = 0, + Name = 1 << 0, + RemoteID = 1 << 1, + RemoteRevision = 1 << 2, + ParentID = 1 << 3, + MimeTypes = 1 << 4, + CachePolicy = 1 << 5, + PersistentSearch = 1 << 6, + RemovedAttributes = 1 << 7, + Attributes = 1 << 8, + ListPreferences = 1 << 9, + Referenced = 1 << 10 + }; + Q_DECLARE_FLAGS(ModifiedParts, ModifiedPart) + + explicit ModifyCollectionCommand(); + explicit ModifyCollectionCommand(const Scope &scope); + ModifyCollectionCommand(const Command &command); + + ModifiedParts modifiedParts() const; + + Scope collection() const; + + void setParentId(qint64 parentId); + qint64 parentId() const; + + void setMimeTypes(const QStringList &mimeTypes); + QStringList mimeTypes() const; + + void setCachePolicy(const Protocol::CachePolicy &cachePolicy); + Protocol::CachePolicy cachePolicy() const; + + void setName(const QString &name); + QString name() const; + + void setRemoteId(const QString &remoteId); + QString remoteId() const; + + void setRemoteRevision(const QString &remoteRevision); + QString remoteRevision() const; + + void setPersistentSearchQuery(const QString &query); + QString persistentSearchQuery() const; + + void setPersistentSearchCollections(const QVector &cols); + QVector persistentSearchCollections() const; + + void setPersistentSearchRemote(bool remote); + bool persistentSearchRemote() const; + + void setPersistentSearchRecursive(bool recursive); + bool persistentSearchRecursive() const; + + void setRemovedAttributes(const QSet &removedAttributes); + QSet removedAttributes() const; + + void setAttributes(const Protocol::Attributes &attributes); + Protocol::Attributes attributes() const; + + void setEnabled(bool enabled); + bool enabled() const; + + void setSyncPref(Tristate sync); + Tristate syncPref() const; + + void setDisplayPref(Tristate display); + Tristate displayPref() const; + + void setIndexPref(Tristate index); + Tristate indexPref() const; + + void setReferenced(bool referenced); + bool referenced() const; + +private: + AKONADI_DECLARE_PRIVATE(ModifyCollectionCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ModifyCollectionCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ModifyCollectionCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT ModifyCollectionResponse : public Response +{ +public: + explicit ModifyCollectionResponse(); + ModifyCollectionResponse(const Command &command); +}; + + + + + +class MoveCollectionCommandPrivate; +class AKONADIPRIVATE_EXPORT MoveCollectionCommand : public Command +{ +public: + explicit MoveCollectionCommand(); + explicit MoveCollectionCommand(const Scope &col, const Scope &dest); + MoveCollectionCommand(const Command &command); + + Scope collection() const; + Scope destination() const; + +private: + AKONADI_DECLARE_PRIVATE(MoveCollectionCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::MoveCollectionCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::MoveCollectionCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT MoveCollectionResponse : public Response +{ +public: + explicit MoveCollectionResponse(); + MoveCollectionResponse(const Command &command); +}; + + + + +class SearchCommandPrivate; +class AKONADIPRIVATE_EXPORT SearchCommand : public Command +{ +public: + explicit SearchCommand(); + SearchCommand(const Command &command); + + void setMimeTypes(const QStringList &mimeTypes); + QStringList mimeTypes() const; + + void setCollections(const QVector &collections); + QVector collections() const; + + void setQuery(const QString &query); + QString query() const; + + void setFetchScope(const FetchScope &fetchScope); + FetchScope fetchScope() const; + + void setRecursive(bool recursive); + bool recursive() const; + + void setRemote(bool remote); + bool remote() const; + +private: + AKONADI_DECLARE_PRIVATE(SearchCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::SearchCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::SearchCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT SearchResponse : public Response +{ +public: + explicit SearchResponse(); + SearchResponse(const Command &command); +}; + + + + + +class SearchResultCommandPrivate; +class AKONADIPRIVATE_EXPORT SearchResultCommand : public Command +{ +public: + explicit SearchResultCommand(); + explicit SearchResultCommand(const QByteArray &searchId, qint64 collectionId, const Scope &result); + SearchResultCommand(const Command &command); + + QByteArray searchId() const; + qint64 collectionId() const; + Scope result() const; + +private: + AKONADI_DECLARE_PRIVATE(SearchResultCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::SearchResultCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::SearchResultCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT SearchResultResponse : public Response +{ +public: + explicit SearchResultResponse(); + SearchResultResponse(const Command &command); +}; + + + + +class StoreSearchCommandPrivate; +class AKONADIPRIVATE_EXPORT StoreSearchCommand : public Command +{ +public: + explicit StoreSearchCommand(); + StoreSearchCommand(const Command &command); + + void setName(const QString &name); + QString name() const; + + void setQuery(const QString &query); + QString query() const; + + void setMimeTypes(const QStringList &mimeTypes); + QStringList mimeTypes() const; + + void setQueryCollections(const QVector &queryCols); + QVector queryCollections() const; + + void setRemote(bool remote); + bool remote() const; + + void setRecursive(bool recursive); + bool recursive() const; + +private: + AKONADI_DECLARE_PRIVATE(StoreSearchCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::StoreSearchCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::StoreSearchCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT StoreSearchResponse : public Response +{ +public: + explicit StoreSearchResponse(); + StoreSearchResponse(const Command &command); +}; + + + + +class CreateTagCommandPrivate; +class AKONADIPRIVATE_EXPORT CreateTagCommand : public Command +{ +public: + explicit CreateTagCommand(); + CreateTagCommand(const Command &command); + + void setGid(const QByteArray &gid); + QByteArray gid() const; + + void setRemoteId(const QByteArray &remoteId); + QByteArray remoteId() const; + + void setType(const QByteArray &type); + QByteArray type() const; + + void setAttributes(const Attributes &attributes); + Attributes attributes() const; + + void setParentId(qint64 parentId); + qint64 parentId() const; + + void setMerge(bool merge); + bool merge() const; + +private: + AKONADI_DECLARE_PRIVATE(CreateTagCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::CreateTagCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::CreateTagCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT CreateTagResponse : public Response +{ +public: + explicit CreateTagResponse(); + CreateTagResponse(const Command &command); +}; + + + + + +class DeleteTagCommandPrivate; +class AKONADIPRIVATE_EXPORT DeleteTagCommand : public Command +{ +public: + explicit DeleteTagCommand(); + explicit DeleteTagCommand(const Scope &scope); + DeleteTagCommand(const Command &command); + + Scope tag() const; + +private: + AKONADI_DECLARE_PRIVATE(DeleteTagCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::DeleteTagCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::DeleteTagCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT DeleteTagResponse : public Response +{ +public: + explicit DeleteTagResponse(); + DeleteTagResponse(const Command &command); +}; + + + + + +class ModifyTagCommandPrivate; +class AKONADIPRIVATE_EXPORT ModifyTagCommand : public Command +{ +public: + enum ModifiedPart { + None = 0, + ParentId = 1 << 0, + Type = 1 << 1, + RemoteId = 1 << 2, + RemovedAttributes = 1 << 3, + Attributes = 1 << 4 + }; + Q_DECLARE_FLAGS(ModifiedParts, ModifiedPart) + + explicit ModifyTagCommand(); + explicit ModifyTagCommand(qint64 tagId); + ModifyTagCommand(const Command &command); + + qint64 tagId() const; + + ModifiedParts modifiedParts() const; + + void setParentId(qint64 parentId); + qint64 parentId() const; + + void setType(const QByteArray &type); + QByteArray type() const; + + void setRemoteId(const QByteArray &remoteId); + QByteArray remoteId() const; + + void setRemovedAttributes(const QSet &removed); + QSet removedAttributes() const; + + void setAttributes(const Protocol::Attributes &attrs); + Protocol::Attributes attributes() const; + +private: + AKONADI_DECLARE_PRIVATE(ModifyTagCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ModifyTagCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ModifyTagCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT ModifyTagResponse : public Response +{ +public: + explicit ModifyTagResponse(); + ModifyTagResponse(const Command &command); +}; + + + + +class ModifyRelationCommandPrivate; +class AKONADIPRIVATE_EXPORT ModifyRelationCommand : public Command +{ +public: + explicit ModifyRelationCommand(); + ModifyRelationCommand(qint64 left, qint64 right, const QByteArray &type, + const QByteArray &remoteId = QByteArray()); + ModifyRelationCommand(const Command &command); + + void setLeft(qint64 left); + qint64 left() const; + + void setRight(qint64 right); + qint64 right() const; + + void setType(const QByteArray &type); + QByteArray type() const; + + void setRemoteId(const QByteArray &remoteId); + QByteArray remoteId() const; + +private: + AKONADI_DECLARE_PRIVATE(ModifyRelationCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ModifyRelationCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ModifyRelationCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT ModifyRelationResponse : public Response +{ +public: + explicit ModifyRelationResponse(); + ModifyRelationResponse(const Command &command); +}; + + + + +class RemoveRelationsCommandPrivate; +class AKONADIPRIVATE_EXPORT RemoveRelationsCommand : public Command +{ +public: + explicit RemoveRelationsCommand(); + RemoveRelationsCommand(qint64 left, qint64 right, const QByteArray &type = QByteArray()); + RemoveRelationsCommand(const Command &command); + + void setLeft(qint64 left); + qint64 left() const; + + void setRight(qint64 right); + qint64 right() const; + + void setType(const QByteArray &type); + QByteArray type() const; + +private: + AKONADI_DECLARE_PRIVATE(RemoveRelationsCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::RemoveRelationsCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::RemoveRelationsCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT RemoveRelationsResponse : public Response +{ +public: + explicit RemoveRelationsResponse(); + RemoveRelationsResponse(const Command &command); +}; + + + + +class SelectResourceCommandPrivate; +class AKONADIPRIVATE_EXPORT SelectResourceCommand : public Command +{ +public: + explicit SelectResourceCommand(); + explicit SelectResourceCommand(const QString &resourceId); + SelectResourceCommand(const Command &command); + + QString resourceId() const; + +private: + AKONADI_DECLARE_PRIVATE(SelectResourceCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::SelectResourceCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::SelectResourceCommand &command); +}; + + + + + +class AKONADIPRIVATE_EXPORT SelectResourceResponse : public Response +{ +public: + explicit SelectResourceResponse(); + SelectResourceResponse(const Command &command); +}; + + + + +class StreamPayloadCommandPrivate; +class AKONADIPRIVATE_EXPORT StreamPayloadCommand : public Command +{ +public: + enum Request : uchar { + MetaData, + Data + }; + + explicit StreamPayloadCommand(); + StreamPayloadCommand(const QByteArray &payloadName, Request request, + const QString &dest = QString()); + StreamPayloadCommand(const Command &command); + + void setPayloadName(const QByteArray &name); + QByteArray payloadName() const; + + void setDestination(const QString &dest); + QString destination() const; + + void setRequest(Request request); + Request request() const; + +private: + AKONADI_DECLARE_PRIVATE(StreamPayloadCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::StreamPayloadCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::StreamPayloadCommand &command); +}; + + + + +class StreamPayloadResponsePrivate; +class AKONADIPRIVATE_EXPORT StreamPayloadResponse : public Response +{ +public: + explicit StreamPayloadResponse(); + StreamPayloadResponse(const QByteArray &payloadName, + const PartMetaData &metadata = PartMetaData()); + StreamPayloadResponse(const QByteArray &payloadName, + const QByteArray &data); + StreamPayloadResponse(const QByteArray &payloadName, + const PartMetaData &metaData, + const QByteArray &data); + StreamPayloadResponse(const Command &command); + + void setPayloadName(const QByteArray &payloadName); + QByteArray payloadName() const; + + void setMetaData(const PartMetaData &metaData); + PartMetaData metaData() const; + + void setData(const QByteArray &data); + QByteArray data() const; + +private: + AKONADI_DECLARE_PRIVATE(StreamPayloadResponse) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::StreamPayloadResponse &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::StreamPayloadResponse &command); +}; + + + + +class ChangeNotificationPrivate; +class AKONADIPRIVATE_EXPORT ChangeNotification : public Command +{ +public: + typedef QVector List; + typedef qint64 Id; + + enum Type : uchar { + InvalidType, + Collections, + Items, + Tags, + Relations + }; + + enum Operation : uchar { + InvalidOp, + Add, + Modify, + Move, + Remove, + Link, + Unlink, + Subscribe, + Unsubscribe, + ModifyFlags, + ModifyTags, + ModifyRelations + }; + + class Entity + { + public: + Entity() + : id(-1) + {} + + Entity(Id id, const QString &remoteId, const QString &remoteRevision, const QString &mimeType) + : id(id) + , remoteId(remoteId) + , remoteRevision(remoteRevision) + , mimeType(mimeType) + {} + + bool operator==(const Entity &other) const + { + return id == other.id + && remoteId == other.remoteId + && remoteRevision == other.remoteRevision + && mimeType == other.mimeType; + } + + Id id; + QString remoteId; + QString remoteRevision; + QString mimeType; + }; + + explicit ChangeNotification(); + ChangeNotification(const Command& other); + + bool isValid() const; + + ChangeNotification::Type type() const; + void setType(ChangeNotification::Type type); + + ChangeNotification::Operation operation() const; + void setOperation(ChangeNotification::Operation operation); + + QByteArray sessionId() const; + void setSessionId(const QByteArray &sessionId); + + void addEntity(Id id, const QString &remoteId = QString(), const QString &remoteRevision = QString(), const QString &mimeType = QString()); + void setEntities(const QVector &items); + QMap entities() const; + Entity entity(Id id) const; + QList uids() const; + void clearEntities(); + + QByteArray resource() const; + void setResource(const QByteArray &resource); + + Id parentCollection() const; + void setParentCollection(Id parent); + + Id parentDestCollection() const; + void setParentDestCollection(Id parent); + + QByteArray destinationResource() const; + void setDestinationResource(const QByteArray &destResource); + + QSet itemParts() const; + void setItemParts(const QSet &parts); + + QSet addedFlags() const; + void setAddedFlags(const QSet &parts); + + QSet removedFlags() const; + void setRemovedFlags(const QSet &parts); + + QSet addedTags() const; + void setAddedTags(const QSet &tags); + + QSet removedTags() const; + void setRemovedTags(const QSet &tags); + + void addMetadata(const QByteArray &metadata); + void removeMetadata(const QByteArray &metadata); + QVector metadata() const; + + /** + Adds a new notification message to the given list and compresses notifications + where possible. + */ + static bool appendAndCompress(ChangeNotification::List &list, const ChangeNotification &msg); + +private: + AKONADI_DECLARE_PRIVATE(ChangeNotification) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ChangeNotification &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ChangeNotification &command); +}; + +inline uint qHash(ChangeNotification::Type type) +{ + return ::qHash(static_cast(type)); +} + +#if 0 +class ChangeNotificationSubscriptionCommandPrivate; +class AKONADIPRIVATE_EXPORT ChangeNotificationSubscriptionCommand : public Command +{ +public: + explicit ChangeNotificationSubscriptionCommand(); + explicit ChangeNotificationSubscriptionCommand(const QByteArray &subscriptionId); + ChangeNotificationSubscriptionCommand(const Command &other); + + void unsubscribe(); + + void monitorCollection(qint64 id, bool monitor = true); + void setMonitoredCollections(const QVector &ids); + QVector monitoredCollections() const; + + void monitorItem(qint64 id, bool monitor = true); + void setMonitoredItems(const QVector &ids); + QVector monitoredItems() const; + + void monitorTag(qint64 id, bool monitor = true); + void setMonitoredTags(const QVector &ids); + QVector monitoredTags() const; + + void monitorType(ChangeNotification::Type type, bool monitor = true); + void setMonitoredTypes(const QVector &types); + QVector monitoredTypes() const; + + void monitorResource(const QByteArray &resourceId, bool monitor = true); + void setMonitoredResources(const QVector &resources); + QVector monitoredResources() const; + + void monitorMimeType(const QString &mimeType, bool monitor = true); + void setMonitoredMimeTypes(const QStringList &mimeTypes); + QStringList monitoredMimeTypes() const; + + void setAllMonitored(bool all); + bool isAllMonitored() const; + + void setExclusive(bool exclusive); + bool isExclusive() const; + + void ignoreSession(const QByteArray &sessionId, bool ignored = true); + void setIgnoredSessions(const QVector &ignoredSessions); + QVector ignoredSessions() const; + +private: + AKONADI_DECLARE_PRIVATE(ChangeNotificationSubscriptionCommand) + + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ChangeNotificationSubscriptionCommand &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ChangeNotificationSubscriptionCommand &command); +}; + + + +class ChangeNotificationSubscriptionResponsePrivate; +class AKONADIPRIVATE_EXPORT ChangeNotificationSubscriptionResponse : public Response +{ +public: + explicit ChangeNotificationSubscriptionResponse(); + explicit ChangeNotificationSubscriptionResponse(const QByteArray &subscriptionId); + ChangeNotificationSubscriptionResponse(const Command &other); + +private: + friend DataStream &operator<<(DataStream &stream, const Akonadi::Protocol::ChangeNotificationSubscriptionResponse &command); + friend DataStream &operator>>(DataStream &stream, Akonadi::Protocol::ChangeNotificationSubscriptionResponse &command); +}; +#endif + +} // namespace Protocol +} // namespace Akonadi + +Q_DECLARE_OPERATORS_FOR_FLAGS(Akonadi::Protocol::FetchScope::FetchFlags) +Q_DECLARE_METATYPE(Akonadi::Protocol::Command::Type) +Q_DECLARE_METATYPE(Akonadi::Protocol::Command) +Q_DECLARE_METATYPE(Akonadi::Protocol::ChangeNotification) +Q_DECLARE_METATYPE(Akonadi::Protocol::ChangeNotification::Type) +Q_DECLARE_METATYPE(Akonadi::Protocol::ChangeNotification::List) + +AKONADIPRIVATE_EXPORT DataStream &operator>>(DataStream &stream, Akonadi::Protocol::Command::Type &type); +AKONADIPRIVATE_EXPORT DataStream &operator<<(DataStream &stream, Akonadi::Protocol::Command::Type type); +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, Akonadi::Protocol::Command::Type type); + +AKONADIPRIVATE_EXPORT inline const QDBusArgument &operator>>(const QDBusArgument &arg, Akonadi::Protocol::ChangeNotification::Type &type) +{ + arg.beginStructure(); + arg >> reinterpret_cast(type); + arg.endStructure(); + return arg; +} + +AKONADIPRIVATE_EXPORT inline QDBusArgument &operator<<(QDBusArgument &arg, Akonadi::Protocol::ChangeNotification::Type type) +{ + arg.beginStructure(); + arg << (int) type; + arg.endStructure(); + return arg; +} + +// Command parameters +#define AKONADI_PARAM_ATR "ATR:" +#define AKONADI_PARAM_CACHEPOLICY "CACHEPOLICY" +#define AKONADI_PARAM_DISPLAY "DISPLAY" +#define AKONADI_PARAM_ENABLED "ENABLED" +#define AKONADI_PARAM_FLAGS "FLAGS" +#define AKONADI_PARAM_TAGS "TAGS" +#define AKONADI_PARAM_GID "GID" +#define AKONADI_PARAM_INDEX "INDEX" +#define AKONADI_PARAM_MIMETYPE "MIMETYPE" +#define AKONADI_PARAM_NAME "NAME" +#define AKONADI_PARAM_PARENT "PARENT" +#define AKONADI_PARAM_PERSISTENTSEARCH "PERSISTENTSEARCH" +#define AKONADI_PARAM_PLD "PLD:" +#define AKONADI_PARAM_PLD_RFC822 "PLD:RFC822" +#define AKONADI_PARAM_RECURSIVE "RECURSIVE" +#define AKONADI_PARAM_REFERENCED "REFERENCED" +#define AKONADI_PARAM_REMOTE "REMOTE" +#define AKONADI_PARAM_REMOTEID "REMOTEID" +#define AKONADI_PARAM_REMOTEREVISION "REMOTEREVISION" +#define AKONADI_PARAM_REVISION "REV" +#define AKONADI_PARAM_SIZE "SIZE" +#define AKONADI_PARAM_SYNC "SYNC" +#define AKONADI_PARAM_TAG "TAG" +#define AKONADI_PARAM_TYPE "TYPE" +#define AKONADI_PARAM_VIRTUAL "VIRTUAL" + +// Flags +#define AKONADI_FLAG_GID "\\Gid" +#define AKONADI_FLAG_IGNORED "$IGNORED" +#define AKONADI_FLAG_MIMETYPE "\\MimeType" +#define AKONADI_FLAG_REMOTEID "\\RemoteId" +#define AKONADI_FLAG_REMOTEREVISION "\\RemoteRevision" +#define AKONADI_FLAG_TAG "\\Tag" +#define AKONADI_FLAG_RTAG "\\RTag" +#define AKONADI_FLAG_SEEN "\\SEEN" + +// Attributes +#define AKONADI_ATTRIBUTE_HIDDEN "ATR:HIDDEN" +#define AKONADI_ATTRIBUTE_MESSAGES "MESSAGES" +#define AKONADI_ATTRIBUTE_UNSEEN "UNSEEN" + +// special resource names +#define AKONADI_SEARCH_RESOURCE "akonadi_search_resource" +#endif diff --git a/src/private/scope.cpp b/src/private/scope.cpp new file mode 100644 index 0000000..d8d09dc --- /dev/null +++ b/src/private/scope.cpp @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2009 Volker Krause + * Copyright (c) 2015 Daniel Vrátil + * + * 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 "scope_p.h" +#include "datastream_p_p.h" + +#include + +#include "imapset_p.h" + +namespace Akonadi +{ + +class ScopePrivate : public QSharedData +{ +public: + ScopePrivate() + : scope(Scope::Invalid) + {} + + ImapSet uidSet; + QStringList ridSet; + QVector hridChain; + QStringList gidSet; + Scope::SelectionScope scope; +}; + +Scope::HRID::HRID() + : id(-1) +{} + +Scope::HRID::HRID(qint64 id, const QString &remoteId) + : id(id) + , remoteId(remoteId) +{} + +Scope::HRID::HRID(const HRID &other) + : id(other.id) + , remoteId(other.remoteId) +{} + +Scope::HRID::HRID(HRID &&other) + : id(other.id) +{ + remoteId.swap(other.remoteId); +} + +Scope::HRID &Scope::HRID::operator=(const HRID &other) +{ + if (*this == other) { + return *this; + } + + id = other.id; + remoteId = other.remoteId; + return *this; +} + +Scope::HRID &Scope::HRID::operator=(HRID &&other) +{ + if (*this == other) { + return *this; + } + + id = other.id; + remoteId.swap(other.remoteId); + return *this; +} + +bool Scope::HRID::isEmpty() const +{ + return id <= 0 && remoteId.isEmpty(); +} + +bool Scope::HRID::operator==(const HRID &other) const +{ + return id == other.id && remoteId == other.remoteId; +} + + + +Scope::Scope() + : d(new ScopePrivate) +{ +} + +Scope::Scope(qint64 id) + : d(new ScopePrivate) +{ + setUidSet(id); +} + +Scope::Scope(const ImapSet &set) + : d(new ScopePrivate) +{ + setUidSet(set); +} + +Scope::Scope(const ImapInterval &interval) + : d(new ScopePrivate) +{ + setUidSet(interval); +} + +Scope::Scope(const QVector &interval) + : d(new ScopePrivate) +{ + setUidSet(interval); +} + +Scope::Scope(SelectionScope scope, const QStringList &ids) + : d(new ScopePrivate) +{ + Q_ASSERT(scope == Rid || scope == Gid); + if (scope == Rid) { + d->scope = scope; + d->ridSet = ids; + } else if (scope == Gid) { + d->scope = scope; + d->gidSet = ids; + } +} + +Scope::Scope(const QVector &hrid) + : d(new ScopePrivate) +{ + d->scope = HierarchicalRid; + d->hridChain = hrid; +} + +Scope::Scope(const Scope &other) + : d(other.d) +{ +} + +Scope::Scope(Scope &&other) +{ + d.swap(other.d); +} + +Scope::~Scope() +{ +} + +Scope &Scope::operator=(const Scope &other) +{ + d = other.d; + return *this; +} + +Scope &Scope::operator=(Scope &&other) +{ + d.swap(other.d); + return *this; +} + +bool Scope::operator==(const Scope &other) const +{ + if (d->scope != other.d->scope) { + return false; + } + + switch (d->scope) { + case Uid: + return d->uidSet == other.d->uidSet; + case Gid: + return d->gidSet == other.d->gidSet; + case Rid: + return d->ridSet == other.d->ridSet; + case HierarchicalRid: + return d->hridChain == other.d->hridChain; + case Invalid: + return true; + } + + return false; +} + +bool Scope::operator!=(const Scope &other) const +{ + return !(*this == other); +} + +Scope::SelectionScope Scope::scope() const +{ + return d->scope; +} + +bool Scope::isEmpty() const +{ + switch (d->scope) { + case Invalid: + return true; + case Uid: + return d->uidSet.isEmpty(); + case Rid: + return d->ridSet.isEmpty(); + case HierarchicalRid: + return d->hridChain.isEmpty(); + case Gid: + return d->gidSet.isEmpty(); + } + + Q_ASSERT(false); + return true; +} + + +void Scope::setUidSet(const ImapSet &uidSet) +{ + d->scope = Uid; + d->uidSet = uidSet; +} + +ImapSet Scope::uidSet() const +{ + return d->uidSet; +} + +void Scope::setRidSet(const QStringList &ridSet) +{ + d->scope = Rid; + d->ridSet = ridSet; +} + +QStringList Scope::ridSet() const +{ + return d->ridSet; +} + +void Scope::setHRidChain(const QVector &hridChain) +{ + d->scope = HierarchicalRid; + d->hridChain = hridChain; +} + +QVector Scope::hridChain() const +{ + return d->hridChain; +} + +void Scope::setGidSet(const QStringList &gidSet) +{ + d->scope = Gid; + d->gidSet = gidSet; +} + +QStringList Scope::gidSet() const +{ + return d->gidSet; +} + +qint64 Scope::uid() const +{ + if (d->uidSet.intervals().size() == 1 && + d->uidSet.intervals().at(0).size() == 1) { + return d->uidSet.intervals().at(0).begin(); + } + + // TODO: Error handling! + return -1; +} + +QString Scope::rid() const +{ + if (d->ridSet.size() != 1) { + // TODO: Error handling! + Q_ASSERT(d->ridSet.size() == 1); + return QString(); + } + return d->ridSet.at(0); +} + +QString Scope::gid() const +{ + if (d->gidSet.size() != 1) { + // TODO: Error hanlding! + Q_ASSERT(d->gidSet.size() == 1); + return QString(); + } + return d->gidSet.at(0); +} + +Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::Scope &scope) +{ + stream << (quint8) scope.d->scope; + switch (scope.d->scope) { + case Scope::Invalid: + return stream; + case Scope::Uid: + stream << scope.d->uidSet; + return stream; + case Scope::Rid: + stream << scope.d->ridSet; + return stream; + case Scope::HierarchicalRid: + stream << scope.d->hridChain; + return stream; + case Scope::Gid: + stream << scope.d->gidSet; + return stream; + } + + Q_ASSERT(false); + return stream; +} + +Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::Scope::HRID &hrid) +{ + return stream << hrid.id << hrid.remoteId; +} + +Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::Scope::HRID &hrid) +{ + return stream >> hrid.id >> hrid.remoteId; +} + +Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::Scope &scope) +{ + scope.d->uidSet = ImapSet(); + scope.d->ridSet.clear(); + scope.d->hridChain.clear(); + scope.d->gidSet.clear(); + + stream >> reinterpret_cast(scope.d->scope); + switch (scope.d->scope) { + case Scope::Invalid: + return stream; + case Scope::Uid: + stream >> scope.d->uidSet; + return stream; + case Scope::Rid: + stream >> scope.d->ridSet; + return stream; + case Scope::HierarchicalRid: + stream >> scope.d->hridChain; + return stream; + case Scope::Gid: + stream >> scope.d->gidSet; + return stream; + } + + Q_ASSERT(false); + return stream; +} + +} // namespace Akonadi + +using namespace Akonadi; + +QDebug operator<<(QDebug dbg, const Akonadi::Scope::HRID &hrid) +{ + return dbg.nospace() << "(ID: " << hrid.id << ", RemoteID: " << hrid.remoteId << ")"; +} + +QDebug operator<<(QDebug dbg, const Akonadi::Scope &scope) +{ + switch (scope.scope()) { + case Scope::Uid: + return dbg.nospace() << "UID " << scope.uidSet(); + case Scope::Rid: + return dbg.nospace() << "RID " << scope.ridSet(); + case Scope::Gid: + return dbg.nospace() << "GID " << scope.gidSet(); + case Scope::HierarchicalRid: + return dbg.nospace() << "HRID " << scope.hridChain(); + default: + return dbg.nospace() << "Invalid scope"; + } +} diff --git a/src/private/scope_p.h b/src/private/scope_p.h new file mode 100644 index 0000000..565acc0 --- /dev/null +++ b/src/private/scope_p.h @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2009 Volker Krause + * Copyright (c) 2015 Daniel Vrátil + * + * 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_PRIVATE_SCOPE_P_H +#define AKONADI_PRIVATE_SCOPE_P_H + +#include "akonadiprivate_export.h" + +#include + +class QString; +class QStringList; + +namespace Akonadi +{ +class ImapSet; +class ImapInterval; + +namespace Protocol +{ +class DataStream; +} + + +class ScopePrivate; +class AKONADIPRIVATE_EXPORT Scope +{ +public: + enum SelectionScope : uchar { + Invalid = 0, + Uid = 1 << 0, + Rid = 1 << 1, + HierarchicalRid = 1 << 2, + Gid = 1 << 3 + }; + + class HRID { + public: + explicit HRID(); + HRID(qint64 id, const QString &remoteId = QString()); + HRID(const HRID &other); + HRID(HRID &&other); + + HRID &operator=(const HRID &other); + HRID &operator=(HRID &&other); + + bool isEmpty() const; + bool operator==(const HRID &other) const; + + qint64 id; + QString remoteId; + }; + + explicit Scope(); + Scope(SelectionScope scope, const QStringList &ids); + + /* UID */ + Scope(qint64 id); + Scope(const ImapSet &uidSet); + Scope(const ImapInterval &interval); + Scope(const QVector &interval); + Scope(const QVector &hridChain); + + Scope(const Scope &other); + Scope(Scope &&other); + ~Scope(); + + Scope &operator=(const Scope &other); + Scope &operator=(Scope &&other); + + bool operator==(const Scope &other) const; + bool operator!=(const Scope &other) const; + + SelectionScope scope() const; + + bool isEmpty() const; + + ImapSet uidSet() const; + void setUidSet(const ImapSet &uidSet); + + void setRidSet(const QStringList &ridSet); + QStringList ridSet() const; + + void setHRidChain(const QVector &ridChain); + QVector hridChain() const; + + void setGidSet(const QStringList &gidChain); + QStringList gidSet() const; + + qint64 uid() const; + QString rid() const; + QString gid() const; + +private: + QSharedDataPointer d; + friend class ScopePrivate; + + friend Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::Scope &scope); + friend Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::Scope &scope); +}; + +} // namespace Akonadi + +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug debug, const Akonadi::Scope &scope); + +#endif diff --git a/src/private/standarddirs.cpp b/src/private/standarddirs.cpp new file mode 100644 index 0000000..9ce0e05 --- /dev/null +++ b/src/private/standarddirs.cpp @@ -0,0 +1,83 @@ +/* + Copyright (c) 2011 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 "standarddirs_p.h" +#include "xdgbasedirs_p.h" +#include "instance_p.h" + + +#include + +using namespace Akonadi; + +QString StandardDirs::configFile(const QString &configFile, Akonadi::XdgBaseDirs::FileAccessMode openMode) +{ + const QString savePath = StandardDirs::saveDir("config") + QLatin1Char('/') + configFile; + + if (openMode == XdgBaseDirs::WriteOnly) { + return savePath; + } + + QString path = XdgBaseDirs::findResourceFile("config", QStringLiteral("akonadi/") + configFile); + // HACK: when using instance namespaces, ignore the non-namespaced file + if (Akonadi::Instance::hasIdentifier() && path.startsWith(XdgBaseDirs::homePath("config"))) { + path.clear(); + } + + if (path.isEmpty()) { + return savePath; + } else if (openMode == XdgBaseDirs::ReadOnly || path == savePath) { + return path; + } + + // file found in system paths and mode is ReadWrite, thus + // we copy to the home path location and return this path + QFile systemFile(path); + + systemFile.copy(savePath); + + return savePath; +} + +QString StandardDirs::serverConfigFile(XdgBaseDirs::FileAccessMode openMode) +{ + return configFile(QStringLiteral("akonadiserverrc"), openMode); +} + +QString StandardDirs::connectionConfigFile(XdgBaseDirs::FileAccessMode openMode) +{ + return configFile(QStringLiteral("akonadiconnectionrc"), openMode); +} + +QString StandardDirs::agentConfigFile(XdgBaseDirs::FileAccessMode openMode) +{ + return configFile(QStringLiteral("agentsrc"), openMode); +} + +QString StandardDirs::saveDir(const char *resource, const QString &relPath) +{ + QString fullRelPath = QStringLiteral("akonadi"); + if (Akonadi::Instance::hasIdentifier()) { + fullRelPath += QStringLiteral("/instance/") + Akonadi::Instance::identifier(); + } + if (!relPath.isEmpty()) { + fullRelPath += QLatin1Char('/') + relPath; + } + return XdgBaseDirs::saveDir(resource, fullRelPath); +} diff --git a/src/private/standarddirs_p.h b/src/private/standarddirs_p.h new file mode 100644 index 0000000..50844ed --- /dev/null +++ b/src/private/standarddirs_p.h @@ -0,0 +1,63 @@ +/* + Copyright (c) 2011 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 AKSTANDARDDIRS_H +#define AKSTANDARDDIRS_H + +#include "xdgbasedirs_p.h" +#include "akonadiprivate_export.h" + +namespace Akonadi { + +/** + * Convenience wrappers on top of XdgBaseDirs that are instance namespace aware. + * @since 1.7 + */ +namespace StandardDirs { +/** + * Returns path to the config file @p configFile. + */ +AKONADIPRIVATE_EXPORT QString configFile(const QString &configFile, Akonadi::XdgBaseDirs::FileAccessMode openMode = Akonadi::XdgBaseDirs::ReadOnly); + +/** + * Returns the full path to the server config file (akonadiserverrc). + */ +AKONADIPRIVATE_EXPORT QString serverConfigFile(Akonadi::XdgBaseDirs::FileAccessMode openMode = Akonadi::XdgBaseDirs::ReadOnly); + +/** + * Returns the full path to the connection config file (akonadiconnectionrc). + */ +AKONADIPRIVATE_EXPORT QString connectionConfigFile(Akonadi::XdgBaseDirs::FileAccessMode openMode = Akonadi::XdgBaseDirs::ReadOnly); + +/** + * Returns the full path to the agent config file (agentsrc). + */ +AKONADIPRIVATE_EXPORT QString agentConfigFile(Akonadi::XdgBaseDirs::FileAccessMode openMode = Akonadi::XdgBaseDirs::ReadOnly); + +/** + * Instance-aware wrapper for XdgBaseDirs::saveDir(). + * @note @p relPath does not need to include the "akonadi/" folder. + * @see XdgBaseDirs::saveDir() + */ +AKONADIPRIVATE_EXPORT QString saveDir(const char *resource, const QString &relPath = QString()); + +} +} + +#endif diff --git a/src/private/tristate.cpp b/src/private/tristate.cpp new file mode 100644 index 0000000..6b33eac --- /dev/null +++ b/src/private/tristate.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015 Daniel Vrátil + * + * 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 "tristate_p.h" + +using namespace Akonadi; + +QDebug operator<<(QDebug dbg, Tristate tristate) +{ + switch (tristate) + { + case Tristate::True: + return dbg << "True"; + case Tristate::False: + return dbg << "False"; + case Tristate::Undefined: + return dbg << "Undefined"; + } + + Q_ASSERT(false); + return dbg; +} diff --git a/src/private/tristate_p.h b/src/private/tristate_p.h new file mode 100644 index 0000000..87ca711 --- /dev/null +++ b/src/private/tristate_p.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2015 Daniel Vrátil + * + * 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_PRIVATE_TRISTATE_P_H_ +#define AKONADI_PRIVATE_TRISTATE_P_H_ + +#include +#include + +#include "akonadiprivate_export.h" + +namespace Akonadi +{ + +enum class Tristate : qint8 +{ + False = 0, + True = 1, + Undefined = 2 +}; + +} + +Q_DECLARE_METATYPE(Akonadi::Tristate) + +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, Akonadi::Tristate tristate); + +#endif diff --git a/src/private/xdgbasedirs.cpp b/src/private/xdgbasedirs.cpp new file mode 100644 index 0000000..4bb182b --- /dev/null +++ b/src/private/xdgbasedirs.cpp @@ -0,0 +1,585 @@ +/*************************************************************************** + * Copyright (C) 2007 by Kevin Krammer * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "xdgbasedirs_p.h" + +#include "akonadi-prefix.h" // for prefix defines + +#include "akonadiprivate_debug.h" + +#include +#include +#include +#include +#include +#include + +#include + +#ifdef Q_OS_WIN +# include +#endif + +static QStringList alternateExecPaths(const QString &path) +{ + QStringList pathList; + + pathList << path; + +#if defined(Q_OS_WIN) //krazy:exclude=cpp + pathList << path + QLatin1String(".exe"); +#elif defined(Q_OS_MAC) //krazy:exclude=cpp + pathList << path + QLatin1String(".app/Contents/MacOS/") + path.section(QLatin1Char('/'), -1); +#endif + + return pathList; +} + +static QStringList splitPathList(const QString &pathList) +{ +#if defined(Q_OS_WIN) //krazy:exclude=cpp + return pathList.split(QLatin1Char(';')); +#else + return pathList.split(QLatin1Char(':')); +#endif +} + +#ifdef Q_OS_WIN +static QMap getEnvironment() +{ + QMap ret; + Q_FOREACH(const QString& str, QProcessEnvironment::systemEnvironment().toStringList()) + { + const int p = str.indexOf(QLatin1Char('=')); + ret[str.left(p)] = str.mid(p + 1); + } + return ret; +} + +QString expandEnvironmentVariables(const QString& str) +{ + static QMap envVars = getEnvironment(); + static QRegExp possibleVars(QLatin1String("((\\{|%)(\\w+)(\\}|%))")); + QString ret = str; + while(possibleVars.indexIn(ret) != -1) + { + QStringList caps = possibleVars.capturedTexts(); + if(caps[2] == QLatin1String("{")) + { + ret.replace(QLatin1String("$") + caps[1], envVars[caps[3]]); + } + else + { + ret.replace(caps[1], envVars[caps[3]]); + } + QString key = possibleVars.cap(); + ret.replace(key, envVars[key]); + } + return ret; +} + +static QSettings* getKdeConf() +{ + WCHAR wPath[MAX_PATH+1]; + GetModuleFileNameW(NULL, wPath, MAX_PATH); + QString kdeconfPath = QString::fromUtf16((const ushort *) wPath); + kdeconfPath = kdeconfPath.left(kdeconfPath.lastIndexOf(QLatin1Char('\\'))).replace(QLatin1Char('\\'), QLatin1Char('/')); + if(QFile::exists(kdeconfPath + QString::fromLatin1("/kde.conf"))) + { + return new QSettings(kdeconfPath + QString::fromLatin1("/kde.conf"), QSettings::IniFormat); + } + else + { + return 0; + } +} +#endif + +namespace Akonadi { + +class XdgBaseDirsPrivate +{ +public: + XdgBaseDirsPrivate() + { + } + + ~XdgBaseDirsPrivate() + { + } +}; + +class XdgBaseDirsSingleton +{ +public: + QString homePath(const char *variable, const char *defaultSubDir); + + QStringList systemPathList(const char *variable, const char *defaultDirList); + +public: + QString mConfigHome; + QString mDataHome; + + QStringList mConfigDirs; + QStringList mDataDirs; + QStringList mExecutableDirs; + QStringList mPluginDirs; +}; + +Q_GLOBAL_STATIC(XdgBaseDirsSingleton, instance) + +} + +using namespace Akonadi; + +XdgBaseDirs::XdgBaseDirs() + : d(new XdgBaseDirsPrivate()) +{ +} + +XdgBaseDirs::~XdgBaseDirs() +{ + delete d; +} + +QString XdgBaseDirs::homePath(const char *resource) +{ +#ifdef Q_OS_WIN + static QSettings* kdeconf = getKdeConf(); +#endif + if (qstrncmp("data", resource, 4) == 0) { + if (instance()->mDataHome.isEmpty()) { +#ifdef Q_OS_WIN + if(kdeconf) { + kdeconf->beginGroup(QLatin1String("XDG")); + if(kdeconf->childKeys().contains(QLatin1String("XDG_DATA_HOME"))) + instance()->mDataHome = expandEnvironmentVariables(kdeconf->value(QLatin1String("XDG_DATA_HOME")).toString()); + else + instance()->mDataHome = instance()->homePath("XDG_DATA_HOME", ".local/share"); + kdeconf->endGroup(); + } else { +#else + { +#endif + instance()->mDataHome = instance()->homePath( "XDG_DATA_HOME", ".local/share" ); + } + } + return instance()->mDataHome; + } else if (qstrncmp("config", resource, 6) == 0) { + if (instance()->mConfigHome.isEmpty()) { +#ifdef Q_OS_WIN + if(kdeconf) { + kdeconf->beginGroup(QLatin1String("XDG")); + if(kdeconf->childKeys().contains(QLatin1String("XDG_CONFIG_HOME"))) + instance()->mConfigHome = expandEnvironmentVariables(kdeconf->value(QLatin1String("XDG_CONFIG_HOME")).toString()); + else + instance()->mConfigHome = instance()->homePath( "XDG_CONFIG_HOME", ".config" ); + kdeconf->endGroup(); + } else { +#else + { +#endif + instance()->mConfigHome = instance()->homePath( "XDG_CONFIG_HOME", ".config" ); + } + } + return instance()->mConfigHome; + } + + return QString(); +} + +QStringList XdgBaseDirs::systemPathList(const char *resource) +{ +#ifdef Q_OS_WIN + static QSettings* kdeconf = getKdeConf(); +#endif + if (qstrncmp("data", resource, 4) == 0) { + if (instance()->mDataDirs.isEmpty()) { +#ifdef Q_OS_WIN + QStringList dataDirs; + if(kdeconf) { + kdeconf->beginGroup(QLatin1String("XDG")); + if(kdeconf->childKeys().contains(QLatin1String("XDG_DATA_DIRS"))) { + dataDirs = instance()->systemPathList( "XDG_DATA_DIRS", expandEnvironmentVariables(kdeconf->value(QLatin1String("XDG_DATA_DIRS")).toString()).toLocal8Bit().constData() ); + } else { + QDir dir(QCoreApplication::applicationDirPath()); + dir.cdUp(); + const QString defaultPathList = dir.absoluteFilePath(QLatin1String("share")); + dataDirs = instance()->systemPathList( "XDG_DATA_DIRS", defaultPathList.toLocal8Bit().constData() ); + } + kdeconf->endGroup(); + } else { + QDir dir( QCoreApplication::applicationDirPath() ); + dir.cdUp(); + const QString defaultPathList = dir.absoluteFilePath( QLatin1String( "share" ) ); + dataDirs = instance()->systemPathList( "XDG_DATA_DIRS", defaultPathList.toLocal8Bit().constData() ); + } +#else + QStringList dataDirs = instance()->systemPathList("XDG_DATA_DIRS", "/usr/local/share:/usr/share"); +#endif + +#ifdef Q_OS_WIN + const QString prefixDataDir = QLatin1String(AKONADIPREFIX "/" AKONADIDATA); +#else + const QString prefixDataDir = QStringLiteral(AKONADIDATA); +#endif + if (!dataDirs.contains(prefixDataDir)) { + dataDirs << prefixDataDir; + } + + instance()->mDataDirs = dataDirs; + } +#ifdef Q_OS_WIN + QStringList dataDirs = instance()->mDataDirs; + // on Windows installation might be scattered across several directories + // so check if any installer providing agents has registered its base path + QSettings agentProviders(QSettings::SystemScope, QLatin1String("Akonadi"), QLatin1String("Akonadi")); + agentProviders.beginGroup(QLatin1String("AgentProviders")); + Q_FOREACH (const QString &agentProvider, agentProviders.childKeys()) { + const QString basePath = agentProviders.value(agentProvider).toString(); + if (!basePath.isEmpty()) { + const QString path = basePath + QDir::separator() + QLatin1String("share"); + if (!dataDirs.contains(path)) { + dataDirs << path; + } + } + } + + return dataDirs; +#else + return instance()->mDataDirs; +#endif + } else if (qstrncmp("config", resource, 6) == 0) { + if (instance()->mConfigDirs.isEmpty()) { +#ifdef Q_OS_WIN + QStringList configDirs; + if(kdeconf) { + kdeconf->beginGroup(QLatin1String("XDG")); + if(kdeconf->childKeys().contains(QLatin1String("XDG_CONFIG_DIRS"))) { + configDirs = instance()->systemPathList( "XDG_CONFIG_DIRS", expandEnvironmentVariables(kdeconf->value(QLatin1String("XDG_CONFIG_DIRS")).toString()).toLocal8Bit().constData() ); + } else { + QDir dir(QCoreApplication::applicationDirPath()); + dir.cdUp(); + const QString defaultPathList = dir.absoluteFilePath(QLatin1String("etc")) + QLatin1Char(';') + dir.absoluteFilePath(QLatin1String("share/config")); + configDirs = instance()->systemPathList( "XDG_CONFIG_DIRS", defaultPathList.toLocal8Bit().constData() ); + } + kdeconf->endGroup(); + } else { + QDir dir( QCoreApplication::applicationDirPath() ); + dir.cdUp(); + const QString defaultPathList = dir.absoluteFilePath( QLatin1String( "etc" ) ) + QLatin1Char( ';' ) + dir.absoluteFilePath( QLatin1String( "share/config" ) ); + configDirs = instance()->systemPathList( "XDG_CONFIG_DIRS", defaultPathList.toLocal8Bit().constData() ); + } +#else + QStringList configDirs = instance()->systemPathList("XDG_CONFIG_DIRS", "/etc/xdg"); +#endif + +#ifdef Q_OS_WIN + const QString prefixConfigDir = QLatin1String(AKONADIPREFIX "/" AKONADICONFIG); +#else + const QString prefixConfigDir = QStringLiteral(AKONADICONFIG); +#endif + if (!configDirs.contains(prefixConfigDir)) { + configDirs << prefixConfigDir; + } + + instance()->mConfigDirs = configDirs; + } + return instance()->mConfigDirs; + } + + return QStringList(); +} + +QString XdgBaseDirs::findResourceFile(const char *resource, const QString &relPath) +{ + const QString fullPath = homePath(resource) + QLatin1Char('/') + relPath; + + QFileInfo fileInfo(fullPath); + if (fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable()) { + return fullPath; + } + + const QStringList pathList = systemPathList(resource); + + Q_FOREACH (const QString &path, pathList) { + fileInfo = QFileInfo(path + QLatin1Char('/') + relPath); + if (fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable()) { + return fileInfo.absoluteFilePath(); + } + } + + return QString(); +} + +QString XdgBaseDirs::findExecutableFile(const QString &relPath, const QStringList &searchPath) +{ + if (instance()->mExecutableDirs.isEmpty()) { + QStringList executableDirs = instance()->systemPathList("PATH", "/usr/local/bin:/usr/bin"); + + const QString prefixExecutableDir = QStringLiteral(AKONADIPREFIX "/bin"); + if (!executableDirs.contains(prefixExecutableDir)) { + executableDirs << prefixExecutableDir; + } + + if (QCoreApplication::instance() != 0) { + const QString appExecutableDir = QCoreApplication::instance()->applicationDirPath(); + if (!executableDirs.contains(appExecutableDir)) { + executableDirs << appExecutableDir; + } + } + + executableDirs += searchPath; + +#if defined(Q_OS_MAC) //krazy:exclude=cpp + executableDirs += QLatin1String(AKONADIBUNDLEPATH); +#endif + qCWarning(AKONADIPRIVATE_LOG) << "search paths: " << executableDirs; + + instance()->mExecutableDirs = executableDirs; + } + +#ifdef Q_OS_WIN + QStringList executableDirs = instance()->mExecutableDirs; + // on Windows installation might be scattered across several directories + // so check if any installer providing agents has registered its base path + QSettings agentProviders(QSettings::SystemScope, QLatin1String("Akonadi"), QLatin1String("Akonadi")); + agentProviders.beginGroup(QLatin1String("AgentProviders")); + Q_FOREACH (const QString &agentProvider, agentProviders.childKeys()) { + const QString basePath = agentProviders.value(agentProvider).toString(); + if (!basePath.isEmpty()) { + const QString path = basePath + QDir::separator() + QLatin1String("bin"); + if (!executableDirs.contains(path)) { + executableDirs << path; + } + } + } + + QStringList::const_iterator pathIt = executableDirs.constBegin(); + const QStringList::const_iterator pathEndIt = executableDirs.constEnd(); +#else + QStringList::const_iterator pathIt = instance()->mExecutableDirs.constBegin(); + const QStringList::const_iterator pathEndIt = instance()->mExecutableDirs.constEnd(); +#endif + for (; pathIt != pathEndIt; ++pathIt) { + const QStringList fullPathList = alternateExecPaths(*pathIt + QLatin1Char('/') + relPath); + + QStringList::const_iterator it = fullPathList.constBegin(); + const QStringList::const_iterator endIt = fullPathList.constEnd(); + for (; it != endIt; ++it) { + const QFileInfo fileInfo(*it); + + // resolve symlinks, happens eg. with Maemo optify + if (fileInfo.canonicalFilePath().isEmpty()) { + continue; + } + + const QFileInfo canonicalFileInfo(fileInfo.canonicalFilePath()); + + if (canonicalFileInfo.exists() && canonicalFileInfo.isFile() && canonicalFileInfo.isExecutable()) { + return *it; + } + } + } + + return QString(); +} + +QStringList XdgBaseDirs::findPluginDirs() +{ + if (instance()->mPluginDirs.isEmpty()) { + QStringList pluginDirs = instance()->systemPathList("QT_PLUGIN_PATH", AKONADILIB ":" AKONADILIB "/qt5/plugins/:" AKONADILIB "/kf5/:" AKONADILIB "/kf5/plugins/:/usr/lib/qt5/plugins/"); + if (QCoreApplication::instance() != 0) { + Q_FOREACH (const QString &libraryPath, QCoreApplication::instance()->libraryPaths()) { + if (!pluginDirs.contains(libraryPath)) { + pluginDirs << libraryPath; + } + } + } + qCWarning(AKONADIPRIVATE_LOG) << "search paths: " << pluginDirs; + instance()->mPluginDirs = pluginDirs; + } + + return instance()->mPluginDirs; +} + +QString XdgBaseDirs::findPluginFile(const QString &relPath, const QStringList &searchPath) +{ + const QStringList searchDirs = findPluginDirs() + searchPath; + +#if defined(Q_OS_WIN) //krazy:exclude=cpp + const QString pluginName = relPath + QLatin1String(".dll"); +#else + const QString pluginName = relPath + QLatin1String(".so"); +#endif + + Q_FOREACH (const QString &path, searchDirs) { + const QFileInfo fileInfo(path + QDir::separator() + pluginName); + + // resolve symlinks, happens eg. with Maemo optify + if (fileInfo.canonicalFilePath().isEmpty()) { + continue; + } + + const QFileInfo canonicalFileInfo(fileInfo.canonicalFilePath()); + if (canonicalFileInfo.exists() && canonicalFileInfo.isFile()) { + return canonicalFileInfo.absoluteFilePath(); + } + } + + return QString(); +} + +QString XdgBaseDirs::findResourceDir(const char *resource, const QString &relPath) +{ + QString fullPath = homePath(resource) + QLatin1Char('/') + relPath; + + QFileInfo fileInfo(fullPath); + if (fileInfo.exists() && fileInfo.isDir() && fileInfo.isReadable()) { + return fullPath; + } + + Q_FOREACH (const QString &path, systemPathList(resource)) { + fileInfo = QFileInfo(path + QLatin1Char('/') + relPath); + if (fileInfo.exists() && fileInfo.isDir() && fileInfo.isReadable()) { + return fileInfo.absoluteFilePath(); + } + } + + return QString(); +} + +QStringList XdgBaseDirs::findAllResourceDirs(const char *resource, const QString &relPath) +{ + QStringList resultList; + + const QString fullPath = homePath(resource) + QLatin1Char('/') + relPath; + + QFileInfo fileInfo(fullPath); + if (fileInfo.exists() && fileInfo.isDir() && fileInfo.isReadable()) { + resultList << fileInfo.absoluteFilePath(); + } + + Q_FOREACH (const QString &path, systemPathList(resource)) { + fileInfo = QFileInfo(path + QLatin1Char('/') + relPath); + if (fileInfo.exists() && fileInfo.isDir() && fileInfo.isReadable()) { + const QString absPath = fileInfo.absoluteFilePath(); + if (!resultList.contains(absPath)) { + resultList << absPath; + } + } + } + + return resultList; +} + +QString XdgBaseDirs::saveDir(const char *resource, const QString &relPath) +{ + const QString fullPath = homePath(resource) + QLatin1Char('/') + relPath; + + QFileInfo fileInfo(fullPath); + if (fileInfo.exists()) { + if (fileInfo.isDir()) { + return fullPath; + } else { + qCWarning(AKONADIPRIVATE_LOG) << "XdgBaseDirs::saveDir: '" << fileInfo.absoluteFilePath() + << "' exists but is not a directory"; + } + } else { + if (!QDir::home().mkpath(fileInfo.absoluteFilePath())) { + qCWarning(AKONADIPRIVATE_LOG) << "XdgBaseDirs::saveDir: failed to create directory '" + << fileInfo.absoluteFilePath() << "'"; + } else { + return fullPath; + } + } + + return QString(); +} + +QString XdgBaseDirs::akonadiServerConfigFile(FileAccessMode openMode) +{ + return akonadiConfigFile(QStringLiteral("akonadiserverrc"), openMode); +} + +QString XdgBaseDirs::akonadiConnectionConfigFile(FileAccessMode openMode) +{ + return akonadiConfigFile(QStringLiteral("akonadiconnectionrc"), openMode); +} + +QString XdgBaseDirs::akonadiConfigFile(const QString &file, FileAccessMode openMode) +{ + const QString akonadiDir = QStringLiteral("akonadi"); + + const QString savePath = saveDir("config", akonadiDir) + QLatin1Char('/') + file; + + if (openMode == WriteOnly) { + return savePath; + } + + const QString path = findResourceFile("config", akonadiDir + QLatin1Char('/') + file); + + if (path.isEmpty()) { + return savePath; + } else if (openMode == ReadOnly || path == savePath) { + return path; + } + + // file found in system paths and mode is ReadWrite, thus + // we copy to the home path location and return this path + QFile systemFile(path); + + systemFile.copy(savePath); + + return savePath; +} + +QString XdgBaseDirsSingleton::homePath(const char *variable, const char *defaultSubDir) +{ + const QByteArray env = qgetenv(variable); + + QString xdgPath; + if (env.isEmpty()) { + xdgPath = QDir::homePath() + QLatin1Char('/') + QLatin1String(defaultSubDir); +#if defined(Q_OS_WIN) //krazy:exclude=cpp + } else if (QDir::isAbsolutePath(QString::fromLocal8Bit(env))) { +#else + } else if (env.startsWith('/')) { +#endif + xdgPath = QString::fromLocal8Bit(env); + } else { + xdgPath = QDir::homePath() + QLatin1Char('/') + QString::fromLocal8Bit(env); + } + + return xdgPath; +} + +QStringList XdgBaseDirsSingleton::systemPathList(const char *variable, const char *defaultDirList) +{ + const QByteArray env = qgetenv(variable); + + QString xdgDirList; + if (env.isEmpty()) { + xdgDirList = QLatin1String(defaultDirList); + } else { + xdgDirList = QString::fromLocal8Bit(env); + } + + return splitPathList(xdgDirList); +} diff --git a/src/private/xdgbasedirs_p.h b/src/private/xdgbasedirs_p.h new file mode 100644 index 0000000..e666916 --- /dev/null +++ b/src/private/xdgbasedirs_p.h @@ -0,0 +1,322 @@ +/*************************************************************************** + * Copyright (C) 2007 by Kevin Krammer * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef XDGBASEDIRS_H +#define XDGBASEDIRS_H + +// Qt includes +#include +#include + +#include "akonadiprivate_export.h" + +// forward declarations +class QString; + +namespace Akonadi { + +class XdgBaseDirsPrivate; + +/** + @brief Resource type based handling of standard directories + + Developers of several Free Software desktop projects have created + a specification for handling so-called "base directories", i.e. + lists of system wide directories and directories within each user's + home directory for installing and later finding certain files. + + This class handles the respective behaviour, i.e. environment variables + and their defaults, for the following type of resources: + - "config" + - "data" + + Example: getting the Akonadi server config file "akonadiserverrc", assuming + that Akonadi stores its config in an additional subdirectoy called "akonadi" + @code + QString relativeFileName = QLatin1String( "akonadi/akonadiserverrc" ); + + // look for the file "akonadiserverrc" with additional subdirectory "akonadi" + // in any directory associated with resource type "config" + QString configFile = XdgBaseDirs::findResourceFile( "config", relativeFileName ); + + if ( configFile.isEmpty() ) { + // No config file yet, get the suitable user specific directory for storing + // a new one + configFile = XdgBaseDirs::saveDir( "config", QLatin1String( "akonadi" ) ); + configFile += QLatin1String( "akonadiserverrc" ); + } + + QSettings serverConfig( configFile ); + @endcode + + @author Kevin Krammer, + + @see http://www.freedesktop.org/wiki/Specifications/basedir-spec + */ +class AKONADIPRIVATE_EXPORT XdgBaseDirs +{ +public: + /** + @brief Creates the instance + */ + XdgBaseDirs(); + + /** + @brief Destroys the instance + */ + ~XdgBaseDirs(); + + /** + @brief Returns the user specific directory for the given resource type + + Unless the user's environment has a specific path set as an override + this will be the default as defined in the freedesktop.org base-dir-spec + + @note Caches the value of the first call + + @param resource a named resource type, e.g. "config" + + @return a directory path + + @see systemPathList() + @see saveDir() + */ + static QString homePath(const char *resource); + + /** + @brief Returns the list of system wide directories for a given resource type + + The returned list can contain one or more directory paths. If there are more + than one, the list is sorted by falling priority, i.e. if an entry is valid + for the respective use case (e.g. contains a file the application looks for) + the list should not be processed further. + + @note The user's resource path should, to be compliant with the spec, + always be treated as having higher priority than any path in the + list of system wide paths + + @note Caches the value of the first call + + @param resource a named resource type, e.g. "config" + + @return a priority sorted list of directory paths + + @see homePath() + */ + static QStringList systemPathList(const char *resource); + + /** + @brief Searches the resource specific directories for a given file + + Convenience method for finding a given file (with optional relative path) + in any of the configured base directories for a given resource type. + + Will check the user local directory first and then process the system + wide path list according to the inherent priority. + + @param resource a named resource type, e.g. "config" + @param relPath relative path of a file to look for, + e.g."akonadi/akonadiserverrc" + + @returns the file path of the first match, or @c QString() if no such + relative path exists in any of the base directories or if + a match is not a file + + @see findResourceDir() + @see saveDir + */ + static QString findResourceFile(const char *resource, const QString &relPath); + + /** + @brief Searches the executable specific directories for a given file + + Convenience method for finding a given executable (with optional relative path) + in any of the configured directories for a this special type. + + @note This is not based on the XDG base dir spec, since it does not cover + executable + + @param relPath relative path of a file to look for, + e.g."akonadiserver" + @param searchPath additional paths to search for the executable, + only used if the file was not found in PATH and the install prefix + + @returns the file path of the first match, or @c QString() if no such + relative path exists in any of the base directories + + @see findResourceFile() + */ + static QString findExecutableFile(const QString &relPath, const QStringList &searchPath = QStringList()); + + /** + @brief Searches the plugin specific directories for a given file + + Convenience method for finding a given plugin (with optional relative path) + in any of the configured directories for a this special type. + + @note This is not based on the XDG base dir spec, since it does not cover + plugins + + @param relPath relative path of a file to look for, + e.g."akonadi_knut_resource" + @param searchPath additional paths to search for the plugin, + only used if the file was not found in QT_PLUGIN_PATH and the install prefix + + @returns the file path of the first match, or @c QString() if no such + relative path exists in any of the base directories + + @see findResourceFile() + */ + static QString findPluginFile(const QString &relPath, const QStringList &searchPath = QStringList()); + + /** + @brief Returns plugin specific directories + + Convenience method for listing directories that can be scanned for available + plugins. + + @note This is not based on the XDG base dir spec, since it does not cover + plugins. + + @return directories where application should look for plugins + */ + static QStringList findPluginDirs(); + + /** + @brief Searches the resource specific directories for a given subdirectory + + Convenience method for finding a given relative subdirectory in any of + the configured base directories for a given resource type. + + Will check the user local directory first and then process the system + wide path list according to the inherent priority. + + Use findAllResourceDirs() if looking for all directories with the given + subdirectory. + + @param resource a named resource type, e.g. "config" + @param relPath relative path of a subdirectory to look for, + e.g."akonadi/agents" + + @returns the directory path of the first match, or @c QString() if no such + relative path exists in any of the base directories or if + a match is not a directory + + @see findResourceFile() + @see saveDir() + */ + static QString findResourceDir(const char *resource, const QString &relPath); + + /** + @brief Searches the resource specific directories for a given subdirectory + + Convenience method for getting a list of directoreis with a given relative + subdirectory in any of the configured base directories for a given + resource type. + + Will check the user local directory first and then process the system + wide path list according to the inherent priority. + + Similar to findResourceDir() but does not just find the first best match + but all matching resource directories. The resuling list will be sorted + according to the same proprity criteria. + + @param resource a named resource type, e.g. "config" + @param relPath relative path of a subdirectory to look for, + e.g."akonadi/agents" + + @returns a list of directory paths, or @c QString() if no such + relative path exists in any of the base directories or if + non of the matches is a directory + + @see findResourceDir() + */ + static QStringList findAllResourceDirs(const char *resource, const QString &relPath); + + /** + @brief Finds or creates the "save to" directory for a given resource + + Convenience method for creating subdirectores relative to a given + resource type's user directory, i.e. homePath() + relPath + + If the target directory does not exists, it an all necessary parent + directories will be created, unless denied by the filesystem. + + @param resource a named resource type, e.g. "config" + @param relPath relative path of a directory to be used for file writing + + @return the directory path of the "save to" directory or @c QString() + if the directory or one of its parents could not be created + + @see findResourceDir() + */ + static QString saveDir(const char *resource, const QString &relPath); + + /** + * @brief Open mode flags for resource files + * + * FileAccessMode is a typedef for QFlags. It stores + * a OR combination of FileAccessFlag values + */ + enum FileAccessFlag { + ReadOnly = 0x1, + WriteOnly = 0x2, + ReadWrite = ReadOnly | WriteOnly + }; + + typedef QFlags FileAccessMode; + + /** + * @brief Returns the path of the Akonadi server config file + * + * Convenience method for getting the server config file "akonadiserverrc" + * since this is an often needed procedure in several parts of the code. + * + * @param openMode how the application wants to use the config file + * + * @return the path of the server config file, suitable for \p openMode + */ + static QString akonadiServerConfigFile(FileAccessMode openMode = ReadOnly); + + /** + * @brief Returns the path of the Akonadi data connection config file + * + * Convenience method for getting the server config file "akonadiconnectionrc" + * since this is an often needed procedure in several parts of the code. + * + * @param openMode how the application wants to use the config file + * + * @return the path of the data connection config file, suitable for \p openMode + */ + static QString akonadiConnectionConfigFile(FileAccessMode openMode = ReadOnly); + +private: + XdgBaseDirsPrivate *const d; + +private: + static QString akonadiConfigFile(const QString &file, FileAccessMode openMode); + +private: + XdgBaseDirs(const XdgBaseDirs &); + XdgBaseDirs &operator=(const XdgBaseDirs &); +}; + +} + +#endif diff --git a/src/qsqlite/.no_coding_style b/src/qsqlite/.no_coding_style new file mode 100644 index 0000000..40fa8f6 --- /dev/null +++ b/src/qsqlite/.no_coding_style @@ -0,0 +1 @@ +# this directory will not be tested. diff --git a/src/qsqlite/CMakeLists.txt b/src/qsqlite/CMakeLists.txt new file mode 100644 index 0000000..228cd10 --- /dev/null +++ b/src/qsqlite/CMakeLists.txt @@ -0,0 +1,28 @@ +set(QSqlite_SRCS + src/sqlite_blocking.cpp + src/qsql_sqlite.cpp + src/smain.cpp +) + +message(STATUS "Building QSQLITE3 driver") + +set(QSQLITE_INSTALL_PREFIX "${PLUGIN_INSTALL_DIR}/sqldrivers") + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${SQLITE_INCLUDE_DIR} +) + +add_library(qsqlite3 SHARED ${QSqlite_SRCS} ${QSqlite_MOC_SRCS}) + +target_link_libraries(qsqlite3 + Qt5::Core + Qt5::Sql + ${SQLITE_LIBRARIES} +) + +INSTALL(TARGETS qsqlite3 + RUNTIME DESTINATION ${QSQLITE_INSTALL_PREFIX} + LIBRARY DESTINATION ${QSQLITE_INSTALL_PREFIX} + ARCHIVE DESTINATION ${QSQLITE_INSTALL_PREFIX} +) diff --git a/src/qsqlite/README b/src/qsqlite/README new file mode 100644 index 0000000..72864b6 --- /dev/null +++ b/src/qsqlite/README @@ -0,0 +1,3 @@ +This is a sliglty adjusted version of the QSQLITE driver. Install this driver +somewhere in the QT_PLUGIN_PATH and use it in akonadi by setting the driver +to QSQLITE3. diff --git a/src/qsqlite/src/QtSql/private/qobject_p.h b/src/qsqlite/src/QtSql/private/qobject_p.h new file mode 100644 index 0000000..180887b --- /dev/null +++ b/src/qsqlite/src/QtSql/private/qobject_p.h @@ -0,0 +1,434 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Olivier Goffart +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QOBJECT_P_H +#define QOBJECT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of qapplication_*.cpp, qwidget*.cpp and qfiledialog.cpp. This header +// file may change from version to version without notice, or even be removed. +// +// We mean it. +// + +#include "QtCore/qobject.h" +#include "QtCore/qpointer.h" +#include "QtCore/qsharedpointer.h" +#include "QtCore/qcoreevent.h" +#include "QtCore/qlist.h" +#include "QtCore/qvector.h" +#include "QtCore/qvariant.h" +#include "QtCore/qreadwritelock.h" + +QT_BEGIN_NAMESPACE + +class QVariant; +class QThreadData; +class QObjectConnectionListVector; +namespace QtSharedPointer { struct ExternalRefCountData; } + +/* for Qt Test */ +struct QSignalSpyCallbackSet +{ + typedef void (*BeginCallback)(QObject *caller, int signal_or_method_index, void **argv); + typedef void (*EndCallback)(QObject *caller, int signal_or_method_index); + BeginCallback signal_begin_callback, + slot_begin_callback; + EndCallback signal_end_callback, + slot_end_callback; +}; +void Q_CORE_EXPORT qt_register_signal_spy_callbacks(const QSignalSpyCallbackSet &callback_set); + +extern QSignalSpyCallbackSet Q_CORE_EXPORT qt_signal_spy_callback_set; + +enum { QObjectPrivateVersion = QT_VERSION }; + +class Q_CORE_EXPORT QAbstractDeclarativeData +{ +public: + static void (*destroyed)(QAbstractDeclarativeData *, QObject *); + static void (*destroyed_qml1)(QAbstractDeclarativeData *, QObject *); + static void (*parentChanged)(QAbstractDeclarativeData *, QObject *, QObject *); + static void (*signalEmitted)(QAbstractDeclarativeData *, QObject *, int, void **); + static int (*receivers)(QAbstractDeclarativeData *, const QObject *, int); + static bool (*isSignalConnected)(QAbstractDeclarativeData *, const QObject *, int); +}; + +// This is an implementation of QAbstractDeclarativeData that is identical with +// the implementation in QtDeclarative and QtQml for the first bit +struct QAbstractDeclarativeDataImpl : public QAbstractDeclarativeData +{ + quint32 ownedByQml1:1; + quint32 unused: 31; +}; + +class Q_CORE_EXPORT QObjectPrivate : public QObjectData +{ + Q_DECLARE_PUBLIC(QObject) + +public: + struct ExtraData + { + ExtraData() {} + #ifndef QT_NO_USERDATA + QVector userData; + #endif + QList propertyNames; + QList propertyValues; + QVector runningTimers; + QList > eventFilters; + QString objectName; + }; + + typedef void (*StaticMetaCallFunction)(QObject *, QMetaObject::Call, int, void **); + struct Connection + { + QObject *sender; + QObject *receiver; + union { + StaticMetaCallFunction callFunction; + QtPrivate::QSlotObjectBase *slotObj; + }; + // The next pointer for the singly-linked ConnectionList + Connection *nextConnectionList; + //senders linked list + Connection *next; + Connection **prev; + QAtomicPointer argumentTypes; + QAtomicInt ref_; + ushort method_offset; + ushort method_relative; + uint signal_index : 27; // In signal range (see QObjectPrivate::signalIndex()) + ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking + ushort isSlotObject : 1; + ushort ownArgumentTypes : 1; + Connection() : nextConnectionList(0), ref_(2), ownArgumentTypes(true) { + //ref_ is 2 for the use in the internal lists, and for the use in QMetaObject::Connection + } + ~Connection(); + int method() const { return method_offset + method_relative; } + void ref() { ref_.ref(); } + void deref() { + if (!ref_.deref()) { + Q_ASSERT(!receiver); + delete this; + } + } + }; + // ConnectionList is a singly-linked list + struct ConnectionList { + ConnectionList() : first(0), last(0) {} + Connection *first; + Connection *last; + }; + + struct Sender + { + QObject *sender; + int signal; + int ref; + }; + + + QObjectPrivate(int version = QObjectPrivateVersion); + virtual ~QObjectPrivate(); + void deleteChildren(); + + void setParent_helper(QObject *); + void moveToThread_helper(); + void setThreadData_helper(QThreadData *currentData, QThreadData *targetData); + void _q_reregisterTimers(void *pointer); + + bool isSender(const QObject *receiver, const char *signal) const; + QObjectList receiverList(const char *signal) const; + QObjectList senderList() const; + + void addConnection(int signal, Connection *c); + void cleanConnectionLists(); + + static inline Sender *setCurrentSender(QObject *receiver, + Sender *sender); + static inline void resetCurrentSender(QObject *receiver, + Sender *currentSender, + Sender *previousSender); + + static QObjectPrivate *get(QObject *o) { + return o->d_func(); + } + + int signalIndex(const char *signalName, const QMetaObject **meta = 0) const; + inline bool isSignalConnected(uint signalIdx) const; + + // To allow abitrary objects to call connectNotify()/disconnectNotify() without making + // the API public in QObject. This is used by QQmlNotifierEndpoint. + inline void connectNotify(const QMetaMethod &signal); + inline void disconnectNotify(const QMetaMethod &signal); + + template + static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer::Object *sender, Func1 signal, + const typename QtPrivate::FunctionPointer::Object *receiverPrivate, Func2 slot, + Qt::ConnectionType type = Qt::AutoConnection); + + template + static inline bool disconnect(const typename QtPrivate::FunctionPointer::Object *sender, Func1 signal, + const typename QtPrivate::FunctionPointer::Object *receiverPrivate, Func2 slot); + + static QMetaObject::Connection connectImpl(const QObject *sender, int signal_index, + const QObject *receiver, void **slot, + QtPrivate::QSlotObjectBase *slotObj, Qt::ConnectionType type, + const int *types, const QMetaObject *senderMetaObject); + static QMetaObject::Connection connect(const QObject *sender, int signal_index, QtPrivate::QSlotObjectBase *slotObj, Qt::ConnectionType type); + static bool disconnect(const QObject *sender, int signal_index, void **slot); +public: + ExtraData *extraData; // extra data set by the user + QThreadData *threadData; // id of the thread that owns the object + + QObjectConnectionListVector *connectionLists; + + Connection *senders; // linked list of connections connected to this object + Sender *currentSender; // object currently activating the object + mutable quint32 connectedSignals[2]; + + union { + QObject *currentChildBeingDeleted; + QAbstractDeclarativeData *declarativeData; //extra data used by the declarative module + }; + + // these objects are all used to indicate that a QObject was deleted + // plus QPointer, which keeps a separate list + QAtomicPointer sharedRefcount; +}; + + +/*! \internal + + Returns \c true if the signal with index \a signal_index from object \a sender is connected. + Signals with indices above a certain range are always considered connected (see connectedSignals + in QObjectPrivate). + + \a signal_index must be the index returned by QObjectPrivate::signalIndex; +*/ +inline bool QObjectPrivate::isSignalConnected(uint signal_index) const +{ + return signal_index >= sizeof(connectedSignals) * 8 + || (connectedSignals[signal_index >> 5] & (1 << (signal_index & 0x1f)) + || (declarativeData && QAbstractDeclarativeData::isSignalConnected + && QAbstractDeclarativeData::isSignalConnected(declarativeData, q_func(), signal_index))); +} + +inline QObjectPrivate::Sender *QObjectPrivate::setCurrentSender(QObject *receiver, + Sender *sender) +{ + Sender *previousSender = receiver->d_func()->currentSender; + receiver->d_func()->currentSender = sender; + return previousSender; +} + +inline void QObjectPrivate::resetCurrentSender(QObject *receiver, + Sender *currentSender, + Sender *previousSender) +{ + // ref is set to zero when this object is deleted during the metacall + if (currentSender->ref == 1) + receiver->d_func()->currentSender = previousSender; + // if we've recursed, we need to tell the caller about the objects deletion + if (previousSender) + previousSender->ref = currentSender->ref; +} + +inline void QObjectPrivate::connectNotify(const QMetaMethod &signal) +{ + q_ptr->connectNotify(signal); +} + +inline void QObjectPrivate::disconnectNotify(const QMetaMethod &signal) +{ + q_ptr->disconnectNotify(signal); +} + +namespace QtPrivate { +template class QPrivateSlotObject : public QSlotObjectBase +{ + typedef QtPrivate::FunctionPointer FuncType; + Func function; + static void impl(int which, QSlotObjectBase *this_, QObject *r, void **a, bool *ret) + { + switch (which) { + case Destroy: + delete static_cast(this_); + break; + case Call: + FuncType::template call(static_cast(this_)->function, + static_cast(QObjectPrivate::get(r)), a); + break; + case Compare: + *ret = *reinterpret_cast(a) == static_cast(this_)->function; + break; + case NumOperations: ; + } + } +public: + explicit QPrivateSlotObject(Func f) : QSlotObjectBase(&impl), function(f) {} +}; +} //namespace QtPrivate + +template +inline QMetaObject::Connection QObjectPrivate::connect(const typename QtPrivate::FunctionPointer::Object *sender, Func1 signal, + const typename QtPrivate::FunctionPointer::Object *receiverPrivate, Func2 slot, + Qt::ConnectionType type) +{ + typedef QtPrivate::FunctionPointer SignalType; + typedef QtPrivate::FunctionPointer SlotType; + Q_STATIC_ASSERT_X(QtPrivate::HasQ_OBJECT_Macro::Value, + "No Q_OBJECT in the class with the signal"); + + //compilation error if the arguments does not match. + Q_STATIC_ASSERT_X(int(SignalType::ArgumentCount) >= int(SlotType::ArgumentCount), + "The slot requires more arguments than the signal provides."); + Q_STATIC_ASSERT_X((QtPrivate::CheckCompatibleArguments::value), + "Signal and slot arguments are not compatible."); + Q_STATIC_ASSERT_X((QtPrivate::AreArgumentsCompatible::value), + "Return type of the slot is not compatible with the return type of the signal."); + + const int *types = 0; + if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection) + types = QtPrivate::ConnectionTypes::types(); + + return QObject::connectImpl(sender, reinterpret_cast(&signal), + receiverPrivate->q_ptr, reinterpret_cast(&slot), + new QtPrivate::QPrivateSlotObject::Value, + typename SignalType::ReturnType>(slot), + type, types, &SignalType::Object::staticMetaObject); +} + +template +bool QObjectPrivate::disconnect(const typename QtPrivate::FunctionPointer< Func1 >::Object* sender, Func1 signal, + const typename QtPrivate::FunctionPointer< Func2 >::Object* receiverPrivate, Func2 slot) +{ + typedef QtPrivate::FunctionPointer SignalType; + typedef QtPrivate::FunctionPointer SlotType; + Q_STATIC_ASSERT_X(QtPrivate::HasQ_OBJECT_Macro::Value, + "No Q_OBJECT in the class with the signal"); + //compilation error if the arguments does not match. + Q_STATIC_ASSERT_X((QtPrivate::CheckCompatibleArguments::value), + "Signal and slot arguments are not compatible."); + return QObject::disconnectImpl(sender, reinterpret_cast(&signal), + receiverPrivate->q_ptr, reinterpret_cast(&slot), + &SignalType::Object::staticMetaObject); +} + +Q_DECLARE_TYPEINFO(QObjectPrivate::Connection, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(QObjectPrivate::Sender, Q_MOVABLE_TYPE); + +class QSemaphore; +class Q_CORE_EXPORT QMetaCallEvent : public QEvent +{ +public: + QMetaCallEvent(ushort method_offset, ushort method_relative, QObjectPrivate::StaticMetaCallFunction callFunction , const QObject *sender, int signalId, + int nargs = 0, int *types = 0, void **args = 0, QSemaphore *semaphore = 0); + /*! \internal + \a signalId is in the signal index range (see QObjectPrivate::signalIndex()). + */ + QMetaCallEvent(QtPrivate::QSlotObjectBase *slotObj, const QObject *sender, int signalId, + int nargs = 0, int *types = 0, void **args = 0, QSemaphore *semaphore = 0); + + ~QMetaCallEvent(); + + inline int id() const { return method_offset_ + method_relative_; } + inline const QObject *sender() const { return sender_; } + inline int signalId() const { return signalId_; } + inline void **args() const { return args_; } + + virtual void placeMetaCall(QObject *object); + +private: + QtPrivate::QSlotObjectBase *slotObj_; + const QObject *sender_; + int signalId_; + int nargs_; + int *types_; + void **args_; + QSemaphore *semaphore_; + QObjectPrivate::StaticMetaCallFunction callFunction_; + ushort method_offset_; + ushort method_relative_; +}; + +class QBoolBlocker +{ + Q_DISABLE_COPY(QBoolBlocker) +public: + explicit inline QBoolBlocker(bool &b, bool value=true):block(b), reset(b){block = value;} + inline ~QBoolBlocker(){block = reset; } +private: + bool █ + bool reset; +}; + +void Q_CORE_EXPORT qDeleteInEventHandler(QObject *o); + +struct QAbstractDynamicMetaObject; +struct Q_CORE_EXPORT QDynamicMetaObjectData +{ + virtual ~QDynamicMetaObjectData() {} + virtual void objectDestroyed(QObject *) { delete this; } + + virtual QAbstractDynamicMetaObject *toDynamicMetaObject(QObject *) = 0; + virtual int metaCall(QObject *, QMetaObject::Call, int _id, void **) = 0; +}; + +struct Q_CORE_EXPORT QAbstractDynamicMetaObject : public QDynamicMetaObjectData, public QMetaObject +{ + virtual QAbstractDynamicMetaObject *toDynamicMetaObject(QObject *) { return this; } + virtual int createProperty(const char *, const char *) { return -1; } + virtual int metaCall(QObject *, QMetaObject::Call c, int _id, void **a) + { return metaCall(c, _id, a); } + virtual int metaCall(QMetaObject::Call, int _id, void **) { return _id; } // Compat overload +}; + +QT_END_NAMESPACE + +#endif // QOBJECT_P_H diff --git a/src/qsqlite/src/QtSql/private/qsqlcachedresult_p.h b/src/qsqlite/src/QtSql/private/qsqlcachedresult_p.h new file mode 100644 index 0000000..ce15cfe --- /dev/null +++ b/src/qsqlite/src/QtSql/private/qsqlcachedresult_p.h @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtSql module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial Usage +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSQLCACHEDRESULT_P_H +#define QSQLCACHEDRESULT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "QtSql/qsqlresult.h" + +QT_BEGIN_NAMESPACE + +class QVariant; +template class QVector; + +class QSqlCachedResultPrivate; + +class Q_SQL_EXPORT QSqlCachedResult: public QSqlResult +{ +public: + virtual ~QSqlCachedResult(); + + typedef QVector ValueCache; + +protected: + QSqlCachedResult(const QSqlDriver * db); + + void init(int colCount); + void cleanup(); + void clearValues(); + + virtual bool gotoNext(ValueCache &values, int index) = 0; + + QVariant data(int i); + bool isNull(int i); + bool fetch(int i); + bool fetchNext(); + bool fetchPrevious(); + bool fetchFirst(); + bool fetchLast(); + + int colCount() const; + ValueCache &cache(); + + void virtual_hook(int id, void *data); +private: + bool cacheNext(); + QSqlCachedResultPrivate *d; +}; + +QT_END_NAMESPACE + +#endif // QSQLCACHEDRESULT_P_H diff --git a/src/qsqlite/src/QtSql/private/qsqldriver_p.h b/src/qsqlite/src/QtSql/private/qsqldriver_p.h new file mode 100644 index 0000000..15b51f8 --- /dev/null +++ b/src/qsqlite/src/QtSql/private/qsqldriver_p.h @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtSql module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSQLDRIVER_P_H +#define QSQLDRIVER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the QtSQL module. This header file may change from version to version +// without notice, or even be removed. +// +// We mean it. +// + +#include "qobject_p.h" +#include "qsqldriver.h" +#include "qsqlerror.h" + +QT_BEGIN_NAMESPACE + +class QSqlDriverPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QSqlDriver) + +public: + enum DBMSType {UnknownDB, MSSqlServer, MySqlServer, PostgreSQL, Oracle, Sybase, SQLite, Interbase, DB2}; + + QSqlDriverPrivate() + : QObjectPrivate(), + isOpen(false), + isOpenError(false), + precisionPolicy(QSql::LowPrecisionDouble), + dbmsType(UnknownDB) + { } + + uint isOpen; + uint isOpenError; + QSqlError error; + QSql::NumericalPrecisionPolicy precisionPolicy; + DBMSType dbmsType; +}; + +QT_END_NAMESPACE + +#endif // QSQLDRIVER_P_H diff --git a/src/qsqlite/src/qsql_sqlite.cpp b/src/qsqlite/src/qsql_sqlite.cpp new file mode 100644 index 0000000..06301cf --- /dev/null +++ b/src/qsqlite/src/qsql_sqlite.cpp @@ -0,0 +1,827 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtSql module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial Usage +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qsql_sqlite.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "QtSql/private/qsqldriver_p.h" +#include "QtSql/private/qsqlcachedresult_p.h" + +#if defined Q_OS_WIN +# include +#else +# include +#endif + +#include + +#include +#include "sqlite_blocking.h" + +Q_DECLARE_OPAQUE_POINTER(sqlite3*) +Q_DECLARE_OPAQUE_POINTER(sqlite3_stmt*) + +Q_DECLARE_METATYPE(sqlite3*) +Q_DECLARE_METATYPE(sqlite3_stmt*) + +QT_BEGIN_NAMESPACE + +static QString _q_escapeIdentifier(const QString &identifier) +{ + QString res = identifier; + if(!identifier.isEmpty() && identifier.left(1) != QString(QLatin1Char('"')) && identifier.right(1) != QString(QLatin1Char('"')) ) { + res.replace(QLatin1Char('"'), QStringLiteral("\"\"")); + res.prepend(QLatin1Char('"')).append(QLatin1Char('"')); + res.replace(QLatin1Char('.'), QStringLiteral("\".\"")); + } + return res; +} + +static QVariant::Type qGetColumnType(const QString &tpName) +{ + const QString typeName = tpName.toLower(); + + if (typeName == QLatin1String("integer") + || typeName == QLatin1String("int")) + return QVariant::Int; + if (typeName == QLatin1String("double") + || typeName == QLatin1String("float") + || typeName == QLatin1String("real") + || typeName.startsWith(QLatin1String("numeric"))) + return QVariant::Double; + if (typeName == QLatin1String("blob")) + return QVariant::ByteArray; + if (typeName == QLatin1String("boolean") + || typeName == QLatin1String("bool")) + return QVariant::Bool; + return QVariant::String; +} + +static QSqlError qMakeError(sqlite3 *access, const QString &descr, QSqlError::ErrorType type, + int errorCode = -1) +{ + return QSqlError(descr, + QString(reinterpret_cast(sqlite3_errmsg16(access))), + type, errorCode); +} + +class QSQLiteResultPrivate; + +class QSQLiteResult : public QSqlCachedResult +{ + friend class QSQLiteDriver; + friend class QSQLiteResultPrivate; +public: + explicit QSQLiteResult(const QSQLiteDriver* db); + ~QSQLiteResult(); + QVariant handle() const; + +protected: + bool gotoNext(QSqlCachedResult::ValueCache& row, int idx); + bool reset(const QString &query); + bool prepare(const QString &query); + bool exec(); + int size(); + int numRowsAffected(); + QVariant lastInsertId() const; + QSqlRecord record() const; + void detachFromResultSet(); + void virtual_hook(int id, void *data); + +private: + QSQLiteResultPrivate* d; +}; + + +class QSQLiteDriverPrivate : public QSqlDriverPrivate +{ +public: + inline QSQLiteDriverPrivate() : access(0) { + dbmsType = SQLite; + } + sqlite3 *access; + QList results; +}; + + +class QSQLiteResultPrivate +{ +public: + QSQLiteResultPrivate(QSQLiteResult *res); + void cleanup(); + bool fetchNext(QSqlCachedResult::ValueCache &values, int idx, bool initialFetch); + // initializes the recordInfo and the cache + void initColumns(bool emptyResultset); + void finalize(); + + QSQLiteResult* q; + sqlite3 *access; + + sqlite3_stmt *stmt; + + bool skippedStatus; // the status of the fetchNext() that's skipped + bool skipRow; // skip the next fetchNext()? + QSqlRecord rInf; + QVector firstRow; +}; + +QSQLiteResultPrivate::QSQLiteResultPrivate(QSQLiteResult* res) : q(res), access(0), + stmt(0), skippedStatus(false), skipRow(false) +{ +} + +void QSQLiteResultPrivate::cleanup() +{ + finalize(); + rInf.clear(); + skippedStatus = false; + skipRow = false; + q->setAt(QSql::BeforeFirstRow); + q->setActive(false); + q->cleanup(); +} + +void QSQLiteResultPrivate::finalize() +{ + if (!stmt) + return; + + sqlite3_finalize(stmt); + stmt = 0; +} + +void QSQLiteResultPrivate::initColumns(bool emptyResultset) +{ + int nCols = sqlite3_column_count(stmt); + if (nCols <= 0) + return; + + q->init(nCols); + + for (int i = 0; i < nCols; ++i) { + QString colName = QString::fromUtf16( + static_cast(sqlite3_column_name16(stmt, i)) + ).remove(QLatin1Char('"')); + + // must use typeName for resolving the type to match QSqliteDriver::record + QString typeName = QString::fromUtf16( + static_cast(sqlite3_column_decltype16(stmt, i))); + + // sqlite3_column_type is documented to have undefined behavior if the result set is empty + int stp = emptyResultset ? -1 : sqlite3_column_type(stmt, i); + + QVariant::Type fieldType; + + if (typeName.isEmpty()) { + fieldType = qGetColumnType(typeName); + } else { + // Get the proper type for the field based on stp value + switch (stp) { + case SQLITE_INTEGER: + fieldType = QVariant::Int; + break; + case SQLITE_FLOAT: + fieldType = QVariant::Double; + break; + case SQLITE_BLOB: + fieldType = QVariant::ByteArray; + break; + case SQLITE_TEXT: + fieldType = QVariant::String; + break; + case SQLITE_NULL: + default: + fieldType = QVariant::Invalid; + break; + } + } + + QSqlField fld(colName, fieldType); + fld.setSqlType(stp); + rInf.append(fld); + } +} + +bool QSQLiteResultPrivate::fetchNext(QSqlCachedResult::ValueCache &values, int idx, bool initialFetch) +{ + int res; + int i; + + if (skipRow) { + // already fetched + Q_ASSERT(!initialFetch); + skipRow = false; + for(int i=0;isetLastError(QSqlError(QCoreApplication::translate("QSQLiteResult", "Unable to fetch row"), + QCoreApplication::translate("QSQLiteResult", "No query"), QSqlError::ConnectionError)); + q->setAt(QSql::AfterLastRow); + return false; + } + res = sqlite3_blocking_step(stmt); + + switch(res) { + case SQLITE_ROW: + // check to see if should fill out columns + if (rInf.isEmpty()) + // must be first call. + initColumns(false); + if (idx < 0 && !initialFetch) + return true; + for (i = 0; i < rInf.count(); ++i) { + switch (sqlite3_column_type(stmt, i)) { + case SQLITE_BLOB: + values[i + idx] = QByteArray(static_cast( + sqlite3_column_blob(stmt, i)), + sqlite3_column_bytes(stmt, i)); + break; + case SQLITE_INTEGER: + values[i + idx] = sqlite3_column_int64(stmt, i); + break; + case SQLITE_FLOAT: + switch(q->numericalPrecisionPolicy()) { + case QSql::LowPrecisionInt32: + values[i + idx] = sqlite3_column_int(stmt, i); + break; + case QSql::LowPrecisionInt64: + values[i + idx] = sqlite3_column_int64(stmt, i); + break; + case QSql::LowPrecisionDouble: + case QSql::HighPrecision: + default: + values[i + idx] = sqlite3_column_double(stmt, i); + break; + }; + break; + case SQLITE_NULL: + values[i + idx] = QVariant(QVariant::String); + break; + default: + values[i + idx] = QString(reinterpret_cast( + sqlite3_column_text16(stmt, i)), + sqlite3_column_bytes16(stmt, i) / sizeof(QChar)); + break; + } + } + return true; + case SQLITE_DONE: + if (rInf.isEmpty()) + // must be first call. + initColumns(true); + q->setAt(QSql::AfterLastRow); + sqlite3_reset(stmt); + return false; + case SQLITE_CONSTRAINT: + case SQLITE_ERROR: + // SQLITE_ERROR is a generic error code and we must call sqlite3_reset() + // to get the specific error message. + res = sqlite3_reset(stmt); + q->setLastError(qMakeError(access, QCoreApplication::translate("QSQLiteResult", + "Unable to fetch row"), QSqlError::ConnectionError, res)); + q->setAt(QSql::AfterLastRow); + return false; + case SQLITE_MISUSE: + case SQLITE_BUSY: + default: + // something wrong, don't get col info, but still return false + q->setLastError(qMakeError(access, QCoreApplication::translate("QSQLiteResult", + "Unable to fetch row"), QSqlError::ConnectionError, res)); + sqlite3_reset(stmt); + q->setAt(QSql::AfterLastRow); + return false; + } + return false; +} + +QSQLiteResult::QSQLiteResult(const QSQLiteDriver* db) + : QSqlCachedResult(db) +{ + d = new QSQLiteResultPrivate(this); + d->access = db->d_func()->access; + const_cast(db->d_func())->results.append(this); +} + +QSQLiteResult::~QSQLiteResult() +{ + const QSqlDriver *sqlDriver = driver(); + if (sqlDriver) + const_cast(qobject_cast(sqlDriver)->d_func())->results.removeOne(this); + d->cleanup(); + delete d; +} + +void QSQLiteResult::virtual_hook(int id, void *data) +{ + QSqlCachedResult::virtual_hook(id, data); +} + +bool QSQLiteResult::reset(const QString &query) +{ + if (!prepare(query)) + return false; + return exec(); +} + +bool QSQLiteResult::prepare(const QString &query) +{ + if (!driver() || !driver()->isOpen() || driver()->isOpenError()) + return false; + + d->cleanup(); + + setSelect(false); + + const void *pzTail = NULL; + +#if (SQLITE_VERSION_NUMBER >= 3003011) +// int res = sqlite3_prepare16_v2(d->access, query.constData(), (query.size() + 1) * sizeof(QChar), +// &d->stmt, 0); + int res = sqlite3_blocking_prepare16_v2(d->access, query.constData(), (query.size() + 1) * sizeof(QChar), + &d->stmt, &pzTail); +#else + int res = sqlite3_prepare16(d->access, query.constData(), (query.size() + 1) * sizeof(QChar), + &d->stmt, &pzTail); +#endif + + if (res != SQLITE_OK) { + setLastError(qMakeError(d->access, QCoreApplication::translate("QSQLiteResult", + "Unable to execute statement"), QSqlError::StatementError, res)); + d->finalize(); + return false; + } else if (pzTail && !QString(reinterpret_cast(pzTail)).trimmed().isEmpty()) { + setLastError(qMakeError(d->access, QCoreApplication::translate("QSQLiteResult", + "Unable to execute multiple statements at a time"), QSqlError::StatementError, SQLITE_MISUSE)); + d->finalize(); + return false; + } + return true; +} + +bool QSQLiteResult::exec() +{ + const QVector values = boundValues(); + + d->skippedStatus = false; + d->skipRow = false; + d->rInf.clear(); + clearValues(); + setLastError(QSqlError()); + + int res = sqlite3_reset(d->stmt); + if (res != SQLITE_OK) { + setLastError(qMakeError(d->access, QCoreApplication::translate("QSQLiteResult", + "Unable to reset statement"), QSqlError::StatementError, res)); + d->finalize(); + return false; + } + int paramCount = sqlite3_bind_parameter_count(d->stmt); + if (paramCount == values.count()) { + for (int i = 0; i < paramCount; ++i) { + res = SQLITE_OK; + const QVariant value = values.at(i); + + if (value.isNull()) { + res = sqlite3_bind_null(d->stmt, i + 1); + } else { + switch (value.type()) { + case QVariant::ByteArray: { + const QByteArray *ba = static_cast(value.constData()); + res = sqlite3_bind_blob(d->stmt, i + 1, ba->constData(), + ba->size(), SQLITE_STATIC); + break; } + case QVariant::Int: + case QVariant::Bool: + res = sqlite3_bind_int(d->stmt, i + 1, value.toInt()); + break; + case QVariant::Double: + res = sqlite3_bind_double(d->stmt, i + 1, value.toDouble()); + break; + case QVariant::UInt: + case QVariant::LongLong: + res = sqlite3_bind_int64(d->stmt, i + 1, value.toLongLong()); + break; + case QVariant::DateTime: { + const QDateTime dateTime = value.toDateTime(); + const QString str = dateTime.toString(QStringLiteral("yyyy-MM-ddThh:mm:ss.zzz")); + res = sqlite3_bind_text16(d->stmt, i + 1, str.utf16(), + str.size() * sizeof(ushort), SQLITE_TRANSIENT); + break; + } + case QVariant::Time: { + const QTime time = value.toTime(); + const QString str = time.toString(QStringLiteral("hh:mm:ss.zzz")); + res = sqlite3_bind_text16(d->stmt, i + 1, str.utf16(), + str.size() * sizeof(ushort), SQLITE_TRANSIENT); + break; + } + case QVariant::String: { + // lifetime of string == lifetime of its qvariant + const QString *str = static_cast(value.constData()); + res = sqlite3_bind_text16(d->stmt, i + 1, str->utf16(), + (str->size()) * sizeof(QChar), SQLITE_STATIC); + break; } + default: { + QString str = value.toString(); + // SQLITE_TRANSIENT makes sure that sqlite buffers the data + res = sqlite3_bind_text16(d->stmt, i + 1, str.utf16(), + (str.size()) * sizeof(QChar), SQLITE_TRANSIENT); + break; } + } + } + if (res != SQLITE_OK) { + setLastError(qMakeError(d->access, QCoreApplication::translate("QSQLiteResult", + "Unable to bind parameters"), QSqlError::StatementError, res)); + d->finalize(); + return false; + } + } + } else { + setLastError(QSqlError(QCoreApplication::translate("QSQLiteResult", + "Parameter count mismatch"), QString(), QSqlError::StatementError)); + return false; + } + d->skippedStatus = d->fetchNext(d->firstRow, 0, true); + if (lastError().isValid()) { + setSelect(false); + setActive(false); + return false; + } + setSelect(!d->rInf.isEmpty()); + setActive(true); + return true; +} + +bool QSQLiteResult::gotoNext(QSqlCachedResult::ValueCache& row, int idx) +{ + return d->fetchNext(row, idx, false); +} + +int QSQLiteResult::size() +{ + return -1; +} + +int QSQLiteResult::numRowsAffected() +{ + return sqlite3_changes(d->access); +} + +QVariant QSQLiteResult::lastInsertId() const +{ + if (isActive()) { + qint64 id = sqlite3_last_insert_rowid(d->access); + if (id) + return id; + } + return QVariant(); +} + +QSqlRecord QSQLiteResult::record() const +{ + if (!isActive() || !isSelect()) + return QSqlRecord(); + return d->rInf; +} + +void QSQLiteResult::detachFromResultSet() +{ + if (d->stmt) + sqlite3_reset(d->stmt); +} + +QVariant QSQLiteResult::handle() const +{ + return qVariantFromValue(d->stmt); +} + +///////////////////////////////////////////////////////// + +QSQLiteDriver::QSQLiteDriver(QObject * parent) + : QSqlDriver(*new QSQLiteDriverPrivate, parent) +{ +} + +QSQLiteDriver::QSQLiteDriver(sqlite3 *connection, QObject *parent) + : QSqlDriver(*new QSQLiteDriverPrivate, parent) +{ + Q_D(QSQLiteDriver); + + d->access = connection; + setOpen(true); + setOpenError(false); +} + + +QSQLiteDriver::~QSQLiteDriver() +{ +} + +bool QSQLiteDriver::hasFeature(DriverFeature f) const +{ + switch (f) { + case BLOB: + case Transactions: + case Unicode: + case LastInsertId: + case PreparedQueries: + case PositionalPlaceholders: + case SimpleLocking: + case FinishQuery: + case LowPrecisionNumbers: + return true; + case QuerySize: + case NamedPlaceholders: + case BatchOperations: + case EventNotifications: + case MultipleResultSets: + case CancelQuery: + return false; + } + return false; +} + +/* + SQLite dbs have no user name, passwords, hosts or ports. + just file names. +*/ +bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, const QString &, int, const QString &conOpts) +{ + Q_D(QSQLiteDriver); + + if (isOpen()) + close(); + + int timeout = 5000; + bool sharedCache = false; + bool openReadOnlyOption = false; + bool openUriOption = false; + + const QStringList opts = QString(conOpts).remove(QLatin1Char(' ' )).split(QLatin1Char(';')); + Q_FOREACH (const QString &option, opts) { + if (option.startsWith(QStringLiteral("QSQLITE_BUSY_TIMEOUT="))) { + bool ok; + const int nt = option.midRef(21).toInt(&ok); + if (ok) + timeout = nt; + } else if (option == QLatin1String("QSQLITE_OPEN_READONLY")) { + openReadOnlyOption = true; + } else if (option == QLatin1String("QSQLITE_OPEN_URI")) { + openUriOption = true; + } else if (option == QLatin1String("QSQLITE_ENABLE_SHARED_CACHE")) { + sharedCache = true; + } + } + + int openMode = (openReadOnlyOption ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)); + if (openUriOption) + openMode |= SQLITE_OPEN_URI; + + sqlite3_enable_shared_cache(sharedCache); + + if (sqlite3_open_v2(db.toUtf8().constData(), &d->access, openMode, NULL) == SQLITE_OK) { + sqlite3_busy_timeout(d->access, timeout); + sqlite3_extended_result_codes(d->access, 1); + setOpen(true); + setOpenError(false); + return true; + } else { + if (d->access) { + sqlite3_close(d->access); + d->access = 0; + } + + setLastError(qMakeError(d->access, tr("Error opening database"), + QSqlError::ConnectionError)); + setOpenError(true); + return false; + } +} + +void QSQLiteDriver::close() +{ + Q_D(QSQLiteDriver); + + if (isOpen()) { + Q_FOREACH (QSQLiteResult *result, d->results) { + result->d->finalize(); + } + + if (sqlite3_close(d->access) != SQLITE_OK) + setLastError(qMakeError(d->access, tr("Error closing database"), + QSqlError::ConnectionError)); + d->access = 0; + setOpen(false); + setOpenError(false); + } +} + +QSqlResult *QSQLiteDriver::createResult() const +{ + return new QSQLiteResult(this); +} + +bool QSQLiteDriver::beginTransaction() +{ + if (!isOpen() || isOpenError()) + return false; + + QSqlQuery q(createResult()); + if (!q.exec(QStringLiteral("BEGIN"))) { + setLastError(QSqlError(tr("Unable to begin transaction"), + q.lastError().databaseText(), QSqlError::TransactionError)); + return false; + } + + return true; +} + +bool QSQLiteDriver::commitTransaction() +{ + if (!isOpen() || isOpenError()) + return false; + + QSqlQuery q(createResult()); + if (!q.exec(QStringLiteral("COMMIT"))) { + setLastError(QSqlError(tr("Unable to commit transaction"), + q.lastError().databaseText(), QSqlError::TransactionError)); + return false; + } + + return true; +} + +bool QSQLiteDriver::rollbackTransaction() +{ + if (!isOpen() || isOpenError()) + return false; + + QSqlQuery q(createResult()); + if (!q.exec(QStringLiteral("ROLLBACK"))) { + setLastError(QSqlError(tr("Unable to rollback transaction"), + q.lastError().databaseText(), QSqlError::TransactionError)); + return false; + } + + return true; +} + +QStringList QSQLiteDriver::tables(QSql::TableType type) const +{ + QStringList res; + if (!isOpen()) + return res; + + QSqlQuery q(createResult()); + q.setForwardOnly(true); + + QString sql = QStringLiteral("SELECT name FROM sqlite_master WHERE %1 " + "UNION ALL SELECT name FROM sqlite_temp_master WHERE %1"); + if ((type & QSql::Tables) && (type & QSql::Views)) + sql = sql.arg(QStringLiteral("type='table' OR type='view'")); + else if (type & QSql::Tables) + sql = sql.arg(QStringLiteral("type='table'")); + else if (type & QSql::Views) + sql = sql.arg(QStringLiteral("type='view'")); + else + sql.clear(); + + if (!sql.isEmpty() && q.exec(sql)) { + while(q.next()) + res.append(q.value(0).toString()); + } + + if (type & QSql::SystemTables) { + // there are no internal tables beside this one: + res.append(QStringLiteral("sqlite_master")); + } + + return res; +} + +static QSqlIndex qGetTableInfo(QSqlQuery &q, const QString &tableName, bool onlyPIndex = false) +{ + QString schema; + QString table(tableName); + int indexOfSeparator = tableName.indexOf(QLatin1Char('.')); + if (indexOfSeparator > -1) { + schema = tableName.left(indexOfSeparator).append(QLatin1Char('.')); + table = tableName.mid(indexOfSeparator + 1); + } + q.exec(QStringLiteral("PRAGMA ") + schema + QStringLiteral("table_info (") + _q_escapeIdentifier(table) + QLatin1Char(')')); + + QSqlIndex ind; + while (q.next()) { + bool isPk = q.value(5).toInt(); + if (onlyPIndex && !isPk) + continue; + QString typeName = q.value(2).toString().toLower(); + QSqlField fld(q.value(1).toString(), qGetColumnType(typeName)); + if (isPk && (typeName == QLatin1String("integer"))) + // INTEGER PRIMARY KEY fields are auto-generated in sqlite + // INT PRIMARY KEY is not the same as INTEGER PRIMARY KEY! + fld.setAutoValue(true); + fld.setRequired(q.value(3).toInt() != 0); + fld.setDefaultValue(q.value(4)); + ind.append(fld); + } + return ind; +} + +QSqlIndex QSQLiteDriver::primaryIndex(const QString &tblname) const +{ + if (!isOpen()) + return QSqlIndex(); + + QString table = tblname; + if (isIdentifierEscaped(table, QSqlDriver::TableName)) + table = stripDelimiters(table, QSqlDriver::TableName); + + QSqlQuery q(createResult()); + q.setForwardOnly(true); + return qGetTableInfo(q, table, true); +} + +QSqlRecord QSQLiteDriver::record(const QString &tbl) const +{ + if (!isOpen()) + return QSqlRecord(); + + QString table = tbl; + if (isIdentifierEscaped(table, QSqlDriver::TableName)) + table = stripDelimiters(table, QSqlDriver::TableName); + + QSqlQuery q(createResult()); + q.setForwardOnly(true); + return qGetTableInfo(q, table); +} + +QVariant QSQLiteDriver::handle() const +{ + Q_D(const QSQLiteDriver); + return QVariant::fromValue(d->access); +} + +QString QSQLiteDriver::escapeIdentifier(const QString &identifier, IdentifierType type) const +{ + Q_UNUSED(type); + return _q_escapeIdentifier(identifier); +} + +QT_END_NAMESPACE diff --git a/src/qsqlite/src/qsql_sqlite.h b/src/qsqlite/src/qsql_sqlite.h new file mode 100644 index 0000000..37fd8bf --- /dev/null +++ b/src/qsqlite/src/qsql_sqlite.h @@ -0,0 +1,99 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtSql module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial Usage +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://www.qtsoftware.com/contact. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSQL_SQLITE_H +#define QSQL_SQLITE_H + +#include +#include +#include + +struct sqlite3; + +#ifdef QT_PLUGIN +#define Q_EXPORT_SQLDRIVER_SQLITE +#else +#define Q_EXPORT_SQLDRIVER_SQLITE Q_SQL_EXPORT +#endif + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE +class QSQLiteDriverPrivate; +class QSQLiteResultPrivate; +class QSQLiteDriver; + +class Q_EXPORT_SQLDRIVER_SQLITE QSQLiteDriver : public QSqlDriver +{ + Q_OBJECT + friend class QSQLiteResult; +public: + explicit QSQLiteDriver(QObject *parent = 0); + explicit QSQLiteDriver(sqlite3 *connection, QObject *parent = 0); + ~QSQLiteDriver(); + bool hasFeature(DriverFeature f) const; + bool open(const QString & db, + const QString & user, + const QString & password, + const QString & host, + int port, + const QString & connOpts); + void close(); + QSqlResult *createResult() const; + bool beginTransaction(); + bool commitTransaction(); + bool rollbackTransaction(); + QStringList tables(QSql::TableType) const; + + QSqlRecord record(const QString& tablename) const; + QSqlIndex primaryIndex(const QString &table) const; + QVariant handle() const; + QString escapeIdentifier(const QString &identifier, IdentifierType) const; + +private: + Q_DECLARE_PRIVATE(QSQLiteDriver) +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QSQL_SQLITE_H diff --git a/src/qsqlite/src/smain.cpp b/src/qsqlite/src/smain.cpp new file mode 100644 index 0000000..668d2fb --- /dev/null +++ b/src/qsqlite/src/smain.cpp @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial Usage +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://www.qtsoftware.com/contact. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include + +#include "qsql_sqlite.h" + +QT_BEGIN_NAMESPACE + +class QSQLiteDriverPlugin : public QSqlDriverPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QSqlDriverFactoryInterface" FILE "sqlite3.json") + +public: + QSQLiteDriverPlugin(); + + QSqlDriver* create(const QString &); +}; + +QSQLiteDriverPlugin::QSQLiteDriverPlugin() + : QSqlDriverPlugin() +{ +} + +QSqlDriver* QSQLiteDriverPlugin::create(const QString &name) +{ + if (name == QLatin1String("QSQLITE3")) { + QSQLiteDriver* driver = new QSQLiteDriver(); + return driver; + } + return 0; +} + +#include "smain.moc" + +QT_END_NAMESPACE diff --git a/src/qsqlite/src/sqlite3.json b/src/qsqlite/src/sqlite3.json new file mode 100644 index 0000000..b8fdc6b --- /dev/null +++ b/src/qsqlite/src/sqlite3.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "QSQLITE3" ] +} diff --git a/src/qsqlite/src/sqlite_blocking.cpp b/src/qsqlite/src/sqlite_blocking.cpp new file mode 100644 index 0000000..fa42360 --- /dev/null +++ b/src/qsqlite/src/sqlite_blocking.cpp @@ -0,0 +1,106 @@ +/* + Copyright (c) 2009 Bertjan Broeksema + Copyright (c) 2014 Daniel Vrátil + + 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 "sqlite_blocking.h" + +#include + +#include +#include +#include "qdebug.h" +#include "qstringbuilder.h" +#include "qthread.h" +#include + +QString debugString() +{ + return QString( QLatin1Literal("[QSQLITE3: ") + QString::number( quint64( QThread::currentThreadId() ) ) + QLatin1Literal("] ") ); +} + +/* Based on example in http://www.sqlite.org/unlock_notify.html */ + +struct UnlockNotification { + bool fired; + QWaitCondition cond; + QMutex mutex; +}; + +static void qSqlite3UnlockNotifyCb(void **apArg, int nArg) +{ + for (int i = 0; i < nArg; ++i) { + UnlockNotification *ntf = static_cast(apArg[i]); + ntf->mutex.lock(); + ntf->fired = true; + ntf->cond.wakeOne(); + ntf->mutex.unlock(); + } +} + +static int qSqlite3WaitForUnlockNotify(sqlite3 *db) +{ + int rc; + UnlockNotification un; + un.fired = false; + + rc = sqlite3_unlock_notify(db, qSqlite3UnlockNotifyCb, (void *)&un); + Q_ASSERT(rc == SQLITE_LOCKED || rc == SQLITE_OK); + + if (rc == SQLITE_OK) { + un.mutex.lock(); + if (!un.fired) { + un.cond.wait(&un.mutex); + } + un.mutex.unlock(); + } + + return rc; +} + +int sqlite3_blocking_step(sqlite3_stmt *pStmt) +{ + int rc; + while (SQLITE_LOCKED_SHAREDCACHE == (rc = sqlite3_step(pStmt))) { + //qDebug() << debugString() << "sqlite3_blocking_step: Waiting..."; QTime now; now.start(); + rc = qSqlite3WaitForUnlockNotify(sqlite3_db_handle(pStmt)); + //qDebug() << debugString() << "sqlite3_blocking_step: Waited for " << now.elapsed() << "ms"; + if (rc != SQLITE_OK) { + break; + } + sqlite3_reset(pStmt); + } + + return rc; +} + +int sqlite3_blocking_prepare16_v2(sqlite3 *db, const void *zSql, int nSql, + sqlite3_stmt **ppStmt, const void **pzTail) +{ + int rc; + while (SQLITE_LOCKED_SHAREDCACHE == (rc = sqlite3_prepare16_v2(db, zSql, nSql, ppStmt, pzTail))) { + //qDebug() << debugString() << "sqlite3_blocking_prepare16_v2: Waiting..."; QTime now; now.start(); + rc = qSqlite3WaitForUnlockNotify(db); + //qDebug() << debugString() << "sqlite3_blocking_prepare16_v2: Waited for " << now.elapsed() << "ms"; + if (rc != SQLITE_OK) { + break; + } + } + + return rc; +} diff --git a/src/qsqlite/src/sqlite_blocking.h b/src/qsqlite/src/sqlite_blocking.h new file mode 100644 index 0000000..9f13946 --- /dev/null +++ b/src/qsqlite/src/sqlite_blocking.h @@ -0,0 +1,39 @@ +/* + Copyright (c) 2009 Bertjan Broeksema + + 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 SQLITE_BLOCKING_H +#define SQLITE_BLOCKING_H + +#include +#include + +QString debugString(); + +struct sqlite3; +struct sqlite3_stmt; + +int sqlite3_blocking_prepare16_v2( sqlite3 *db, /* Database handle. */ + const void *zSql, /* SQL statement, UTF-16 encoded */ + int nSql, /* Length of zSql in bytes. */ + sqlite3_stmt **ppStmt, /* OUT: A pointer to the prepared statement */ + const void **pzTail /* OUT: Pointer to unused portion of zSql */ ); + +int sqlite3_blocking_step(sqlite3_stmt *pStmt); + +#endif // SQLITE_BLOCKING_H diff --git a/src/rds/CMakeLists.txt b/src/rds/CMakeLists.txt new file mode 100644 index 0000000..6fc1bdd --- /dev/null +++ b/src/rds/CMakeLists.txt @@ -0,0 +1,21 @@ +########### next target ############### + +set(akonadi_rds_srcs + bridgeserver.cpp + bridgeconnection.cpp + main.cpp +) + +add_executable(akonadi_rds ${akonadi_rds_srcs}) + +target_link_libraries(akonadi_rds + akonadi_shared + KF5AkonadiPrivate + Qt5::Core + Qt5::Network +) + +install(TARGETS akonadi_rds + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + diff --git a/src/rds/bridgeconnection.cpp b/src/rds/bridgeconnection.cpp new file mode 100644 index 0000000..017e091 --- /dev/null +++ b/src/rds/bridgeconnection.cpp @@ -0,0 +1,121 @@ +/*************************************************************************** + * Copyright (C) 2010 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "bridgeconnection.h" + +#include + +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_UNIX +#include +#include +#endif + +BridgeConnection::BridgeConnection(QTcpSocket *remoteSocket, QObject *parent) + : QObject(parent) + , m_localSocket(0) + , m_remoteSocket(remoteSocket) +{ + // wait for the vtable to be complete + QMetaObject::invokeMethod(this, "doConnects", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "connectLocal", Qt::QueuedConnection); +} + +BridgeConnection::~BridgeConnection() +{ + delete m_remoteSocket; +} + +void BridgeConnection::slotDataAvailable() +{ + if (m_localSocket->bytesAvailable() > 0) { + m_remoteSocket->write(m_localSocket->read(m_localSocket->bytesAvailable())); + } + if (m_remoteSocket->bytesAvailable() > 0) { + m_localSocket->write(m_remoteSocket->read(m_remoteSocket->bytesAvailable())); + } +} + +AkonadiBridgeConnection::AkonadiBridgeConnection(QTcpSocket *remoteSocket, QObject *parent) + : BridgeConnection(remoteSocket, parent) +{ + m_localSocket = new QLocalSocket(this); +} + +void AkonadiBridgeConnection::connectLocal() +{ + const QSettings connectionSettings(Akonadi::StandardDirs::connectionConfigFile(), QSettings::IniFormat); +#ifdef Q_OS_WIN //krazy:exclude=cpp + const QString namedPipe = connectionSettings.value(QLatin1String("Data/NamedPipe"), QLatin1String("Akonadi")).toString(); + (static_cast(m_localSocket))->connectToServer(namedPipe); +#else + const QString defaultSocketDir = Akonadi::StandardDirs::saveDir("data"); + const QString path = connectionSettings.value(QStringLiteral("Data/UnixPath"), QString(defaultSocketDir + QLatin1String("/akonadiserver.socket"))).toString(); + (static_cast(m_localSocket))->connectToServer(path); +#endif +} + +DBusBridgeConnection::DBusBridgeConnection(QTcpSocket *remoteSocket, QObject *parent) + : BridgeConnection(remoteSocket, parent) +{ + m_localSocket = new QLocalSocket(this); +} + +void DBusBridgeConnection::connectLocal() +{ + // TODO: support for !Linux +#ifdef Q_OS_UNIX + const QByteArray sessionBusAddress = qgetenv("DBUS_SESSION_BUS_ADDRESS"); + QRegExp rx(QStringLiteral("=(.*)[,$]")); + if (rx.indexIn(QString::fromLatin1(sessionBusAddress)) >= 0) { + const QString dbusPath = rx.cap(1); + qDebug() << dbusPath; + if (sessionBusAddress.contains("abstract")) { + const int fd = socket(PF_UNIX, SOCK_STREAM, 0); + Q_ASSERT(fd >= 0); + struct sockaddr_un dbus_socket_addr; + dbus_socket_addr.sun_family = PF_UNIX; + dbus_socket_addr.sun_path[0] = '\0'; // this marks an abstract unix socket on linux, something QLocalSocket doesn't support + memcpy(dbus_socket_addr.sun_path + 1, dbusPath.toLatin1().data(), dbusPath.toLatin1().size() + 1); + /*sizeof(dbus_socket_addr) gives me a too large value for some reason, although that's what QLocalSocket uses*/ + const int result = ::connect(fd, (struct sockaddr *) &dbus_socket_addr, sizeof (dbus_socket_addr.sun_family) + dbusPath.size() + 1 /* for the leading \0 */); + Q_ASSERT(result != -1); + Q_UNUSED(result); // in release mode + (static_cast(m_localSocket))->setSocketDescriptor(fd, QLocalSocket::ConnectedState, QLocalSocket::ReadWrite); + } else { + (static_cast(m_localSocket))->connectToServer(dbusPath); + } + } +#endif +} + +void BridgeConnection::doConnects() +{ + connect(m_localSocket, SIGNAL(disconnected()), SLOT(deleteLater())); + connect(m_remoteSocket, &QAbstractSocket::disconnected, this, &QObject::deleteLater); + connect(m_localSocket, &QIODevice::readyRead, this, &BridgeConnection::slotDataAvailable); + connect(m_remoteSocket, &QIODevice::readyRead, this, &BridgeConnection::slotDataAvailable); + connect(m_localSocket, SIGNAL(connected()), SLOT(slotDataAvailable())); +} diff --git a/src/rds/bridgeconnection.h b/src/rds/bridgeconnection.h new file mode 100644 index 0000000..b7e97a6 --- /dev/null +++ b/src/rds/bridgeconnection.h @@ -0,0 +1,72 @@ +/*************************************************************************** + * Copyright (C) 2010 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BRIDGECONNECTION_H +#define BRIDGECONNECTION_H + +#include + +class QTcpSocket; +class QIODevice; + +class BridgeConnection : public QObject +{ + Q_OBJECT + +public: + explicit BridgeConnection(QTcpSocket *remoteSocket, QObject *parent = 0); + ~BridgeConnection(); + +protected Q_SLOTS: + virtual void connectLocal() = 0; + void doConnects(); + +protected: + QIODevice *m_localSocket; + +private Q_SLOTS: + void slotDataAvailable(); + +private: + QTcpSocket *m_remoteSocket; +}; + +class AkonadiBridgeConnection : public BridgeConnection +{ + Q_OBJECT + +public: + explicit AkonadiBridgeConnection(QTcpSocket *remoteSocket, QObject *parent = 0); + +protected: + void connectLocal(); +}; + +class DBusBridgeConnection : public BridgeConnection +{ + Q_OBJECT + +public: + explicit DBusBridgeConnection(QTcpSocket *remoteSocket, QObject *parent = 0); + +protected: + void connectLocal(); +}; + +#endif // BRIDGECONNECTION_H diff --git a/src/rds/bridgeserver.cpp b/src/rds/bridgeserver.cpp new file mode 100644 index 0000000..d6ae7d8 --- /dev/null +++ b/src/rds/bridgeserver.cpp @@ -0,0 +1,33 @@ +/*************************************************************************** + * Copyright (C) 2010 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "bridgeserver.h" + +#include "exception.h" + +BridgeServerBase::BridgeServerBase(quint16 port, QObject *parent) + : QObject(parent) + , m_server(new QTcpServer(this)) +{ + connect(m_server, &QTcpServer::newConnection, this, &BridgeServerBase::slotNewConnection); + if (!m_server->listen(QHostAddress::Any, port)) { + throw Exception(tr("Can't listen to port %1: %2") + .arg(port).arg(m_server->errorString())); + } +} diff --git a/src/rds/bridgeserver.h b/src/rds/bridgeserver.h new file mode 100644 index 0000000..b16b942 --- /dev/null +++ b/src/rds/bridgeserver.h @@ -0,0 +1,58 @@ +/*************************************************************************** + * Copyright (C) 2010 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BRIDGESERVER_H +#define BRIDGESERVER_H + +#include +#include + +class BridgeServerBase : public QObject +{ + Q_OBJECT + +public: + explicit BridgeServerBase(quint16 port, QObject *parent = 0); + +protected Q_SLOTS: + virtual void slotNewConnection() = 0; + +protected: + QTcpServer *m_server; +}; + +template +class BridgeServer : public BridgeServerBase +{ +public: + explicit BridgeServer(quint16 port, QObject *parent = 0) + : BridgeServerBase(port, parent) + { + } + +protected: + void slotNewConnection() + { + while (m_server->hasPendingConnections()) { + new ConnectionType(m_server->nextPendingConnection(), this); + } + } +}; + +#endif // BRIDGESERVER_H diff --git a/src/rds/exception.h b/src/rds/exception.h new file mode 100644 index 0000000..250d196 --- /dev/null +++ b/src/rds/exception.h @@ -0,0 +1,39 @@ +/*************************************************************************** + * Copyright (C) 2010 by Marc Mutz * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef AKONADI_RDS_EXCEPTION_H +#define AKONADI_RDS_EXCEPTION_H + +#include +#include + +template +class Exception : Ex +{ +public: + explicit Exception(const QString &message) + : Ex(message.toStdString()) + { + } + + ~Exception() throw() + { + } +}; + +#endif diff --git a/src/rds/main.cpp b/src/rds/main.cpp new file mode 100644 index 0000000..cee43ed --- /dev/null +++ b/src/rds/main.cpp @@ -0,0 +1,44 @@ +/*************************************************************************** + * Copyright (C) 2010 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "bridgeserver.h" +#include "bridgeconnection.h" + +#include + +#include +#include + +int main(int argc, char **argv) +{ + AkCoreApplication app(argc, argv); + app.setDescription(QStringLiteral("Akonadi Remote Debugging Server\nUse for debugging only.")); + app.parseCommandLine(); + try { + new BridgeServer(31415); + new BridgeServer(31416); + return app.exec(); + } catch (const std::exception &e) { + qDebug("Caught exception: %s", e.what()); + return EXIT_FAILURE; + } catch (...) { + qDebug("Caught unknown exception - fix the program!"); + return EXIT_FAILURE; + } +} diff --git a/src/selftest/CMakeLists.txt b/src/selftest/CMakeLists.txt new file mode 100644 index 0000000..c13ef91 --- /dev/null +++ b/src/selftest/CMakeLists.txt @@ -0,0 +1,20 @@ +set(selftest_SRCS + main.cpp +) + +add_executable(akonadiselftest ${selftest_SRCS}) + +target_link_libraries(akonadiselftest +PRIVATE + KF5::AkonadiWidgets + KF5::AkonadiPrivate + KF5::DBusAddons + KF5::I18n + Qt5::Sql + Qt5::Widgets +) + +install(TARGETS + akonadiselftest + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) diff --git a/src/selftest/main.cpp b/src/selftest/main.cpp new file mode 100644 index 0000000..6bb07ff --- /dev/null +++ b/src/selftest/main.cpp @@ -0,0 +1,47 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "selftestdialog.h" + +#include + +#include +#include + +int main(int argc, char *argv[]) +{ + QCommandLineParser parser; + KAboutData about(QStringLiteral("akonadiselftest"), + i18n("Akonadi Self Test"), + QStringLiteral("1.0"), + i18n("Checks and reports state of Akonadi server"), + KAboutLicense::GPL_V2, + i18n("(c) 2008 Volker Krause ")); + about.setupCommandLine(&parser); + + QApplication app(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("akonadiselftest")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + parser.process(app); + + Akonadi::SelfTestDialog dlg; + dlg.show(); + + return app.exec(); +} diff --git a/src/server/AkonadiServerProtocol.txt b/src/server/AkonadiServerProtocol.txt new file mode 100644 index 0000000..1fb6ae1 --- /dev/null +++ b/src/server/AkonadiServerProtocol.txt @@ -0,0 +1,578 @@ +The Akonadi Server Protocol +============================ + +This document is the official specification of the Akonadi server protocol +in version 17. + +Table of Contents +------------------- + 1. General Information + 2. Commands + 2.1 States + 2.2 Scopes + 2.3 Command Descriptions + + +1) General Information +======================= +The protocol used for the communication between the applications and the Akonadi server +has its roots in the IMAP protocol [RFC 3501], therefor the overall command structure is +quite similar and existing IMAP libraries can be abused by extending them with the additional, +Akonadi specific commands. However in some parts, the IMAP standard has been extended or +changed to better match the requirements of Akonadi's data transport mechanisms. + +The connection to the Akonadi server is established via a UnixDomain Socket under *nix +or a NamedPipe under Windows. After the connection is up, the server initializes the protocol +by sending the greeting message + + * OK Akonadi Almost IMAP Server [PROTOCOL 17] + +that includes the number of the protocol version, which is 17 in this example. +Clients should always check that version number and avoid communication if their +minimum requirement is not met. In the next step the client can start with sending commands +to the server to continue communication. + +2) Commands +============ +The basic commands of IMAP have been reused, sometimes with slightly different semantics. +For example the LOGIN command does not take user credentials as arguments, as every user +runs its own Akonadi server and an authentication is pointless in this case. Instead a +session identifier is passed, that allowas easy management of parallel communication. + +2.1) States +------------ +Like in IMAP, the Akonadi server protocol categorizes the allowed commands into 3 states: + - Always + - UnAuthenticated + - Authenticated + +Commands from the 'Always' category can be send to the server at any time, independent of +any other state information. Examples are the NOOP command, that does nothing then keeping +the connection alive. Commands from the 'UnAuthenticated' category can only be executed +if the connection is in the UnAuthenticated state. That's the case after the connection +has been initialized or after the command LOGOUT has been executed. In this state +the command LOGIN can be used to switch into the third state Authenticated, in which most +of the other commands can be executed. + +2.2) Scopes +------------ +In opposite to the IMAP protocol, the Akonadi server protocol supports so called scopes. +That are status information that influence how the parameter of commands are interpreted +by the server. Scope identifiers can be prepended to the command strings and are valid for +a single command call. Available scopes are + - Empty scope + - Uid scope + - Rid scope + +Uid scope means that identifiers that are passed as arguments to a command are interpreted +as the unique identifier that every item and collection inside Akonadi has. The Rid scope +means that the passed identifier is the remote identifier of the item or collection. +The different behaviour will be explained in detail in the descriptions of the single commands. + +2.3) Commands +-------------- +To describe the commands extensivly we introduce an abstract description of the following form: + +DESCRIPTION: A short textual description of what the commands is supposed to do + COMMAND: The literal command string that is send to the server + STATES: A list of states in which the commands can be used, possible values are: Always, UnAuthenticated, Authenticated + SCOPES: A list of scopes that can be passed together with the command + ARGUMENTS: A formal description of the arguments that can be passed with the command + EXAMPLES: Some example command calls that shall improve the understanding + RESPONSES: Possible responses of the example calls + DETAILS: Further descriptions of the command + +2.3.X) The LOGIN command +------------------------ +DESCRIPTION: + + COMMAND: LOGIN + + STATES: UnAuthenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The LOGOUT command +------------------------ +DESCRIPTION: + + COMMAND: LOGOUT + + STATES: Always + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The CAPABILITY command +------------------------ +DESCRIPTION: + + COMMAND: CAPABILITY + + STATES: Always + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The SELECT command +-------------------------- +DESCRIPTION: + + COMMAND: SELECT + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + +2.3.X) The LIST command +---------------------------- +DESCRIPTION: + + COMMAND: LIST + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The LSUB command +---------------------------- +DESCRIPTION: + + COMMAND: LSUB + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The SEARCH_STORE command +-------------------------------- +DESCRIPTION: + + COMMAND: SEARCH_STORE + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The STATUS command +-------------------------- +DESCRIPTION: + + COMMAND: STATUS + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The BEGIN command +------------------------- +DESCRIPTION: + + COMMAND: BEGIN + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The ROLLBACK command +---------------------------- +DESCRIPTION: + + COMMAND: ROLLBACK + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The COMMIT command +-------------------------- +DESCRIPTION: + + COMMAND: COMMIT + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The SUBSCRIBE command +----------------------------- +DESCRIPTION: + + COMMAND: SUBSCRIBE + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The UNSUBSCRIBE command +------------------------------- +DESCRIPTION: + + COMMAND: UNSUBSCRIBE + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The LINK command +------------------------ +DESCRIPTION: + + COMMAND: LINK + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The UNLINK command +-------------------------- +DESCRIPTION: + + COMMAND: UNLINK + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The COLCOPY command +-------------------------- +DESCRIPTION: Copies a collection in the storage + + COMMAND: COLCOPY + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The CREATE command +-------------------------- +DESCRIPTION: Creates a new collection in the storage + + COMMAND: CREATE + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The DELETE command +-------------------------- +DESCRIPTION: Deletes a collection in the storage + + COMMAND: DELETE + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The MODIFY command +-------------------------- +DESCRIPTION: Modifies the properties of a collection in the storage + + COMMAND: MODIFY + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The COLMOVE command +-------------------------- +DESCRIPTION: Moves a collection in the storage + + COMMAND: COLMOVE + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + + +2.3.X) The COPY command +-------------------------- +DESCRIPTION: Copies an item in the storage + + COMMAND: COPY + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The X-AKAPPEND command +-------------------------- +DESCRIPTION: Creates a new item in the storage + + COMMAND: X-AKAPPEND + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The REMOVE command +-------------------------- +DESCRIPTION: Deletes an item from the storage + + COMMAND: REMOVE + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The FETCH command +-------------------------- +DESCRIPTION: Fetches the data of an item from the storage + + COMMAND: FETCH + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The STORE command +-------------------------- +DESCRIPTION: Modifies the properties of an item in the storage + + COMMAND: STORE + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + + +2.3.X) The MOVE command +-------------------------- +DESCRIPTION: Moves an item in the storage + + COMMAND: MOVE + + STATES: Authenticated + + SCOPES: + + ARGUMENTS: + + EXAMPLES: + + RESPONSES: + + DETAILS: + +#define AKONADI_CMD_RESOURCESELECT "RESSELECT" diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt new file mode 100644 index 0000000..3d5841b --- /dev/null +++ b/src/server/CMakeLists.txt @@ -0,0 +1,194 @@ +include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}) +include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) + +if(MYSQLD_EXECUTABLE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMYSQLD_EXECUTABLE=\"\\\"${MYSQLD_EXECUTABLE}\\\"\"") +endif() + +if(POSTGRES_PATH) + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DPOSTGRES_PATH=\"\\\"${POSTGRES_PATH}\\\"\"") +endif() + +########### next target ############### + +set(AKONADI_DB_SCHEME ${CMAKE_CURRENT_SOURCE_DIR}/storage/akonadidb.xml) + +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/entities.h + ${CMAKE_CURRENT_BINARY_DIR}/entities.cpp + COMMAND ${XSLTPROC_EXECUTABLE} + --output ${CMAKE_CURRENT_BINARY_DIR}/entities.h + --stringparam code header + ${CMAKE_CURRENT_SOURCE_DIR}/storage/entities.xsl + ${AKONADI_DB_SCHEME} + COMMAND ${XSLTPROC_EXECUTABLE} + --output ${CMAKE_CURRENT_BINARY_DIR}/entities.cpp + --stringparam code source + ${CMAKE_CURRENT_SOURCE_DIR}/storage/entities.xsl + ${AKONADI_DB_SCHEME} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/storage/entities.xsl + ${CMAKE_CURRENT_SOURCE_DIR}/storage/entities-header.xsl + ${CMAKE_CURRENT_SOURCE_DIR}/storage/entities-source.xsl + ${AKONADI_DB_SCHEME} +) + +add_test(akonadidb-xmllint ${XMLLINT_EXECUTABLE} --noout --schema ${CMAKE_CURRENT_SOURCE_DIR}/storage/akonadidb.xsd ${CMAKE_CURRENT_SOURCE_DIR}/storage/akonadidb.xml) +add_test(akonadidbupdate-xmllint ${XMLLINT_EXECUTABLE} --noout --schema ${CMAKE_CURRENT_SOURCE_DIR}/storage/dbupdate.xsd ${CMAKE_CURRENT_SOURCE_DIR}/storage/dbupdate.xml) + +akonadi_generate_schema(${AKONADI_DB_SCHEME} AkonadiSchema akonadischema) + +set(libakonadiserver_SRCS + akonadi.cpp + akthread.cpp + commandcontext.cpp + connection.cpp + collectionscheduler.cpp + dbusconnectionpool.cpp + handler.cpp + handlerhelper.cpp + intervalcheck.cpp + collectionreferencemanager.cpp + handler/akappend.cpp + handler/copy.cpp + handler/colcopy.cpp + handler/colmove.cpp + handler/create.cpp + handler/delete.cpp + handler/fetch.cpp + handler/fetchhelper.cpp + handler/link.cpp + handler/list.cpp + handler/login.cpp + handler/logout.cpp + handler/modify.cpp + handler/move.cpp + handler/remove.cpp + handler/resourceselect.cpp + handler/relationstore.cpp + handler/relationremove.cpp + handler/relationfetch.cpp + handler/search.cpp + handler/searchhelper.cpp + handler/searchpersistent.cpp + handler/searchresult.cpp + handler/status.cpp + handler/store.cpp + handler/tagappend.cpp + handler/tagfetch.cpp + handler/tagfetchhelper.cpp + handler/tagremove.cpp + handler/tagstore.cpp + handler/transaction.cpp + search/agentsearchengine.cpp + search/agentsearchinstance.cpp + search/searchtaskmanager.cpp + search/searchrequest.cpp + search/searchmanager.cpp + + storage/collectionqueryhelper.cpp + storage/collectionstatistics.cpp + storage/entity.cpp + ${CMAKE_CURRENT_BINARY_DIR}/entities.cpp + ${CMAKE_CURRENT_BINARY_DIR}/akonadischema.cpp + storage/datastore.cpp + storage/dbconfig.cpp + storage/dbconfigmysql.cpp + storage/dbconfigpostgresql.cpp + storage/dbconfigsqlite.cpp + storage/dbexception.cpp + storage/dbinitializer.cpp + storage/dbinitializer_p.cpp + storage/dbintrospector.cpp + storage/dbintrospector_impl.cpp + storage/dbupdater.cpp + storage/dbtype.cpp + storage/itemqueryhelper.cpp + storage/itemretriever.cpp + storage/itemretrievalmanager.cpp + storage/itemretrievaljob.cpp + storage/notificationcollector.cpp + storage/parthelper.cpp + storage/parttypehelper.cpp + storage/query.cpp + storage/querybuilder.cpp + storage/querycache.cpp + storage/queryhelper.cpp + storage/schematypes.cpp + storage/tagqueryhelper.cpp + storage/transaction.cpp + storage/parthelper.cpp + storage/partstreamer.cpp + storage/storagedebugger.cpp + tracer.cpp + utils.cpp + dbustracer.cpp + filetracer.cpp + notificationmanager.cpp + notificationsource.cpp + resourcemanager.cpp + cachecleaner.cpp + debuginterface.cpp + preprocessorinstance.cpp + preprocessormanager.cpp + storagejanitor.cpp +) + +set(akonadiserver_SRCS + main.cpp +) +ecm_qt_declare_logging_category(akonadiserver_SRCS HEADER akonadiserver_debug.h IDENTIFIER AKONADISERVER_LOG CATEGORY_NAME log_akonadiserver) +qt5_generate_dbus_interface(debuginterface.h org.freedesktop.Akonadi.DebugInterface.xml) + +qt5_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.TracerNotification.xml dbustracer.h Akonadi::Server::DBusTracer) +qt5_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Tracer.xml tracer.h Akonadi::Server::Tracer) +qt5_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml notificationmanager.h Akonadi::Server::NotificationManager) +qt5_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Server.xml akonadi.h Akonadi::Server::AkonadiServer) +qt5_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml notificationsource.h Akonadi::Server::NotificationSource) +qt5_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml storage/storagedebugger.h Akonadi::Server::StorageDebugger) +qt5_add_dbus_adaptor(libakonadiserver_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.DebugInterface.xml debuginterface.h Akonadi::Server::DebugInterface) +qt5_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml resourcemanager.h Akonadi::Server::ResourceManager) +qt5_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml preprocessormanager.h Akonadi::Server::PreprocessorManager) +qt5_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml agentmanagerinterface) +qt5_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Resource.xml resourceinterface) +qt5_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml preprocessorinterface) +qt5_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml agentcontrolinterface) +qt5_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml agentsearchinterface) + +qt5_add_resources(libakonadiserver_SRCS storage/akonadidb.qrc) + +add_library(libakonadiserver STATIC ${libakonadiserver_SRCS}) +target_link_libraries(libakonadiserver + akonadi_shared + KF5AkonadiPrivate + Qt5::Core + Qt5::Network + Qt5::Sql + Qt5::DBus + Qt5::Xml +) + +add_executable(akonadiserver ${akonadiserver_SRCS}) +set_target_properties(akonadiserver PROPERTIES OUTPUT_NAME akonadiserver) +target_link_libraries(akonadiserver + libakonadiserver +) + +install(TARGETS akonadiserver + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + storage/mysql-global.conf + storage/mysql-global-mobile.conf + DESTINATION ${CONFIG_INSTALL_DIR}/akonadi +) + +install(FILES + search/abstractsearchplugin.h + DESTINATION ${KF5_INCLUDE_INSTALL_DIR}/akonadi +) + +## DBus XML files +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.DebugInterface.xml + DESTINATION ${AKONADI_DBUS_INTERFACES_INSTALL_DIR} +) diff --git a/src/server/akonadi.cpp b/src/server/akonadi.cpp new file mode 100644 index 0000000..7cfcd0a --- /dev/null +++ b/src/server/akonadi.cpp @@ -0,0 +1,440 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "akonadi.h" +#include "connection.h" +#include "serveradaptor.h" + +#include + +#include "cachecleaner.h" +#include "intervalcheck.h" +#include "storagejanitor.h" +#include "storage/dbconfig.h" +#include "storage/datastore.h" +#include "notificationmanager.h" +#include "resourcemanager.h" +#include "tracer.h" +#include "utils.h" +#include "debuginterface.h" +#include "storage/itemretrievalmanager.h" +#include "storage/collectionstatistics.h" +#include "preprocessormanager.h" +#include "search/searchmanager.h" +#include "search/searchtaskmanager.h" + +#include "collectionreferencemanager.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#ifdef HAVE_UNISTD_H +# include +#endif +#include + +#ifdef Q_OS_WIN +#include +#include +#endif + +using namespace Akonadi; +using namespace Akonadi::Server; + +AkonadiServer *AkonadiServer::s_instance = 0; + +AkonadiServer::AkonadiServer(QObject *parent) + : QLocalServer(parent) + , mCacheCleaner(Q_NULLPTR) + , mIntervalCheck(Q_NULLPTR) + , mStorageJanitor(Q_NULLPTR) + , mItemRetrieval(Q_NULLPTR) + , mAgentSearchManager(Q_NULLPTR) + , mDatabaseProcess(0) + , mSearchManager(0) + , mAlreadyShutdown(false) +{ + // Register bunch of useful types + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + + qRegisterMetaType(); + qDBusRegisterMetaType(); +} + +bool AkonadiServer::init() +{ + const QString serverConfigFile = StandardDirs::serverConfigFile(XdgBaseDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + // Restrict permission to 600, as the file might contain database password in plaintext + QFile::setPermissions(serverConfigFile, QFile::ReadOwner | QFile::WriteOwner); + + if (!DbConfig::configuredDatabase()) { + quit(); + return false; + } + + if (DbConfig::configuredDatabase()->useInternalServer()) { + if (!startDatabaseProcess()) { + quit(); + return false; + } + } else { + if (!createDatabase()) { + quit(); + return false; + } + } + + DbConfig::configuredDatabase()->setup(); + + s_instance = this; + + const QString connectionSettingsFile = StandardDirs::connectionConfigFile(XdgBaseDirs::WriteOnly); + QSettings connectionSettings(connectionSettingsFile, QSettings::IniFormat); + +#ifdef Q_OS_WIN + HANDLE hToken = NULL; + PSID sid; + QString userID; + + OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &hToken); + if (hToken) { + DWORD size; + PTOKEN_USER userStruct; + + GetTokenInformation(hToken, TokenUser, NULL, 0, &size); + if (ERROR_INSUFFICIENT_BUFFER == GetLastError()) { + userStruct = reinterpret_cast(new BYTE[size]); + GetTokenInformation(hToken, TokenUser, userStruct, size, &size); + + int sidLength = GetLengthSid(userStruct->User.Sid); + sid = (PSID) malloc(sidLength); + CopySid(sidLength, sid, userStruct->User.Sid); + CloseHandle(hToken); + delete [] userStruct; + } + + LPWSTR s; + if (!ConvertSidToStringSidW(sid, &s)) { + akError() << "Could not determine user id for current process."; + userID = QString(); + } else { + userID = QString::fromUtf16(reinterpret_cast(s)); + LocalFree(s); + } + free(sid); + } + QString defaultPipe = QLatin1String("Akonadi-") + userID; + + QString namedPipe = settings.value(QLatin1String("Connection/NamedPipe"), defaultPipe).toString(); + if (!listen(namedPipe)) { + akError() << "Unable to listen on Named Pipe" << namedPipe; + quit(); + return false; + } + + connectionSettings.setValue(QLatin1String("Data/Method"), QLatin1String("NamedPipe")); + connectionSettings.setValue(QLatin1String("Data/NamedPipe"), namedPipe); +#else + const QString socketDir = Utils::preferredSocketDirectory(StandardDirs::saveDir("data")); + const QString socketFile = socketDir + QLatin1String("/akonadiserver.socket"); + QFile::remove(socketFile); + if (!listen(socketFile)) { + akError() << "Unable to listen on Unix socket" << socketFile; + quit(); + return false; + } + + connectionSettings.setValue(QStringLiteral("Data/Method"), QLatin1String("UnixPath")); + connectionSettings.setValue(QStringLiteral("Data/UnixPath"), socketFile); +#endif + + // initialize the database + DataStore *db = DataStore::self(); + if (!db->database().isOpen()) { + akError() << "Unable to open database" << db->database().lastError().text(); + quit(); + return false; + } + if (!db->init()) { + akError() << "Unable to initialize database."; + quit(); + return false; + } + + NotificationManager::self(); + Tracer::self(); + new DebugInterface(this); + ResourceManager::self(); + + CollectionStatistics::self(); + + // Initialize the preprocessor manager + PreprocessorManager::init(); + + // Forcibly disable it if configuration says so + if (settings.value(QStringLiteral("General/DisablePreprocessing"), false).toBool()) { + PreprocessorManager::instance()->setEnabled(false); + } + + if (settings.value(QStringLiteral("Cache/EnableCleaner"), true).toBool()) { + mCacheCleaner = new CacheCleaner(); + } + + mIntervalCheck = new IntervalCheck(); + mStorageJanitor = new StorageJanitor(); + mItemRetrieval = new ItemRetrievalManager(); + mAgentSearchManager = new SearchTaskManager(); + + const QStringList searchManagers = settings.value(QStringLiteral("Search/Manager"), + QStringList() << QStringLiteral("Agent")).toStringList(); + mSearchManager = new SearchManager(searchManagers); + + new ServerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Server"), this); + + const QByteArray dbusAddress = qgetenv("DBUS_SESSION_BUS_ADDRESS"); + if (!dbusAddress.isEmpty()) { + connectionSettings.setValue(QStringLiteral("DBUS/Address"), QLatin1String(dbusAddress)); + } + + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(DBus::serviceName(DBus::Control), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForOwnerChange, this); + + connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, + this, &AkonadiServer::serviceOwnerChanged); + + // Unhide all the items that are actually hidden. + // The hidden flag was probably left out after an (abrupt) + // server quit. We don't attempt to resume preprocessing + // for the items as we don't actually know at which stage the + // operation was interrupted... + db->unhideAllPimItems(); + + // Cleanup referenced collections from the last run + CollectionReferenceManager::cleanup(); + + // We are ready, now register org.freedesktop.Akonadi service to DBus and + // the fun can begin + if (!QDBusConnection::sessionBus().registerService(DBus::serviceName(DBus::Server))) { + akError() << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message(); + quit(); + return false; + } + + return true; +} + +AkonadiServer::~AkonadiServer() +{ +} + +template static void quitThread(T &thread) +{ + if (!thread) { + return; + } + thread->quit(); + thread->wait(); + delete thread; + thread = 0; +} + +bool AkonadiServer::quit() +{ + if (mAlreadyShutdown) { + return true; + } + mAlreadyShutdown = true; + + akDebug() << "terminating service threads"; + delete mCacheCleaner; + delete mIntervalCheck; + delete mStorageJanitor; + delete mItemRetrieval; + delete mAgentSearchManager; + delete mSearchManager; + + akDebug() << "terminating connection threads"; + for (int i = 0; i < mConnections.count(); ++i) { + delete mConnections[i]; + } + mConnections.clear(); + + // Terminate the preprocessor manager before the database but after all connections are gone + PreprocessorManager::done(); + + if (DbConfig::isConfigured()) { + if (DataStore::hasDataStore()) { + DataStore::self()->close(); + } + akDebug() << "stopping db process"; + stopDatabaseProcess(); + } + + QSettings settings(StandardDirs::serverConfigFile(), QSettings::IniFormat); + const QString connectionSettingsFile = StandardDirs::connectionConfigFile(XdgBaseDirs::WriteOnly); + +#ifndef Q_OS_WIN + const QString socketDir = Utils::preferredSocketDirectory(StandardDirs::saveDir("data")); + + if (!QDir::home().remove(socketDir + QLatin1String("/akonadiserver.socket"))) { + akError() << "Failed to remove Unix socket"; + } +#endif + if (!QDir::home().remove(connectionSettingsFile)) { + akError() << "Failed to remove runtime connection config file"; + } + + QTimer::singleShot(0, this, &AkonadiServer::doQuit); + + return true; +} + +void AkonadiServer::doQuit() +{ + QCoreApplication::exit(); +} + +void AkonadiServer::incomingConnection(quintptr socketDescriptor) +{ + if (mAlreadyShutdown) { + return; + } + QPointer connection = new Connection(socketDescriptor); + connect(connection.data(), &Connection::disconnected, + this, [connection]() { + delete connection.data(); + }, Qt::QueuedConnection); + mConnections.append(connection); +} + +AkonadiServer *AkonadiServer::instance() +{ + if (!s_instance) { + s_instance = new AkonadiServer(); + } + return s_instance; +} + +bool AkonadiServer::startDatabaseProcess() +{ + if (!DbConfig::configuredDatabase()->useInternalServer()) { + akFatal() << "Trying to start external database!"; + } + + // create the database directories if they don't exists + StandardDirs::saveDir("data"); + StandardDirs::saveDir("data", QStringLiteral("file_db_data")); + + return DbConfig::configuredDatabase()->startInternalServer(); +} + +bool AkonadiServer::createDatabase() +{ + bool success = true; + const QLatin1String initCon("initConnection"); + QSqlDatabase db = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), initCon); + DbConfig::configuredDatabase()->apply(db); + db.setDatabaseName(DbConfig::configuredDatabase()->databaseName()); + if (!db.isValid()) { + akError() << "Invalid database object during initial database connection"; + return false; + } + + if (db.open()) { + db.close(); + } else { + akError() << "Failed to use database" << DbConfig::configuredDatabase()->databaseName(); + akError() << "Database error:" << db.lastError().text(); + akDebug() << "Trying to create database now..."; + + db.close(); + db.setDatabaseName(QString()); + if (db.open()) { + { + QSqlQuery query(db); + if (!query.exec(QStringLiteral("CREATE DATABASE %1").arg(DbConfig::configuredDatabase()->databaseName()))) { + akError() << "Failed to create database"; + akError() << "Query error:" << query.lastError().text(); + akError() << "Database error:" << db.lastError().text(); + success = false; + } + } // make sure query is destroyed before we close the db + db.close(); + } else { + akError() << "Failed to connect to database!"; + akError() << "Database error:" << db.lastError().text(); + success = false; + } + } + QSqlDatabase::removeDatabase(initCon); + return success; +} + +void AkonadiServer::stopDatabaseProcess() +{ + if (!DbConfig::configuredDatabase()->useInternalServer()) { + return; + } + + DbConfig::configuredDatabase()->stopInternalServer(); +} + +void AkonadiServer::serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(name); + Q_UNUSED(oldOwner); + if (newOwner.isEmpty()) { + akError() << "Control process died, committing suicide!"; + quit(); + } +} + +CacheCleaner *AkonadiServer::cacheCleaner() +{ + if (mCacheCleaner) { + return mCacheCleaner; + } + + return Q_NULLPTR; +} + +IntervalCheck *AkonadiServer::intervalChecker() +{ + if (mIntervalCheck) { + return mIntervalCheck; + } + + return Q_NULLPTR; +} diff --git a/src/server/akonadi.h b/src/server/akonadi.h new file mode 100644 index 0000000..a02f0ec --- /dev/null +++ b/src/server/akonadi.h @@ -0,0 +1,99 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADISERVER_H +#define AKONADISERVER_H + +#include +#include + +#include + +class QProcess; + +namespace Akonadi { +namespace Server { + +class Connection; +class SearchManagerThread; +class ItemRetrievalManager; +class SearchTaskManager; +class SearchManager; +class StorageJanitor; +class CacheCleaner; +class IntervalCheck; + +class AkonadiServer : public QLocalServer +{ + Q_OBJECT + +public: + ~AkonadiServer(); + static AkonadiServer *instance(); + + /** + * Can return a nullptr + */ + CacheCleaner *cacheCleaner(); + + /** + * Can return a nullptr + */ + IntervalCheck *intervalChecker(); + +public Q_SLOTS: + /** + * Triggers a clean server shutdown. + */ + virtual bool quit(); + + virtual bool init(); + +private Q_SLOTS: + void doQuit(); + void serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner); + +protected: + /** reimpl */ + virtual void incomingConnection(quintptr socketDescriptor); + +private: + bool startDatabaseProcess(); + bool createDatabase(); + void stopDatabaseProcess(); + +protected: + AkonadiServer(QObject *parent = 0); + + CacheCleaner *mCacheCleaner; + IntervalCheck *mIntervalCheck; + StorageJanitor *mStorageJanitor; + ItemRetrievalManager *mItemRetrieval; + SearchTaskManager *mAgentSearchManager; + QProcess *mDatabaseProcess; + QVector> mConnections; + SearchManager *mSearchManager; + bool mAlreadyShutdown; + + static AkonadiServer *s_instance; +}; + +} // namespace Server +} // namespace Akonadi +#endif diff --git a/src/server/akthread.cpp b/src/server/akthread.cpp new file mode 100644 index 0000000..c6241ac --- /dev/null +++ b/src/server/akthread.cpp @@ -0,0 +1,86 @@ +/* + Copyright (c) 2015 Daniel Vrátil + + 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 "akthread.h" +#include "storage/datastore.h" + +#include +#include + +using namespace Akonadi::Server; + +AkThread::AkThread(StartMode startMode, QThread::Priority priority, QObject *parent) + : QObject(parent) +{ + QThread *thread = new QThread(); + moveToThread(thread); + thread->start(priority); + + if (startMode == AutoStart) { + startThread(); + } +} + +AkThread::AkThread(QThread::Priority priority, QObject *parent) + : AkThread(AutoStart, priority, parent) +{ +} + +AkThread::~AkThread() +{ +} + +void AkThread::startThread() +{ + const bool init = QMetaObject::invokeMethod(this, "init", Qt::QueuedConnection); + Q_ASSERT(init); Q_UNUSED(init); +} + +void AkThread::quitThread() +{ + akDebug() << "Shutting down" << objectName() << "..."; + const bool invoke = QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); + Q_ASSERT(invoke); Q_UNUSED(invoke); + if (!thread()->wait(10 * 1000)) { + thread()->terminate(); + thread()->wait(); + } + delete thread(); +} + +void AkThread::init() +{ + Q_ASSERT(thread() == QThread::currentThread()); + Q_ASSERT(thread() != qApp->thread()); +} + +void AkThread::quit() +{ + Q_ASSERT(thread() == QThread::currentThread()); + Q_ASSERT(thread() != qApp->thread()); + + if (DataStore::hasDataStore()) { + DataStore::self()->close(); + } + + thread()->quit(); +} + + diff --git a/src/server/akthread.h b/src/server/akthread.h new file mode 100644 index 0000000..09b4efe --- /dev/null +++ b/src/server/akthread.h @@ -0,0 +1,58 @@ +/* + Copyright (c) 2015 Daniel Vrátil + + 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_SERVER_AKTHREAD_H +#define AKONADI_SERVER_AKTHREAD_H + +#include +#include + +namespace Akonadi { +namespace Server { + +class AkThread : public QObject +{ + Q_OBJECT +public: + enum StartMode { + AutoStart, + ManualStart + }; + + explicit AkThread(QThread::Priority priority = QThread::InheritPriority, + QObject *parent = Q_NULLPTR); + explicit AkThread(StartMode startMode, + QThread::Priority priority = QThread::InheritPriority, + QObject *parent = Q_NULLPTR); + virtual ~AkThread(); + +protected: + void quitThread(); + void startThread(); + +protected Q_SLOTS: + virtual void init(); + virtual void quit(); + +}; + +} +} + +#endif // AKONADI_SERVER_AKTHREAD_H diff --git a/src/server/cachecleaner.cpp b/src/server/cachecleaner.cpp new file mode 100644 index 0000000..9277fdf --- /dev/null +++ b/src/server/cachecleaner.cpp @@ -0,0 +1,153 @@ +/* + Copyright (c) 2007 Volker Krause + Copyright (C) 2014 Daniel Vrátil + + 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 "cachecleaner.h" +#include "storage/parthelper.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" +#include "storage/entity.h" +#include "akonadi.h" + +#include +#include + +using namespace Akonadi::Server; + +QMutex CacheCleanerInhibitor::sLock; +int CacheCleanerInhibitor::sInhibitCount = 0; + +CacheCleanerInhibitor::CacheCleanerInhibitor(bool doInhibit) + : mInhibited(false) +{ + if (doInhibit) { + inhibit(); + } +} + +CacheCleanerInhibitor::~CacheCleanerInhibitor() +{ + if (mInhibited) { + uninhibit(); + } +} + +void CacheCleanerInhibitor::inhibit() +{ + if (mInhibited) { + akError() << "Cannot recursively inhibit an inhibitor"; + return; + } + + sLock.lock(); + if (++sInhibitCount == 1) { + if (AkonadiServer::instance()->cacheCleaner()) { + AkonadiServer::instance()->cacheCleaner()->inhibit(true); + } + } + sLock.unlock(); + mInhibited = true; +} + +void CacheCleanerInhibitor::uninhibit() +{ + if (!mInhibited) { + akError() << "Cannot uninhibit an uninhibited inhibitor"; // aaaw yeah + return; + } + mInhibited = false; + + sLock.lock(); + Q_ASSERT(sInhibitCount > 0); + if (--sInhibitCount == 0) { + if (AkonadiServer::instance()->cacheCleaner()) { + AkonadiServer::instance()->cacheCleaner()->inhibit(false); + } + } + sLock.unlock(); +} + +CacheCleaner::CacheCleaner(QObject *parent) + : CollectionScheduler(QThread::IdlePriority, parent) +{ + setObjectName(QStringLiteral("CacheCleaner")); + setMinimumInterval(5); +} + +CacheCleaner::~CacheCleaner() +{ + quitThread(); +} + +int CacheCleaner::collectionScheduleInterval(const Collection &collection) +{ + return collection.cachePolicyCacheTimeout(); +} + +bool CacheCleaner::hasChanged(const Collection &collection, const Collection &changed) +{ + return collection.cachePolicyLocalParts() != changed.cachePolicyLocalParts() + || collection.cachePolicyCacheTimeout() != changed.cachePolicyCacheTimeout() + || collection.cachePolicyInherit() != changed.cachePolicyInherit(); +} + +bool CacheCleaner::shouldScheduleCollection(const Collection &collection) +{ + return collection.cachePolicyLocalParts() != QLatin1String("ALL") + && collection.cachePolicyCacheTimeout() >= 0 + && (collection.enabled() || (collection.displayPref() == Tristate::True) || (collection.syncPref() == Tristate::True) || (collection.indexPref() == Tristate::True)) + && collection.resourceId() > 0; +} + +void CacheCleaner::collectionExpired(const Collection &collection) +{ + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdColumn(), PimItem::idFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); + qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::Equals, collection.id()); + qb.addValueCondition(PimItem::atimeFullColumnName(), Query::Less, QDateTime::currentDateTime().addSecs(-60 * collection.cachePolicyCacheTimeout())); + qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); + qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("PLD")); + qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false); + + QStringList localParts; + QStringList partNames = collection.cachePolicyLocalParts().split(QStringLiteral(" ")); + Q_FOREACH (QString partName, partNames) { + if (partName.startsWith(QLatin1String(AKONADI_PARAM_PLD))) { + partName = partName.mid(4); + } + qb.addValueCondition(PartType::nameFullColumnName(), Query::NotEquals, partName); + } + if (qb.exec()) { + const Part::List parts = qb.result(); + if (!parts.isEmpty()) { + akDebug() << "found" << parts.count() << "item parts to expire in collection" << collection.name(); + // clear data field + Q_FOREACH (Part part, parts) { + try { + if (!PartHelper::truncate(part)) { + akDebug() << "failed to update item part" << part.id(); + } + } catch (const PartHelperException &e) { + akError() << e.type() << e.what(); + } + } + } + } +} diff --git a/src/server/cachecleaner.h b/src/server/cachecleaner.h new file mode 100644 index 0000000..fc33b83 --- /dev/null +++ b/src/server/cachecleaner.h @@ -0,0 +1,89 @@ +/* + 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_CACHECLEANER_H +#define AKONADI_CACHECLEANER_H + +#include "collectionscheduler.h" + +#include + +namespace Akonadi { +namespace Server { + +class Collection; + +/** + * A RAII helper class to temporarily stop the CacheCleaner. This allows long-lasting + * operations to safely retrieve all data from resource and perform an operation on them + * (like move or copy) without risking that the cache will be cleaned in the meanwhile + * + * The inhibitor is recursive, so it's possible to create multiple instances of the + * CacheCleanerInhibitor and the CacheCleaner will be inhibited until all instances + * are destroyed again. However it's not possible to inhibit a single inhibitor + * multiple times. + */ +class CacheCleanerInhibitor +{ +public: + CacheCleanerInhibitor(bool inhibit = true); + ~CacheCleanerInhibitor(); + + void inhibit(); + void uninhibit(); + +private: + Q_DISABLE_COPY(CacheCleanerInhibitor) + static QMutex sLock; + static int sInhibitCount; + bool mInhibited; +}; + +/** + Cache cleaner. + */ +class CacheCleaner : public CollectionScheduler +{ + Q_OBJECT + +public: + /** + Creates a new cache cleaner thread. + @param parent The parent object. + */ + CacheCleaner(QObject *parent = 0); + ~CacheCleaner(); + +protected: + void collectionExpired(const Collection &collection); + int collectionScheduleInterval(const Collection &collection); + bool hasChanged(const Collection &collection, const Collection &changed); + bool shouldScheduleCollection(const Collection &collection); + +private: + static CacheCleaner *sInstance; + + friend class CacheCleanerInhibitor; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/collectionreferencemanager.cpp b/src/server/collectionreferencemanager.cpp new file mode 100644 index 0000000..1cabdf4 --- /dev/null +++ b/src/server/collectionreferencemanager.cpp @@ -0,0 +1,112 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "collectionreferencemanager.h" + +#include "akonadi.h" +#include "cachecleaner.h" +#include "storage/selectquerybuilder.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +CollectionReferenceManager *CollectionReferenceManager::s_instance = 0; + +CollectionReferenceManager::CollectionReferenceManager() + : mReferenceLock(QMutex::Recursive) +{ +} + +CollectionReferenceManager *CollectionReferenceManager::instance() +{ + static QMutex s_instanceLock; + QMutexLocker locker(&s_instanceLock); + if (!s_instance) { + s_instance = new CollectionReferenceManager(); + } + return s_instance; +} + +void CollectionReferenceManager::referenceCollection(const QByteArray &sessionId, const Collection &collection, bool reference) +{ + QMutexLocker locker(&mReferenceLock); + if (reference) { + if (!mReferenceMap.contains(collection.id(), sessionId)) { + mReferenceMap.insert(collection.id(), sessionId); + } + } else { + mReferenceMap.remove(collection.id(), sessionId); + expireCollectionIfNecessary(collection.id()); + } +} + +void CollectionReferenceManager::removeSession(const QByteArray &sessionId) +{ + QMutexLocker locker(&mReferenceLock); + Q_FOREACH (Collection::Id col, mReferenceMap.keys(sessionId)) { + mReferenceMap.remove(col, sessionId); + expireCollectionIfNecessary(col); + if (!isReferenced(col)) { + Collection collection = Collection::retrieveById(col); + collection.setReferenced(false); + collection.update(); + } + } +} + +bool CollectionReferenceManager::isReferenced(Collection::Id collection) const +{ + QMutexLocker locker(&mReferenceLock); + return mReferenceMap.contains(collection); +} + +bool CollectionReferenceManager::isReferenced(Collection::Id collection, const QByteArray &sessionId) const +{ + QMutexLocker locker(&mReferenceLock); + return mReferenceMap.contains(collection, sessionId); +} + +void CollectionReferenceManager::expireCollectionIfNecessary(Collection::Id collection) +{ + QMutexLocker locker(&mReferenceLock); + if (!isReferenced(collection)) { + if (AkonadiServer::instance()->cacheCleaner()) { + AkonadiServer::instance()->cacheCleaner()->collectionChanged(collection); + } + } +} + +void CollectionReferenceManager::cleanup() +{ + SelectQueryBuilder qb; + qb.addValueCondition(Collection::referencedColumn(), Query::Equals, true); + if (!qb.exec()) { + akError() << "Failed to execute collection reference cleanup query."; + return; + } + Q_FOREACH (Collection col, qb.result()) { + col.setReferenced(false); + col.update(); + if (AkonadiServer::instance()->cacheCleaner()) { + AkonadiServer::instance()->cacheCleaner()->collectionChanged(col.id()); + } + } + QMutexLocker locker(&instance()->mReferenceLock); + instance()->mReferenceMap.clear(); +} diff --git a/src/server/collectionreferencemanager.h b/src/server/collectionreferencemanager.h new file mode 100644 index 0000000..b6df0bb --- /dev/null +++ b/src/server/collectionreferencemanager.h @@ -0,0 +1,53 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 COLLECTIONREFERENCEMANAGER_H +#define COLLECTIONREFERENCEMANAGER_H + +#include "storage/entity.h" +#include "entities.h" +#include + +namespace Akonadi { +namespace Server { + +class CollectionReferenceManager +{ +public: + static CollectionReferenceManager *instance(); + + void referenceCollection(const QByteArray &sessionId, const Collection &collection, bool reference); + void removeSession(const QByteArray &sessionId); + bool isReferenced(Collection::Id collection) const; + bool isReferenced(Collection::Id collection, const QByteArray &sessionId) const; + + static void cleanup(); + +private: + CollectionReferenceManager(); + void expireCollectionIfNecessary(Collection::Id collection); + + QMultiHash mReferenceMap; + static CollectionReferenceManager *s_instance; + mutable QMutex mReferenceLock; +}; + +} +} + +#endif diff --git a/src/server/collectionscheduler.cpp b/src/server/collectionscheduler.cpp new file mode 100644 index 0000000..3235a7f --- /dev/null +++ b/src/server/collectionscheduler.cpp @@ -0,0 +1,322 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "collectionscheduler.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" +#include "akonadiserver_debug.h" + +#include + +#include +#include + +namespace Akonadi { +namespace Server { + +/** + * @warning: QTimer's methods are not virtual, so it's necessary to always call + * methods on pointer to PauseableTimer! + */ +class PauseableTimer : public QTimer +{ + Q_OBJECT + +public: + PauseableTimer(QObject *parent = 0) + : QTimer(parent) + { + } + + void start(int interval) + { + mStarted = QDateTime::currentDateTime(); + mPaused = QDateTime(); + setInterval(interval); + QTimer::start(interval); + } + + void start() + { + start(interval()); + } + + void stop() + { + mStarted = QDateTime(); + mPaused = QDateTime(); + QTimer::stop(); + } + + Q_INVOKABLE void pause() + { + if (!isActive()) { + akError() << "Cannot pause an inactive timer"; + return; + } + if (isPaused()) { + akError() << "Cannot pause an already paused timer"; + return; + } + + mPaused = QDateTime::currentDateTime(); + QTimer::stop(); + } + + Q_INVOKABLE void resume() + { + if (!isPaused()) { + akError() << "Cannot resume a timer that is not paused."; + return; + } + + const int remainder = interval() - (mStarted.secsTo(mPaused) * 1000); + start(qMax(0, remainder)); + mPaused = QDateTime(); + // Update mStarted so that pause() can be called repeatedly + mStarted = QDateTime::currentDateTime(); + } + + bool isPaused() const + { + return mPaused.isValid(); + } + +private: + QDateTime mStarted; + QDateTime mPaused; +}; + +} // namespace Server +} // namespace Akonadi + +using namespace Akonadi::Server; + +CollectionScheduler::CollectionScheduler(QThread::Priority priority, QObject *parent) + : AkThread(priority, parent) + , mScheduler(Q_NULLPTR) + , mMinInterval(5) +{ +} + +CollectionScheduler::~CollectionScheduler() +{ +} + +void CollectionScheduler::quit() +{ + delete mScheduler; + + AkThread::quit(); +} + +void CollectionScheduler::inhibit(bool inhibit) +{ + if (inhibit && mScheduler->isActive() && !mScheduler->isPaused()) { + const bool success = QMetaObject::invokeMethod(mScheduler, "pause", Qt::QueuedConnection); + Q_ASSERT(success); + Q_UNUSED(success); + } else if (!inhibit && mScheduler->isPaused()) { + const bool success = QMetaObject::invokeMethod(mScheduler, "resume", Qt::QueuedConnection); + Q_ASSERT(success); + Q_UNUSED(success); + } +} + +int CollectionScheduler::minimumInterval() const +{ + return mMinInterval; +} + +void CollectionScheduler::setMinimumInterval(int intervalMinutes) +{ + mMinInterval = intervalMinutes; +} + +void CollectionScheduler::collectionAdded(qint64 collectionId) +{ + Collection collection = Collection::retrieveById(collectionId); + DataStore::self()->activeCachePolicy(collection); + if (shouldScheduleCollection(collection)) { + QMetaObject::invokeMethod(this, "scheduleCollection", + Qt::QueuedConnection, + Q_ARG(Collection, collection)); + } +} + +void CollectionScheduler::collectionChanged(qint64 collectionId) +{ + QMutexLocker locker(&mScheduleLock); + Q_FOREACH (const Collection &collection, mSchedule) { + if (collection.id() == collectionId) { + Collection changed = Collection::retrieveById(collectionId); + DataStore::self()->activeCachePolicy(changed); + if (hasChanged(collection, changed)) { + if (shouldScheduleCollection(changed)) { + locker.unlock(); + // Scheduling the changed collection will automatically remove the old one + scheduleCollection(changed); + } else { + locker.unlock(); + // If the collection should no longer be scheduled then remove it + collectionRemoved(collectionId); + } + } + + return; + } + } + + // We don't know the collection yet, but maybe now it can be scheduled + collectionAdded(collectionId); +} + +void CollectionScheduler::collectionRemoved(qint64 collectionId) +{ + QMutexLocker locker(&mScheduleLock); + Q_FOREACH (const Collection &collection, mSchedule) { + if (collection.id() == collectionId) { + const uint key = mSchedule.key(collection); + const bool reschedule = (key == mSchedule.constBegin().key()); + mSchedule.remove(key); + locker.unlock(); + + // If we just remove currently scheduled collection, schedule the next one + if (reschedule) { + startScheduler(); + } + + return; + } + } +} + +void CollectionScheduler::startScheduler() +{ + // Don't restart timer if we are paused. + if (mScheduler->isPaused()) { + return; + } + + QMutexLocker locker(&mScheduleLock); + if (mSchedule.isEmpty()) { + // Stop the timer. It will be started again once some collection is scheduled + mScheduler->stop(); + return; + } + + // Get next collection to expire and start the timer + const uint next = mSchedule.constBegin().key(); + // cast next - now() to int, so that we get negative result when next is in the past + mScheduler->start(qMax(0, (int)(next - QDateTime::currentDateTime().toTime_t()) * 1000)); +} + +void CollectionScheduler::scheduleCollection(Collection collection, bool shouldStartScheduler) +{ + QMutexLocker locker(&mScheduleLock); + auto i = std::find(mSchedule.cbegin(), mSchedule.cend(), collection); + if (i != mSchedule.cend()) { + mSchedule.remove(i.key(), i.value()); + } + + DataStore::self()->activeCachePolicy(collection); + + if (!shouldScheduleCollection(collection)) { + return; + } + + const int expireMinutes = qMax(mMinInterval, collectionScheduleInterval(collection)); + uint nextCheck = QDateTime::currentDateTime().toTime_t() + (expireMinutes * 60); + + // Check whether there's another check scheduled within a minute after this one. + // If yes, then delay this check so that it's scheduled together with the others + // This is a minor optimization to reduce wakeups and SQL queries + QMap::iterator it = mSchedule.lowerBound(nextCheck); + if (it != mSchedule.end() && it.key() - nextCheck < 60) { + nextCheck = it.key(); + + // Also check whether there's another checked scheduled within a minute before + // this one. + } else if (it != mSchedule.begin()) { + --it; + if (nextCheck - it.key() < 60) { + nextCheck = it.key(); + } + } + + mSchedule.insert(nextCheck, collection); + if (shouldStartScheduler && !mScheduler->isActive()) { + locker.unlock(); + startScheduler(); + } +} + +void CollectionScheduler::init() +{ + AkThread::init(); + + mScheduler = new PauseableTimer(); + mScheduler->setSingleShot(true); + connect(mScheduler, &QTimer::timeout, + this, &CollectionScheduler::schedulerTimeout); + + // Only retrieve enabled collections and referenced collections, we don't care + // about anything else + SelectQueryBuilder qb; + Query::Condition orCondition(Query::Or); + orCondition.addValueCondition(Collection::syncPrefFullColumnName(), Query::Equals, (int)Akonadi::Tristate::True); + Query::Condition andCondition(Query::And); + andCondition.addValueCondition(Collection::syncPrefFullColumnName(), Query::Equals, (int)Akonadi::Tristate::Undefined); + andCondition.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true); + orCondition.addCondition(andCondition); + orCondition.addValueCondition(Collection::referencedFullColumnName(), Query::Equals, true); + qb.addCondition(orCondition); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to query initial collections for scheduler!"; + qCWarning(AKONADISERVER_LOG) << "Not a fatal error, no collections will be scheduled for sync or cache expiration!"; + } + + const Collection::List collections = qb.result(); + Q_FOREACH (const Collection &collection, collections) { + scheduleCollection(collection); + } + + startScheduler(); +} + +void CollectionScheduler::schedulerTimeout() +{ + // Call stop() explicitly to reset the timer + mScheduler->stop(); + + mScheduleLock.lock(); + const uint timestamp = mSchedule.constBegin().key(); + const QList collections = mSchedule.values(timestamp); + mSchedule.remove(timestamp); + mScheduleLock.unlock(); + + Q_FOREACH (const Collection &collection, collections) { + collectionExpired(collection); + scheduleCollection(collection, false); + } + + startScheduler(); +} + +#include "collectionscheduler.moc" diff --git a/src/server/collectionscheduler.h b/src/server/collectionscheduler.h new file mode 100644 index 0000000..148610a --- /dev/null +++ b/src/server/collectionscheduler.h @@ -0,0 +1,88 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_SERVER_COLLECTIONSCHEDULER_H +#define AKONADI_SERVER_COLLECTIONSCHEDULER_H + +#include +#include +#include +#include + +#include "entities.h" +#include "akthread.h" + +namespace Akonadi { +namespace Server { + +class Collection; +class PauseableTimer; + +class CollectionScheduler : public AkThread +{ + Q_OBJECT + +public: + CollectionScheduler(QThread::Priority priority, QObject *parent = 0); + virtual ~CollectionScheduler(); + + void collectionChanged(qint64 collectionId); + void collectionRemoved(qint64 collectionId); + void collectionAdded(qint64 collectionId); + + /** + * Sets the minimum timeout interval. + * + * Default value is 5. + * + * @p intervalMinutes Minimum timeout interval in minutes. + */ + void setMinimumInterval(int intervalMinutes); + int minimumInterval() const; + +protected: + virtual void init() Q_DECL_OVERRIDE; + virtual void quit() Q_DECL_OVERRIDE; + + virtual bool shouldScheduleCollection(const Collection &collection) = 0; + virtual bool hasChanged(const Collection &collection, const Collection &changed) = 0; + /** + * @return Return cache timeout in minutes + */ + virtual int collectionScheduleInterval(const Collection &collection) = 0; + virtual void collectionExpired(const Collection &collection) = 0; + + void inhibit(bool inhibit = true); + +protected Q_SLOTS: + void schedulerTimeout(); + void startScheduler(); + void scheduleCollection(/*sic!*/ Collection collection, bool shouldStartScheduler = true); + +protected: + QMutex mScheduleLock; + QMultiMap mSchedule; + PauseableTimer *mScheduler; + int mMinInterval; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SERVER_COLLECTIONSCHEDULER_H diff --git a/src/server/commandcontext.cpp b/src/server/commandcontext.cpp new file mode 100644 index 0000000..8557c73 --- /dev/null +++ b/src/server/commandcontext.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "commandcontext.h" +#include "storage/selectquerybuilder.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +CommandContext::CommandContext() + : mTagId(-1) +{ +} + +void CommandContext::setResource(const Resource &resource) +{ + mResource = resource; +} + +Resource CommandContext::resource() const +{ + return mResource; +} + +bool CommandContext::setScopeContext(const Protocol::ScopeContext &scopeContext) +{ + if (scopeContext.hasContextId(Protocol::ScopeContext::Collection)) { + mCollection = Collection::retrieveById(scopeContext.contextId(Protocol::ScopeContext::Collection)); + } else if (scopeContext.hasContextRID(Protocol::ScopeContext::Collection)) { + if (mResource.isValid()) { + SelectQueryBuilder qb; + qb.addValueCondition(Collection::remoteIdColumn(), Query::Equals, scopeContext.contextRID(Protocol::ScopeContext::Collection)); + qb.addValueCondition(Collection::resourceIdColumn(), Query::Equals, mResource.id()); + qb.exec(); + Collection::List cols = qb.result(); + if (cols.isEmpty()) { + // error + return false; + } + mCollection = cols.at(0); + } else { + return false; + } + } + + if (scopeContext.hasContextId(Protocol::ScopeContext::Tag)) { + mTagId = scopeContext.contextId(Protocol::ScopeContext::Tag); + } + + return true; +} + +void CommandContext::setCollection(const Collection &collection) +{ + mCollection = collection; +} + +qint64 CommandContext::collectionId() const +{ + return mCollection.id(); +} + +Collection CommandContext::collection() const +{ + return mCollection; +} + +void CommandContext::setTag(qint64 tagId) +{ + mTagId = tagId; +} + +qint64 CommandContext::tagId() const +{ + return mTagId; +} + +Tag CommandContext::tag() const +{ + if (mTagId == -1) { + return Tag(); + } + + return Tag::retrieveById(mTagId); +} + +bool CommandContext::isEmpty() const +{ + return !mCollection.isValid() && mTagId < 0; +} diff --git a/src/server/commandcontext.h b/src/server/commandcontext.h new file mode 100644 index 0000000..6286eb8 --- /dev/null +++ b/src/server/commandcontext.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef COMMANDCONTEXT_H +#define COMMANDCONTEXT_H + +#include "entities.h" + +namespace Akonadi { + +namespace Protocol +{ +class ScopeContext; +} + +namespace Server { + +class CommandContext +{ +public: + CommandContext(); + + void setResource(const Resource &resource); + Resource resource() const; + + bool setScopeContext(const Protocol::ScopeContext &scopeContext); + + void setCollection(const Collection &collection); + qint64 collectionId() const; + Collection collection() const; + + void setTag(qint64 tagId); + qint64 tagId() const; + Tag tag() const; + + bool isEmpty() const; + +private: + Resource mResource; + Collection mCollection; + qint64 mTagId; +}; + +} + +} + +#endif // COMMANDCONTEXT_H diff --git a/src/server/connection.cpp b/src/server/connection.cpp new file mode 100644 index 0000000..bf3d6b8 --- /dev/null +++ b/src/server/connection.cpp @@ -0,0 +1,444 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * Copyright (C) 2013 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#include "connection.h" +#include "akonadiserver_debug.h" + +#include +#include +#include +#include + +#include "storage/datastore.h" +#include "handler.h" +#include "notificationmanager.h" + +#include "tracer.h" +#include "collectionreferencemanager.h" + +#include +#include + +#include + +#include +#include +#include + + +using namespace Akonadi; +using namespace Akonadi::Server; + +#define IDLE_TIMER_TIMEOUT 180000 // 3 min + +Connection::Connection(QObject *parent) + : AkThread(QThread::InheritPriority, parent) + , m_socketDescriptor(0) + , m_socket(0) + , m_currentHandler(0) + , m_connectionState(NonAuthenticated) + , m_isNotificationBus(false) + , m_backend(0) + , m_verifyCacheOnRetrieval(false) + , m_idleTimer(Q_NULLPTR) + , m_totalTime( 0 ) + , m_reportTime( false ) +{ +} + +Connection::Connection(quintptr socketDescriptor, QObject *parent) + : Connection(parent) +{ + m_socketDescriptor = socketDescriptor; + m_identifier.sprintf("%p", static_cast(this)); + setObjectName(m_identifier); + + const QSettings settings(Akonadi::StandardDirs::serverConfigFile(), QSettings::IniFormat); + m_verifyCacheOnRetrieval = settings.value(QStringLiteral("Cache/VerifyOnRetrieval"), m_verifyCacheOnRetrieval).toBool(); +} + +void Connection::init() +{ + AkThread::init(); + + QLocalSocket *socket = new QLocalSocket(); + + if (!socket->setSocketDescriptor(m_socketDescriptor)) { + qCWarning(AKONADISERVER_LOG) << "Connection(" << m_identifier + << ")::run: failed to set socket descriptor: " + << socket->error() << "(" << socket->errorString() << ")"; + delete socket; + return; + } + + m_socket = socket; + + /* Whenever a full command has been read, it is delegated to the responsible + * handler and processed by that. If that command needs to do something + * asynchronous such as ask the db for data, it returns and the input + * queue can continue to be processed. Whenever there is something to + * be sent back to the user it is queued in the form of a Response object. + * All this is meant to make it possible to process large incoming or + * outgoing data transfers in a streaming manner, without having to + * hold them in memory 'en gros'. */ + + connect(socket, &QIODevice::readyRead, + this, &Connection::slotNewData); + connect(socket, &QLocalSocket::disconnected, + this, &Connection::disconnected); + + m_idleTimer = new QTimer(this); + connect(m_idleTimer, &QTimer::timeout, + this, &Connection::slotConnectionIdle); + + slotSendHello(); +} + +void Connection::quit() +{ + Tracer::self()->endConnection(m_identifier, QString()); + collectionReferenceManager()->removeSession(m_sessionId); + NotificationManager::self()->unregisterConnection(this); + + delete m_socket; + m_socket = 0; + + m_idleTimer->stop(); + delete m_idleTimer; + + AkThread::quit(); +} + +void Connection::slotSendHello() +{ + sendResponse(0, Protocol::HelloResponse(QStringLiteral("Akonadi"), + QStringLiteral("Not Really IMAP server"), + Protocol::version())); +} + +DataStore *Connection::storageBackend() +{ + if (!m_backend) { + m_backend = DataStore::self(); + } + return m_backend; +} + +CollectionReferenceManager *Connection::collectionReferenceManager() +{ + return CollectionReferenceManager::instance(); +} + +Connection::~Connection() +{ + quitThread(); + + if (m_reportTime) { + reportTime(); + } +} + +void Connection::slotConnectionIdle() +{ + Q_ASSERT(m_currentHandler == 0); + if (m_backend && m_backend->isOpened() ) { + if (m_backend->inTransaction()) { + // This is a programming error, the timer should not have fired. + // But it is safer to abort and leave the connection open, until + // a later operation causes the idle timer to fire (than crash + // the akonadi server). + akDebug() << "NOT Closing idle db connection; we are in transaction"; + return; + } + m_backend->close(); + } +} + +void Connection::slotNewData() +{ + if (m_isNotificationBus) { + qWarning() << "Connection" << sessionId() << ": received data when in NotificationBus mode!"; + return; + } + + m_idleTimer->stop(); + + // will only open() a previously idle backend. + // Otherwise, a new backend could lazily be constructed by later calls. + if (!storageBackend()->isOpened()) { + m_backend->open(); + } + + QString currentCommand; + while (m_socket->bytesAvailable() > (int) sizeof(qint64)) { + QDataStream stream(m_socket); + + qint64 tag = -1; + stream >> tag; + // TODO: Check tag is incremental sequence + + Protocol::Command cmd; + try { + cmd = Protocol::deserialize(m_socket); + } catch (const Akonadi::ProtocolException &e) { + qDebug() << "ProtocolException:" << e.what(); + slotConnectionStateChange(Server::LoggingOut); + return; + } catch (const std::exception &e) { + qDebug() << "Unknown exception:" << e.what(); + slotConnectionStateChange(Server::LoggingOut); + return; + } + if (cmd.type() == Protocol::Command::Invalid) { + qDebug() << "Received an invalid command: resetting connection"; + slotConnectionStateChange(Server::LoggingOut); + return; + } + + // Tag context and collection context is not persistent. + context()->setTag(-1); + context()->setCollection(Collection()); + if (Tracer::self()->currentTracer() != QLatin1String("null")) { + Tracer::self()->connectionInput(m_identifier, QByteArray::number(tag) + ' ' + cmd.debugString().toUtf8()); + } + + m_currentHandler = findHandlerForCommand(cmd.type()); + if (!m_currentHandler) { + qDebug() << "Invalid command: no such handler for" << cmd.type(); + slotConnectionStateChange(Server::LoggingOut); + return; + } + if (m_reportTime) { + startTime(); + } + connect(m_currentHandler.data(), &Handler::connectionStateChange, + this, &Connection::slotConnectionStateChange, + Qt::DirectConnection); + + m_currentHandler->setConnection(this); + m_currentHandler->setTag(tag); + m_currentHandler->setCommand(cmd); + try { + if (!m_currentHandler->parseStream()) { + // TODO: What to do? How do we know we reached the end of command? + } + } catch (const Akonadi::Server::HandlerException &e) { + if (m_currentHandler) { + m_currentHandler->failureResponse(e.what()); + } + } catch (const Akonadi::Server::Exception &e) { + if (m_currentHandler) { + m_currentHandler->failureResponse(QString::fromUtf8(e.type()) + QLatin1String(": ") + QString::fromUtf8(e.what())); + } + } catch (...) { + akError() << "Unknown exception caught: " << akBacktrace(); + if (m_currentHandler) { + m_currentHandler->failureResponse("Unknown exception caught"); + } + } + if (m_reportTime) { + stopTime(currentCommand); + } + delete m_currentHandler; + m_currentHandler = 0; + } + + // reset, arm the timer + m_idleTimer->start(IDLE_TIMER_TIMEOUT); +} + +CommandContext *Connection::context() const +{ + return const_cast(&m_context); +} + +Handler *Connection::findHandlerForCommand(Protocol::Command::Type command) +{ + Handler *handler = Handler::findHandlerForCommandAlwaysAllowed(command); + if (handler) { + return handler; + } + + switch (m_connectionState) { + case NonAuthenticated: + handler = Handler::findHandlerForCommandNonAuthenticated(command); + break; + case Authenticated: + handler = Handler::findHandlerForCommandAuthenticated(command); + break; + case Selected: + break; + case LoggingOut: + break; + } + + return handler; +} + +void Connection::slotConnectionStateChange(ConnectionState state) +{ + if (state == m_connectionState) { + return; + } + m_connectionState = state; + switch (m_connectionState) { + case NonAuthenticated: + assert(0); // can't happen, it's only the initial state, we can't go back to it + break; + case Authenticated: + break; + case Selected: + break; + case LoggingOut: + m_socket->disconnectFromServer(); + break; + } +} + +void Connection::setSessionId(const QByteArray &id) +{ + m_identifier.sprintf("%s (%p)", id.data(), static_cast(this)); + Tracer::self()->beginConnection(m_identifier, QString()); + //m_streamParser->setTracerIdentifier(m_identifier); + + m_sessionId = id; + setObjectName(QString::fromLatin1(id)); + storageBackend()->setSessionId(id); + storageBackend()->notificationCollector()->setSessionId(id); +} + +QByteArray Connection::sessionId() const +{ + return m_sessionId; +} + +void Connection::setIsNotificationBus(bool on) +{ + if (m_isNotificationBus == on) { + return; + } + + m_isNotificationBus = on; + if (m_isNotificationBus) { + qDebug() << "New notification bus:" << m_sessionId; + NotificationManager::self()->registerConnection(this); + } else { + NotificationManager::self()->unregisterConnection(this); + } +} + +bool Connection::isNotificationBus() const +{ + return m_isNotificationBus; +} + + + +bool Connection::isOwnerResource(const PimItem &item) const +{ + if (context()->resource().isValid() && item.collection().resourceId() == context()->resource().id()) { + return true; + } + // fallback for older resources + if (sessionId() == item.collection().resource().name().toUtf8()) { + return true; + } + return false; +} + +bool Connection::isOwnerResource(const Collection &collection) const +{ + if (context()->resource().isValid() && collection.resourceId() == context()->resource().id()) { + return true; + } + if (sessionId() == collection.resource().name().toUtf8()) { + return true; + } + return false; +} + +bool Connection::verifyCacheOnRetrieval() const +{ + return m_verifyCacheOnRetrieval; +} + +void Connection::startTime() +{ + m_time.start(); +} + +void Connection::stopTime(const QString &identifier) +{ + int elapsed = m_time.elapsed(); + m_totalTime += elapsed; + m_totalTimeByHandler[identifier] += elapsed; + m_executionsByHandler[identifier]++; + qCDebug(AKONADISERVER_LOG) << identifier <<" time : " << elapsed << " total: " << m_totalTime; +} + +void Connection::reportTime() const +{ + qCDebug(AKONADISERVER_LOG) << "===== Time report for " << m_identifier << " ====="; + qCDebug(AKONADISERVER_LOG) << " total: " << m_totalTime; + for (auto it = m_totalTimeByHandler.cbegin(), end = m_totalTimeByHandler.cend(); it != end; ++it) { + const QString &handler = it.key(); + qCDebug(AKONADISERVER_LOG) << "handler : " << handler << " time: " << m_totalTimeByHandler.value(handler) << " executions " << m_executionsByHandler.value(handler) << " avg: " << m_totalTimeByHandler.value(handler)/m_executionsByHandler.value(handler); + } +} + +void Connection::sendResponse(qint64 tag, const Protocol::Command &response) +{ + // Notifications have their own debugging system + if (!m_isNotificationBus) { + if (Tracer::self()->currentTracer() != QLatin1String("null")) { + Tracer::self()->connectionOutput(m_identifier, QByteArray::number(tag) + ' ' + response.debugString().toUtf8()); + } + } + QDataStream stream(m_socket); + stream << tag; + Protocol::serialize(m_socket, response); +} + +void Connection::sendResponse(const Protocol::Command &response) +{ + if (m_isNotificationBus) { + // FIXME: Don't hardcode the tag for notifications + sendResponse(4, response); + } else { + Q_ASSERT(m_currentHandler); + sendResponse(m_currentHandler->tag(), response); + } +} + +Protocol::Command Connection::readCommand() +{ + while (m_socket->bytesAvailable() < (int) sizeof(qint64)) { + if (m_socket->state() == QLocalSocket::UnconnectedState) { + return Protocol::Command(); + } + m_socket->waitForReadyRead(500); + } + + QDataStream stream(m_socket); + qint64 tag; + stream >> tag; + + // TODO: compare tag with m_currentHandler->tag() ? + return Protocol::deserialize(m_socket); +} diff --git a/src/server/connection.h b/src/server/connection.h new file mode 100644 index 0000000..cb17894 --- /dev/null +++ b/src/server/connection.h @@ -0,0 +1,138 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADI_CONNECTION_H +#define AKONADI_CONNECTION_H + +#include +#include +#include +#include +#include + +#include "akthread.h" +#include "entities.h" +#include "global.h" +#include "commandcontext.h" + +#include + +namespace Akonadi { +namespace Server { + +class Handler; +class Response; +class DataStore; +class Collection; +class CollectionReferenceManager; + +/** + An Connection represents one connection of a client to the server. +*/ +class Connection : public AkThread +{ + Q_OBJECT +public: + Connection(quintptr socketDescriptor, QObject *parent = 0); + virtual ~Connection(); + + virtual DataStore *storageBackend(); + + CollectionReferenceManager *collectionReferenceManager(); + + CommandContext *context() const; + + /** + Returns @c true if this connection belongs to the owning resource of @p item. + */ + bool isOwnerResource(const PimItem &item) const; + bool isOwnerResource(const Collection &collection) const; + + void setSessionId(const QByteArray &id); + QByteArray sessionId() const; + + void setIsNotificationBus(bool on); + bool isNotificationBus() const; + + /** Returns @c true if permanent cache verification is enabled. */ + bool verifyCacheOnRetrieval() const; + + + Protocol::Command readCommand(); + +public Q_SLOTS: + virtual void sendResponse(const Protocol::Command &response); + +Q_SIGNALS: + void disconnected(); + +protected Q_SLOTS: + /** + * New data arrived from the client. Creates a handler for it and passes the data to the handler. + */ + void slotNewData(); + void slotConnectionStateChange(ConnectionState state); + void slotConnectionIdle(); + + void slotSendHello(); + +protected: + Connection(QObject *parent = 0); // used for testing + + virtual void init() Q_DECL_OVERRIDE; + virtual void quit() Q_DECL_OVERRIDE; + + virtual Handler *findHandlerForCommand(Protocol::Command::Type cmd); + +protected: + quintptr m_socketDescriptor; + QLocalSocket *m_socket; + QPointer m_currentHandler; + ConnectionState m_connectionState; + bool m_isNotificationBus; + mutable DataStore *m_backend; + QList m_statusMessageQueue; + QString m_identifier; + QByteArray m_sessionId; + bool m_verifyCacheOnRetrieval; + CommandContext m_context; + QTimer *m_idleTimer; + + QTime m_time; + qint64 m_totalTime; + QHash m_totalTimeByHandler; + QHash m_executionsByHandler; + +private: + void sendResponse(qint64 tag, const Protocol::Command &response); + + /** For debugging */ + void startTime(); + void stopTime(const QString &identifier); + void reportTime() const; + bool m_reportTime; + +}; + +} // namespace Server +} // namespace Akonadi + + + +#endif diff --git a/src/server/dbusconnectionpool.cpp b/src/server/dbusconnectionpool.cpp new file mode 100644 index 0000000..dbabc65 --- /dev/null +++ b/src/server/dbusconnectionpool.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 Sebastian Trueg + * + * 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 "dbusconnectionpool.h" +#include +#include +#include + +namespace { +QAtomicInt s_connectionCounter; + +class DBusConnectionPoolPrivate +{ +public: + DBusConnectionPoolPrivate() + : m_connection(QDBusConnection::connectToBus( + QDBusConnection::SessionBus, + QStringLiteral("AkonadiServer-%1").arg(newNumber()))) + { + } + ~DBusConnectionPoolPrivate() { + QDBusConnection::disconnectFromBus(m_connection.name()); + } + + QDBusConnection connection() const { + return m_connection; + } + +private: + static int newNumber() { + return s_connectionCounter.fetchAndAddAcquire(1); + } + QDBusConnection m_connection; +}; +} + +QThreadStorage s_perThreadConnection; + +QDBusConnection Akonadi::Server::DBusConnectionPool::threadConnection() +{ + if (!QCoreApplication::instance() || QCoreApplication::instance()->thread() == QThread::currentThread()) { + return QDBusConnection::sessionBus(); // main thread, use the default session bus + } + if (!s_perThreadConnection.hasLocalData()) { + s_perThreadConnection.setLocalData(new DBusConnectionPoolPrivate); + } + return s_perThreadConnection.localData()->connection(); +} diff --git a/src/server/dbusconnectionpool.h b/src/server/dbusconnectionpool.h new file mode 100644 index 0000000..4f8a93e --- /dev/null +++ b/src/server/dbusconnectionpool.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 Sebastian Trueg + * + * 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 DBUSCONNECTIONPOOL_H +#define DBUSCONNECTIONPOOL_H + +#include + +namespace Akonadi { +namespace Server { +namespace DBusConnectionPool { + +/** + * Returns a new QDBusConnection for each thread, because QDBusConnection is + * not thread-safe in Qt 4. + * + * FIXME: Remove in KF5 + */ +QDBusConnection threadConnection(); + +} +} +} + +#endif diff --git a/src/server/dbustracer.cpp b/src/server/dbustracer.cpp new file mode 100644 index 0000000..d05658a --- /dev/null +++ b/src/server/dbustracer.cpp @@ -0,0 +1,70 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "dbustracer.h" +#include "tracernotificationadaptor.h" + +using namespace Akonadi::Server; + +DBusTracer::DBusTracer() + : QObject(0) +{ + new TracerNotificationAdaptor(this); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/tracing/notifications"), this, QDBusConnection::ExportAdaptors); +} + +DBusTracer::~DBusTracer() +{ +} + +void DBusTracer::beginConnection(const QString &identifier, const QString &msg) +{ + Q_EMIT connectionStarted(identifier, msg); +} + +void DBusTracer::endConnection(const QString &identifier, const QString &msg) +{ + Q_EMIT connectionEnded(identifier, msg); +} + +void DBusTracer::connectionInput(const QString &identifier, const QByteArray &msg) +{ + Q_EMIT connectionDataInput(identifier, QString::fromUtf8(msg)); +} + +void DBusTracer::connectionOutput(const QString &identifier, const QByteArray &msg) +{ + Q_EMIT connectionDataOutput(identifier, QString::fromUtf8(msg)); +} + +void DBusTracer::signal(const QString &signalName, const QString &msg) +{ + Q_EMIT signalEmitted(signalName, msg); +} + +void DBusTracer::warning(const QString &componentName, const QString &msg) +{ + Q_EMIT warningEmitted(componentName, msg); +} + +void DBusTracer::error(const QString &componentName, const QString &msg) +{ + Q_EMIT errorEmitted(componentName, msg); +} diff --git a/src/server/dbustracer.h b/src/server/dbustracer.h new file mode 100644 index 0000000..4e5b546 --- /dev/null +++ b/src/server/dbustracer.h @@ -0,0 +1,62 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADI_DBUSTRACER_H +#define AKONADI_DBUSTRACER_H + +#include + +#include "tracerinterface.h" + +namespace Akonadi { +namespace Server { + +/** + * A tracer which forwards all tracing information as dbus signals. + */ +class DBusTracer : public QObject, public TracerInterface +{ + Q_OBJECT + +public: + DBusTracer(); + virtual ~DBusTracer(); + + virtual void beginConnection(const QString &identifier, const QString &msg); + virtual void endConnection(const QString &identifier, const QString &msg); + virtual void connectionInput(const QString &identifier, const QByteArray &msg); + virtual void connectionOutput(const QString &identifier, const QByteArray &msg); + virtual void signal(const QString &signalName, const QString &msg); + virtual void warning(const QString &componentName, const QString &msg); + virtual void error(const QString &componentName, const QString &msg); + +Q_SIGNALS: + void connectionStarted(const QString &identifier, const QString &msg); + void connectionEnded(const QString &identifier, const QString &msg); + void connectionDataInput(const QString &identifier, const QString &msg); + void connectionDataOutput(const QString &identifier, const QString &msg); + void signalEmitted(const QString &signalName, const QString &msg); + void warningEmitted(const QString &componentName, const QString &msg); + void errorEmitted(const QString &componentName, const QString &msg); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/debuginterface.cpp b/src/server/debuginterface.cpp new file mode 100644 index 0000000..edb1ce6 --- /dev/null +++ b/src/server/debuginterface.cpp @@ -0,0 +1,43 @@ +/* + 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 "debuginterface.h" +#include "debuginterfaceadaptor.h" +#include "tracer.h" +#include + +using namespace Akonadi::Server; + +DebugInterface::DebugInterface(QObject *parent) + : QObject(parent) +{ + new DebugInterfaceAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/debug"), + this, QDBusConnection::ExportAdaptors); +} + +QString DebugInterface::tracer() const +{ + return Tracer::self()->currentTracer(); +} + +void DebugInterface::setTracer(const QString &tracer) +{ + Tracer::self()->activateTracer(tracer); +} diff --git a/src/server/debuginterface.h b/src/server/debuginterface.h new file mode 100644 index 0000000..61e46a8 --- /dev/null +++ b/src/server/debuginterface.h @@ -0,0 +1,48 @@ +/* + 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_DEBUGINTERFACE_H +#define AKONADI_DEBUGINTERFACE_H + +#include + +namespace Akonadi { +namespace Server { + +/** + * Interface to configure and query debugging options. + */ +class DebugInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.DebugInterface") + +public: + explicit DebugInterface(QObject *parent = 0); + +public Q_SLOTS: + Q_SCRIPTABLE QString tracer() const; + Q_SCRIPTABLE void setTracer(const QString &tracer); + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/exception.h b/src/server/exception.h new file mode 100644 index 0000000..ae37a5c --- /dev/null +++ b/src/server/exception.h @@ -0,0 +1,99 @@ +/* + 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. +*/ + +#ifndef AKONADI_EXCEPTION_H +#define AKONADI_EXCEPTION_H + +#include +#include +#include + +namespace Akonadi { +namespace Server { + +/** + Base class for exception used internally by the Akonadi server. +*/ +class Exception : public std::exception +{ +public: + Exception(const char *what) throw() + : mWhat(what) + { + } + + Exception(const QByteArray &what) throw() + : mWhat(what) + { + } + + Exception(const QString &what) throw() + : mWhat(what.toUtf8()) + { + } + + Exception(const Exception &other) throw() + : std::exception(other) + , mWhat(other.what()) + { + } + + virtual ~Exception() throw() + { + } + + const char *what() const throw() + { + return mWhat.constData(); + } + + virtual const char *type() const throw() + { + return "General Exception"; + } +protected: + QByteArray mWhat; +}; + +#define AKONADI_EXCEPTION_MAKE_INSTANCE( classname ) \ +class classname : public Akonadi::Server::Exception \ +{ \ +public: \ + classname ( const char *what ) throw() \ + : Akonadi::Server::Exception( what ) \ + { \ + } \ + classname ( const QByteArray &what ) throw() \ + : Akonadi::Server::Exception( what ) \ + { \ + } \ + classname ( const QString &what ) throw() \ + : Akonadi::Server::Exception( what ) \ + { \ + } \ + const char *type() const throw() \ + { \ + return "" #classname; \ + } \ +} + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/filetracer.cpp b/src/server/filetracer.cpp new file mode 100644 index 0000000..cd58047 --- /dev/null +++ b/src/server/filetracer.cpp @@ -0,0 +1,76 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#include "filetracer.h" + +#include +#include + +using namespace Akonadi::Server; + +FileTracer::FileTracer(const QString &fileName) +{ + m_file = new QFile(fileName); + m_file->open(QIODevice::WriteOnly | QIODevice::Unbuffered); +} + +FileTracer::~FileTracer() +{ + delete m_file; +} + +void FileTracer::beginConnection(const QString &identifier, const QString &msg) +{ + output(identifier, QStringLiteral("begin_connection: %1").arg(msg)); +} + +void FileTracer::endConnection(const QString &identifier, const QString &msg) +{ + output(identifier, QStringLiteral("end_connection: %1").arg(msg)); +} + +void FileTracer::connectionInput(const QString &identifier, const QByteArray &msg) +{ + output(identifier, QStringLiteral("input: %1").arg(QString::fromUtf8(msg))); +} + +void FileTracer::connectionOutput(const QString &identifier, const QByteArray &msg) +{ + output(identifier, QStringLiteral("output: %1").arg(QString::fromUtf8(msg))); +} + +void FileTracer::signal(const QString &signalName, const QString &msg) +{ + output(QStringLiteral("signal"), QStringLiteral("<%1> %2").arg(signalName, msg)); +} + +void FileTracer::warning(const QString &componentName, const QString &msg) +{ + output(QStringLiteral("warning"), QStringLiteral("<%1> %2").arg(componentName, msg)); +} + +void FileTracer::error(const QString &componentName, const QString &msg) +{ + output(QStringLiteral("error"), QStringLiteral("<%1> %2").arg(componentName, msg)); +} + +void FileTracer::output(const QString &id, const QString &msg) +{ + QString output = QStringLiteral("%1: %2: %3\r\n").arg(QTime::currentTime().toString(QStringLiteral("HH:mm:ss.zzz")), id, msg.left(msg.indexOf(QLatin1String("\n")))); + m_file->write(output.toUtf8()); +} diff --git a/src/server/filetracer.h b/src/server/filetracer.h new file mode 100644 index 0000000..86ee05b --- /dev/null +++ b/src/server/filetracer.h @@ -0,0 +1,57 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADI_FILETRACER_H +#define AKONADI_FILETRACER_H + +#include "tracerinterface.h" + +class QFile; + +namespace Akonadi { +namespace Server { + +/** + * A tracer which forwards all tracing information to a + * log file. + */ +class FileTracer : public TracerInterface +{ +public: + FileTracer(const QString &fileName); + virtual ~FileTracer(); + + virtual void beginConnection(const QString &identifier, const QString &msg); + virtual void endConnection(const QString &identifier, const QString &msg); + virtual void connectionInput(const QString &identifier, const QByteArray &msg); + virtual void connectionOutput(const QString &identifier, const QByteArray &msg); + virtual void signal(const QString &signalName, const QString &msg); + virtual void warning(const QString &componentName, const QString &msg); + virtual void error(const QString &componentName, const QString &msg); + +private: + void output(const QString &id, const QString &msg); + + QFile *m_file; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/global.h b/src/server/global.h new file mode 100644 index 0000000..2a1438a --- /dev/null +++ b/src/server/global.h @@ -0,0 +1,38 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef GLOBAL_H +#define GLOBAL_H + +namespace Akonadi { +namespace Server { + +// rfc1730 section 3 +/** The state of the client +*/ +enum ConnectionState { + NonAuthenticated, ///< Not yet authenticated + Authenticated, ///< The client is authenticated + Selected, + LoggingOut +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler.cpp b/src/server/handler.cpp new file mode 100644 index 0000000..e0f0896 --- /dev/null +++ b/src/server/handler.cpp @@ -0,0 +1,253 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#include "handler.h" + +#include +#include + +#include "connection.h" + +#include "handler/akappend.h" +#include "handler/copy.h" +#include "handler/colcopy.h" +#include "handler/colmove.h" +#include "handler/create.h" +#include "handler/delete.h" +#include "handler/fetch.h" +#include "handler/link.h" +#include "handler/list.h" +#include "handler/login.h" +#include "handler/logout.h" +#include "handler/modify.h" +#include "handler/move.h" +#include "handler/remove.h" +#include "handler/resourceselect.h" +#include "handler/search.h" +#include "handler/searchpersistent.h" +#include "handler/searchresult.h" +#include "handler/status.h" +#include "handler/store.h" +#include "handler/transaction.h" +#include "handler/tagappend.h" +#include "handler/tagfetch.h" +#include "handler/tagremove.h" +#include "handler/tagstore.h" +#include "handler/relationstore.h" +#include "handler/relationremove.h" +#include "handler/relationfetch.h" + +#include "storage/querybuilder.h" + + +using namespace Akonadi; +using namespace Akonadi::Server; + +Handler::Handler() + : QObject() + , m_connection(0) +{ +} + +Handler::~Handler() +{ +} + +Handler *Handler::findHandlerForCommandNonAuthenticated(Protocol::Command::Type cmd) +{ + // allowed are LOGIN + if (cmd == Protocol::Command::Login) { + return new Login(); + } + + return Q_NULLPTR; +} + +Handler *Handler::findHandlerForCommandAlwaysAllowed(Protocol::Command::Type cmd) +{ + // allowed is LOGOUT + if (cmd == Protocol::Command::Logout) { + return new Logout(); + } + return Q_NULLPTR; +} + +void Handler::setTag(quint64 tag) +{ + m_tag = tag; +} + +quint64 Handler::tag() const +{ + return m_tag; +} + +void Handler::setCommand(const Protocol::Command &cmd) +{ + m_command = cmd; +} + +Protocol::Command Handler::command() const +{ + return m_command; +} + + +Handler *Handler::findHandlerForCommandAuthenticated(Protocol::Command::Type cmd) +{ + switch (cmd) + { + case Protocol::Command::Invalid: + Q_ASSERT_X(cmd != Protocol::Command::Invalid, + "Handler::findHandlerForCommandAuthenticated()", + "Invalid command is not allowed"); + return Q_NULLPTR; + case Protocol::Command::Hello: + Q_ASSERT_X(cmd != Protocol::Command::Hello, + "Handler::findHandlerForCommandAuthenticated()", + "Hello command is not allowed in this context"); + return Q_NULLPTR; + case Protocol::Command::Login: + Q_ASSERT_X(cmd != Protocol::Command::StreamPayload, + "Handler::findHandlerForCommandAuthenticated()", + "Login command is not allowed in this context"); + return Q_NULLPTR; + case Protocol::Command::Logout: + Q_ASSERT_X(cmd != Protocol::Command::StreamPayload, + "Handler::findHandlerForCommandAuthenticated()", + "Logout command is not allowed in this context"); + return Q_NULLPTR; + case Protocol::Command::_ResponseBit: + Q_ASSERT_X(cmd != Protocol::Command::_ResponseBit, + "Handler::findHandlerForCommandAuthenticated()", + "ResponseBit is not a valid command type"); + return Q_NULLPTR; + + case Protocol::Command::Transaction: + return new TransactionHandler(); + + case Protocol::Command::CreateItem: + return new AkAppend(); + case Protocol::Command::CopyItems: + return new Copy(); + case Protocol::Command::DeleteItems: + return new Remove(); + case Protocol::Command::FetchItems: + return new Fetch(); + case Protocol::Command::LinkItems: + return new Link(); + case Protocol::Command::ModifyItems: + return new Store(); + case Protocol::Command::MoveItems: + return new Move(); + + case Protocol::Command::CreateCollection: + return new Create(); + case Protocol::Command::CopyCollection: + return new ColCopy(); + case Protocol::Command::DeleteCollection: + return new Delete(); + case Protocol::Command::FetchCollections: + return new List(); + case Protocol::Command::FetchCollectionStats: + return new Status(); + case Protocol::Command::ModifyCollection: + return new Modify(); + case Protocol::Command::MoveCollection: + return new ColMove(); + + case Protocol::Command::Search: + return new Search(); + case Protocol::Command::SearchResult: + return new SearchResult(); + case Protocol::Command::StoreSearch: + return new SearchPersistent(); + + case Protocol::Command::CreateTag: + return new TagAppend(); + case Protocol::Command::DeleteTag: + return new TagRemove(); + case Protocol::Command::FetchTags: + return new TagFetch(); + case Protocol::Command::ModifyTag: + return new TagStore(); + + case Protocol::Command::FetchRelations: + return new RelationFetch(); + case Protocol::Command::ModifyRelation: + return new RelationStore(); + case Protocol::Command::RemoveRelations: + return new RelationRemove(); + + case Protocol::Command::SelectResource: + return new ResourceSelect(); + + case Protocol::Command::StreamPayload: + Q_ASSERT_X(cmd != Protocol::Command::StreamPayload, + "Handler::findHandlerForCommandAuthenticated()", + "StreamPayload command is not allowed in this context"); + return Q_NULLPTR; + case Protocol::Command::ChangeNotification: + Q_ASSERT_X(cmd != Protocol::Command::ChangeNotification, + "Handler::findHandlerForCommandNonAuthenticated()", + "ChangeNotification command is not allowed in this context"); + } + + return Q_NULLPTR; +} + +void Handler::setConnection(Connection *connection) +{ + m_connection = connection; +} + +Connection *Handler::connection() const +{ + return m_connection; +} + +bool Handler::failureResponse(const QByteArray &failureMessage) +{ + return failureResponse(QString::fromUtf8(failureMessage)); +} + +bool Handler::failureResponse(const char *failureMessage) +{ + return failureResponse(QString::fromUtf8(failureMessage)); +} + +bool Handler::failureResponse(const QString &failureMessage) +{ + Protocol::Response r = Protocol::Factory::response(m_command.type()); + // FIXME: Error enums? + r.setError(1, failureMessage); + + sendResponse(r); + + return false; +} + +void Handler::sendResponse(const Protocol::Command &response) +{ + m_connection->sendResponse(response); +} + +bool Handler::checkScopeConstraints(const Akonadi::Scope &scope, int permittedScopes) +{ + return scope.scope() & permittedScopes; +} diff --git a/src/server/handler.h b/src/server/handler.h new file mode 100644 index 0000000..f3f3a41 --- /dev/null +++ b/src/server/handler.h @@ -0,0 +1,158 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef AKONADIHANDLER_H +#define AKONADIHANDLER_H + +#include +#include + +#include "global.h" +#include "exception.h" +#include "connection.h" + +#include + +namespace Akonadi { + +class ImapSet; + +namespace Server { + +class Response; +class Connection; +class QueryBuilder; + +AKONADI_EXCEPTION_MAKE_INSTANCE(HandlerException); + +/** + \defgroup akonadi_server_handler Command handlers + + All commands supported by the Akonadi server are implemented as sub-classes of Akonadi::Handler. +*/ + +/** +The handler interfaces describes an entity capable of handling an AkonadiIMAP command.*/ +class Handler : public QObject +{ + Q_OBJECT +public: + Handler(); + + virtual ~Handler(); + + /** + * Set the tag of the command to be processed, and thus of the response + * generated by this handler. + * @param tag The command tag, an alphanumerical string, normally. + */ + void setTag(quint64 tag); + + /** + * The tag of the command associated with this handler. + */ + quint64 tag() const; + + void setCommand(const Protocol::Command &cmd); + + Protocol::Command command() const; + + /** + * Find a handler for a command that is always allowed, like LOGOUT. + * @param line the command string + * @return an instance to the handler. The handler is deleted after @see handelLine is executed. The caller needs to delete the handler in case an exception is thrown from handelLine. + */ + static Handler *findHandlerForCommandAlwaysAllowed(Protocol::Command::Type cmd); + + /** + * Find a handler for a command that is allowed when the client is not yet authenticated, like LOGIN. + * @param line the command string + * @return an instance to the handler. The handler is deleted after @see handelLine is executed. The caller needs to delete the handler in case an exception is thrown from handelLine. + */ + static Handler *findHandlerForCommandNonAuthenticated(Protocol::Command::Type cmd); + + /** + * Find a handler for a command that is allowed when the client is authenticated, like LIST, FETCH, etc. + * @param line the command string + * @return an instance to the handler. The handler is deleted after @see handelLine is executed. The caller needs to delete the handler in case an exception is thrown from handelLine. + */ + static Handler *findHandlerForCommandAuthenticated(Protocol::Command::Type cmd); + + void setConnection(Connection *connection); + Connection *connection() const; + + bool failureResponse(const char *response); + bool failureResponse(const QByteArray &response); + bool failureResponse(const QString &response); + + template + typename std::enable_if::value, bool>::type + successResponse(const T &response = T()); + + template + typename std::enable_if::value, void>::type + sendResponse(const T&response = T()); + + + /** + * Parse and handle the IMAP message using the streaming parser. The implementation MUST leave the trailing newline character(s) in the stream! + * @return true if parsed successfully, false in case of parse failure + */ + virtual bool parseStream() = 0; + + bool checkScopeConstraints(const Scope &scope, int permittedScopes); + +public Q_SLOTS: + void sendResponse(const Protocol::Command &response); + +Q_SIGNALS: + /** + * Emitted whenever a handler wants the connection to change into a + * different state. The connection usually honors such requests, but + * the decision is up to it. + * @param state The new state the handler suggests to enter. + */ + void connectionStateChange(ConnectionState state); + +private: + quint64 m_tag; + Connection *m_connection; + +protected: + Protocol::Command m_command; +}; + +template +typename std::enable_if::value, bool>::type +Handler::successResponse(const T &response) +{ + sendResponse(response); + return true; +} + +template +typename std::enable_if::value, void>::type +Handler::sendResponse(const T &response) +{ + sendResponse(static_cast(response)); +} + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/akappend.cpp b/src/server/handler/akappend.cpp new file mode 100644 index 0000000..eaae5b5 --- /dev/null +++ b/src/server/handler/akappend.cpp @@ -0,0 +1,423 @@ +/*************************************************************************** + * Copyright (C) 2007 by Robert Zwerus * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "akappend.h" + +#include "fetchhelper.h" +#include "connection.h" +#include "preprocessormanager.h" +#include "handlerhelper.h" +#include "storage/datastore.h" +#include "storage/transaction.h" +#include "storage/parttypehelper.h" +#include "storage/dbconfig.h" +#include "storage/partstreamer.h" +#include "storage/parthelper.h" +#include "storage/selectquerybuilder.h" +#include + +#include //std::accumulate + +using namespace Akonadi; +using namespace Akonadi::Server; + +static QVector localFlagsToPreserve = QVector() << "$ATTACHMENT" + << "$INVITATION" + << "$ENCRYPTED" + << "$SIGNED" + << "$WATCHED"; + +bool AkAppend::buildPimItem(const Protocol::CreateItemCommand &cmd, PimItem &item, + Collection &parentCol) +{ + parentCol = HandlerHelper::collectionFromScope(cmd.collection(), connection()); + if (!parentCol.isValid()) { + return failureResponse("Invalid parent collection"); + } + if (parentCol.isVirtual()) { + return failureResponse("Cannot append item into virtual collection"); + } + + MimeType mimeType = MimeType::retrieveByName(cmd.mimeType()); + if (!mimeType.isValid()) { + MimeType m(cmd.mimeType()); + if (!m.insert()) { + return failureResponse(QStringLiteral("Unable to create mimetype '") % cmd.mimeType() % QStringLiteral("'.")); + } + mimeType = m; + } + + item.setRev(0); + item.setSize(cmd.itemSize()); + item.setMimeTypeId(mimeType.id()); + item.setCollectionId(parentCol.id()); + item.setDatetime(cmd.dateTime().isValid() ? cmd.dateTime() : QDateTime::currentDateTimeUtc()); + if (cmd.remoteId().isEmpty()) { + // from application + item.setDirty(true); + } else { + // from resource + item.setRemoteId(cmd.remoteId()); + item.setDirty(false); + } + item.setRemoteRevision(cmd.remoteRevision()); + item.setGid(cmd.gid()); + item.setAtime(QDateTime::currentDateTime()); + + return true; +} + +bool AkAppend::insertItem(const Protocol::CreateItemCommand &cmd, PimItem &item, + const Collection &parentCol) +{ + if (!item.insert()) { + return failureResponse("Failed to append item"); + } + + // set message flags + const QSet flags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.flags() : cmd.addedFlags(); + if (!flags.isEmpty()) { + // This will hit an entry in cache inserted there in buildPimItem() + const Flag::List flagList = HandlerHelper::resolveFlags(flags); + bool flagsChanged = false; + if (!DataStore::self()->appendItemsFlags(PimItem::List() << item, flagList, &flagsChanged, false, parentCol, true)) { + return failureResponse("Unable to append item flags."); + } + } + + const Scope tags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.tags() : cmd.addedTags(); + if (!tags.isEmpty()) { + const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()); + bool tagsChanged = false; + if (!DataStore::self()->appendItemsTags(PimItem::List() << item, tagList, &tagsChanged, false, parentCol, true)) { + return failureResponse("Unable to append item tags."); + } + } + + // Handle individual parts + qint64 partSizes = 0; + PartStreamer streamer(connection(), item, this); + connect(&streamer, &PartStreamer::responseAvailable, + this, static_cast(&Handler::sendResponse)); + Q_FOREACH (const QByteArray &partName, cmd.parts()) { + qint64 partSize = 0; + if (!streamer.stream(true, partName, partSize)) { + return failureResponse(streamer.error()); + } + partSizes += partSize; + } + const Protocol::Attributes attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + if (!streamer.streamAttribute(true, iter.key(), iter.value())) { + return failureResponse(streamer.error()); + } + } + + // TODO: Try to avoid this addition query + if (partSizes > item.size()) { + item.setSize(partSizes); + item.update(); + } + + // Preprocessing + if (PreprocessorManager::instance()->isActive()) { + Part hiddenAttribute; + hiddenAttribute.setPimItemId(item.id()); + hiddenAttribute.setPartType(PartTypeHelper::fromFqName(QStringLiteral(AKONADI_ATTRIBUTE_HIDDEN))); + hiddenAttribute.setData(QByteArray()); + hiddenAttribute.setDatasize(0); + // TODO: Handle errors? Technically, this is not a critical issue as no data are lost + PartHelper::insert(&hiddenAttribute); + } + + notify(item, item.collection()); + sendResponse(item, Protocol::CreateItemCommand::None); + + return true; +} + +bool AkAppend::mergeItem(const Protocol::CreateItemCommand &cmd, + PimItem &newItem, PimItem ¤tItem, + const Collection &parentCol) +{ + QSet changedParts; + + if (newItem.rev() > 0) { + currentItem.setRev(newItem.rev()); + } + if (!newItem.remoteId().isEmpty() && currentItem.remoteId() != newItem.remoteId()) { + currentItem.setRemoteId(newItem.remoteId()); + changedParts.insert(AKONADI_PARAM_REMOTEID); + } + if (!newItem.remoteRevision().isEmpty() && currentItem.remoteRevision() != newItem.remoteRevision()) { + currentItem.setRemoteRevision(newItem.remoteRevision()); + changedParts.insert(AKONADI_PARAM_REMOTEREVISION); + } + if (!newItem.gid().isEmpty() && currentItem.gid() != newItem.gid()) { + currentItem.setGid(newItem.gid()); + changedParts.insert(AKONADI_PARAM_GID); + } + if (newItem.datetime().isValid()) { + currentItem.setDatetime(newItem.datetime()); + } + currentItem.setAtime(QDateTime::currentDateTime()); + currentItem.setSize(newItem.size()); + + // Only mark dirty when merged from application + currentItem.setDirty(!connection()->context()->resource().isValid()); + + const Collection col = Collection::retrieveById(parentCol.id()); + if (cmd.flags().isEmpty()) { + bool flagsAdded = false, flagsRemoved = false; + if (!cmd.addedFlags().isEmpty()) { + const Flag::List addedFlags = HandlerHelper::resolveFlags(cmd.addedFlags()); + DataStore::self()->appendItemsFlags(PimItem::List() << currentItem, addedFlags, + &flagsAdded, true, col, true); + } + if (!cmd.removedFlags().isEmpty()) { + const Flag::List removedFlags = HandlerHelper::resolveFlags(cmd.removedFlags()); + DataStore::self()->removeItemsFlags(PimItem::List() << currentItem, removedFlags, + &flagsRemoved, col, true); + } + if (flagsAdded || flagsRemoved) { + changedParts.insert(AKONADI_PARAM_FLAGS); + } + } else if (!cmd.flags().isEmpty()) { + bool flagsChanged = false; + QSet flagNames = cmd.flags(); + + // Make sure we don't overwrite some local-only flags that can't come + // through from Resource during ItemSync, like $ATTACHMENT, because the + // resource is not aware of them (they are usually assigned by client + // upon inspecting the payload) + Q_FOREACH (const Flag ¤tFlag, currentItem.flags()) { + const QByteArray currentFlagName = currentFlag.name().toLatin1(); + if (localFlagsToPreserve.contains(currentFlagName)) { + flagNames.insert(currentFlagName); + } + } + const Flag::List flags = HandlerHelper::resolveFlags(flagNames); + DataStore::self()->setItemsFlags(PimItem::List() << currentItem, flags, + &flagsChanged, col, true); + if (flagsChanged) { + changedParts.insert(AKONADI_PARAM_FLAGS); + } + } + + if (cmd.tags().isEmpty()) { + bool tagsAdded = false, tagsRemoved = false; + if (!cmd.addedTags().isEmpty()) { + const Tag::List addedTags = HandlerHelper::tagsFromScope(cmd.addedTags(), connection()); + DataStore::self()->appendItemsTags(PimItem::List() << currentItem, addedTags, + &tagsAdded, true, col, true); + } + if (!cmd.removedTags().isEmpty()) { + const Tag::List removedTags = HandlerHelper::tagsFromScope(cmd.removedTags(), connection()); + DataStore::self()->removeItemsTags(PimItem::List() << currentItem, removedTags, + &tagsRemoved, true); + } + + if (tagsAdded || tagsRemoved) { + changedParts.insert(AKONADI_PARAM_TAGS); + } + } else if (!cmd.tags().isEmpty()) { + bool tagsChanged = false; + const Tag::List tags = HandlerHelper::tagsFromScope(cmd.tags(), connection()); + DataStore::self()->setItemsTags(PimItem::List() << currentItem, tags, + &tagsChanged, true); + if (tagsChanged) { + changedParts.insert(AKONADI_PARAM_TAGS); + } + } + + const Part::List existingParts = Part::retrieveFiltered(Part::pimItemIdColumn(), currentItem.id()); + QMap partsSizes; + Q_FOREACH (const Part &part, existingParts ) { + partsSizes.insert(PartTypeHelper::fullName(part.partType()).toLatin1(), part.datasize()); + } + + PartStreamer streamer(connection(), currentItem); + connect(&streamer, &PartStreamer::responseAvailable, + this, static_cast(&Handler::sendResponse)); + Q_FOREACH (const QByteArray &partName, cmd.parts()) { + bool changed = false; + qint64 partSize = 0; + if (!streamer.stream(true, partName, partSize, &changed)) { + return failureResponse(streamer.error()); + } + + if (changed) { + changedParts.insert(partName); + partsSizes.insert(partName, partSize); + } + } + + const qint64 size = std::accumulate(partsSizes.begin(), partsSizes.end(), 0); + if (size > currentItem.size()) { + currentItem.setSize(size); + } + + // Store all changes + if (!currentItem.update()) { + return failureResponse("Failed to store merged item"); + } + + + notify(currentItem, currentItem.collection(), changedParts); + sendResponse(currentItem, cmd.mergeModes()); + + return true; +} + +bool AkAppend::sendResponse(const PimItem& item, Protocol::CreateItemCommand::MergeModes mergeModes) +{ + if (mergeModes & Protocol::CreateItemCommand::Silent || mergeModes & Protocol::CreateItemCommand::None) { + Protocol::FetchItemsResponse resp(item.id()); + resp.setMTime(item.datetime()); + Handler::sendResponse(resp); + return true; + } + + Protocol::FetchScope fetchScope; + fetchScope.setAncestorDepth(Protocol::Ancestor::ParentAncestor); + fetchScope.setFetch(Protocol::FetchScope::AllAttributes | + Protocol::FetchScope::FullPayload | + Protocol::FetchScope::CacheOnly | + Protocol::FetchScope::Flags | + Protocol::FetchScope::GID | + Protocol::FetchScope::MTime | + Protocol::FetchScope::RemoteID | + Protocol::FetchScope::RemoteRevision | + Protocol::FetchScope::Size | + Protocol::FetchScope::Tags); + fetchScope.setTagFetchScope({ "GID" }); + + ImapSet set; + set.add(QVector() << item.id()); + Scope scope; + scope.setUidSet(set); + + FetchHelper fetchHelper(connection(), scope, fetchScope); + if (!fetchHelper.fetchItems()) { + return failureResponse("Failed to retrieve item"); + } + + return true; +} + + +bool AkAppend::notify(const PimItem &item, const Collection &collection) +{ + DataStore::self()->notificationCollector()->itemAdded(item, collection); + + if (PreprocessorManager::instance()->isActive()) { + // enqueue the item for preprocessing + PreprocessorManager::instance()->beginHandleItem(item, DataStore::self()); + } + return true; +} + +bool AkAppend::notify(const PimItem &item, const Collection &collection, + const QSet &changedParts) +{ + if (!changedParts.isEmpty()) { + DataStore::self()->notificationCollector()->itemChanged(item, changedParts, collection); + } + return true; +} + + +bool AkAppend::parseStream() +{ + Protocol::CreateItemCommand cmd(m_command); + + // FIXME: The streaming/reading of all item parts can hold the transaction for + // unnecessary long time -> should we wrap the PimItem into one transaction + // and try to insert Parts independently? In case we fail to insert a part, + // it's not a problem as it can be re-fetched at any time, except for attributes. + DataStore *db = DataStore::self(); + Transaction transaction(db); + ExternalPartStorageTransaction storageTrx; + + PimItem item; + Collection parentCol; + if (!buildPimItem(cmd, item, parentCol)) { + return false; + } + + if (cmd.mergeModes() == Protocol::CreateItemCommand::None) { + if (!insertItem(cmd, item, parentCol)) { + return false; + } + if (!transaction.commit()) { + return failureResponse("Failed to commit transaction"); + } + storageTrx.commit(); + } else { + // Merging is always restricted to the same collection + SelectQueryBuilder qb; + qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, parentCol.id()); + if (cmd.mergeModes() & Protocol::CreateItemCommand::GID) { + qb.addValueCondition(PimItem::gidColumn(), Query::Equals, item.gid()); + } + if (cmd.mergeModes() & Protocol::CreateItemCommand::RemoteID) { + qb.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, item.remoteId()); + } + + if (!qb.exec()) { + return failureResponse("Failed to query database for item"); + } + + const QVector result = qb.result(); + if (result.count() == 0) { + // No item with such GID/RID exists, so call AkAppend::insert() and behave + // like if this was a new item + if (!insertItem(cmd, item, parentCol)) { + return false; + } + if (!transaction.commit()) { + return failureResponse("Failed to commit transaction"); + } + storageTrx.commit(); + + } else if (result.count() == 1) { + // Item with matching GID/RID combination exists, so merge this item into it + // and send itemChanged() + PimItem existingItem = result.at(0); + + if (!mergeItem(cmd, item, existingItem, parentCol)) { + return false; + } + if (!transaction.commit()) { + return failureResponse("Failed to commit transaction"); + } + storageTrx.commit(); + } else { + akDebug() << "Multiple merge candidates:"; + Q_FOREACH (const PimItem &item, result) { + akDebug() << "\t ID: " << item.id() << ", RID:" << item.remoteId() << ", GID:" << item.gid(); + } + // Nor GID or RID are guaranteed to be unique, so make sure we don't merge + // something we don't want + return failureResponse("Multiple merge candidates, aborting"); + } + } + + return successResponse(); +} diff --git a/src/server/handler/akappend.h b/src/server/handler/akappend.h new file mode 100644 index 0000000..a340345 --- /dev/null +++ b/src/server/handler/akappend.h @@ -0,0 +1,66 @@ +/*************************************************************************** + * Copyright (C) 2007 by Robert Zwerus * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef AKONADI_AKAPPEND_H +#define AKONADI_AKAPPEND_H + +#include "handler.h" +#include "entities.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the X-AKAPPEND command. + + This command is used to append an item with multiple parts. + + */ +class AkAppend : public Handler +{ + Q_OBJECT +public: + bool parseStream(); + +private: + bool buildPimItem(const Protocol::CreateItemCommand &cmd, + PimItem &item, + Collection &parentCollection); + + bool insertItem(const Protocol::CreateItemCommand &cmd, + PimItem &item, + const Collection &parentCollection); + + bool mergeItem(const Protocol::CreateItemCommand &cmd, + PimItem &newItem, + PimItem ¤tItem, + const Collection &parentCollection); + + bool sendResponse(const PimItem &item, Protocol::CreateItemCommand::MergeModes mergeModes); + + bool notify(const PimItem &item, const Collection &collection); + bool notify(const PimItem &item, const Collection &collection, + const QSet &changedParts); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/colcopy.cpp b/src/server/handler/colcopy.cpp new file mode 100644 index 0000000..c24f7fd --- /dev/null +++ b/src/server/handler/colcopy.cpp @@ -0,0 +1,123 @@ +/* + 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 "colcopy.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "cachecleaner.h" +#include "storage/datastore.h" +#include "storage/transaction.h" +#include "storage/itemretriever.h" +#include "storage/collectionqueryhelper.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool ColCopy::copyCollection(const Collection &source, const Collection &target) +{ + if (!CollectionQueryHelper::canBeMovedTo(source, target)) { + // We don't accept source==target, or source being an ancestor of target. + return false; + } + + // copy the source collection + Collection col = source; + col.setParentId(target.id()); + col.setResourceId(target.resourceId()); + // clear remote id and revision on inter-resource copies + if (source.resourceId() != target.resourceId()) { + col.setRemoteId(QString()); + col.setRemoteRevision(QString()); + } + + DataStore *db = connection()->storageBackend(); + if (!db->appendCollection(col)) { + return false; + } + + Q_FOREACH (const MimeType &mt, source.mimeTypes()) { + if (!col.addMimeType(mt)) { + return false; + } + } + + Q_FOREACH (const CollectionAttribute &attr, source.attributes()) { + CollectionAttribute newAttr = attr; + newAttr.setId(-1); + newAttr.setCollectionId(col.id()); + if (!newAttr.insert()) { + return false; + } + } + + // copy sub-collections + Q_FOREACH (const Collection &child, source.children()) { + if (!copyCollection(child, col)) { + return false; + } + } + + // copy items + Q_FOREACH (const PimItem &item, source.items()) { + if (!copyItem(item, col)) { + return false; + } + } + + return true; +} + +bool ColCopy::parseStream() +{ + Protocol::CopyCollectionCommand cmd(m_command); + + const Collection source = HandlerHelper::collectionFromScope(cmd.collection(), connection()); + if (!source.isValid()) { + return failureResponse("No valid source specified"); + } + + const Collection target = HandlerHelper::collectionFromScope(cmd.destination(), connection()); + if (!target.isValid()) { + return failureResponse("No valid target specified"); + } + + CacheCleanerInhibitor inhibitor; + + // retrieve all not yet cached items of the source + ItemRetriever retriever(connection()); + retriever.setCollection(source, true); + retriever.setRetrieveFullPayload(true); + if (!retriever.exec()) { + return failureResponse(retriever.lastError()); + } + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store); + + if (!copyCollection(source, target)) { + return failureResponse("Failed to copy collection"); + } + + if (!transaction.commit()) { + return failureResponse("Cannot commit transaction."); + } + + return successResponse(); +} diff --git a/src/server/handler/colcopy.h b/src/server/handler/colcopy.h new file mode 100644 index 0000000..ff5b533 --- /dev/null +++ b/src/server/handler/colcopy.h @@ -0,0 +1,60 @@ +/* + 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_COLCOPY_H +#define AKONADI_COLCOPY_H + +#include "handler/copy.h" +#include "entities.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the COLCOPY command. + + This command is used to copy a single collection into another collection, including + all sub-collections and their content. + + The copied items differ in the following points from the originals: + - new unique id + - empty remote id + - possible located in a different collection (and thus resource) + + The copied collections differ in the following points from the originals: + - new unique id + - empty remote id + - owning resource is the same as the one of the target collection + */ +class ColCopy : public Copy +{ + Q_OBJECT +public: + bool parseStream() Q_DECL_OVERRIDE; + +private: + bool copyCollection(const Collection &source, const Collection &target); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/colmove.cpp b/src/server/handler/colmove.cpp new file mode 100644 index 0000000..9d4f469 --- /dev/null +++ b/src/server/handler/colmove.cpp @@ -0,0 +1,79 @@ +/* + 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 "colmove.h" + +#include "handlerhelper.h" +#include "connection.h" +#include "cachecleaner.h" +#include "storage/datastore.h" +#include "storage/itemretriever.h" +#include "storage/transaction.h" +#include "storage/collectionqueryhelper.h" +#include "storage/selectquerybuilder.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool ColMove::parseStream() +{ + Protocol::MoveCollectionCommand cmd(m_command); + + Collection source = HandlerHelper::collectionFromScope(cmd.collection(), connection()); + if (!source.isValid()) { + return failureResponse("Invalid collection to move"); + } + + Collection target; + if (cmd.destination().isEmpty()) { + target.setId(0); + } else { + target = HandlerHelper::collectionFromScope(cmd.destination(), connection()); + if (!target.isValid()) { + return failureResponse("Invalid destination collection"); + } + } + + if (source.parentId() == target.id()) { + return successResponse(); + } + + CacheCleanerInhibitor inhibitor; + + // retrieve all not yet cached items of the source + ItemRetriever retriever(connection()); + retriever.setCollection(source, true); + retriever.setRetrieveFullPayload(true); + if (!retriever.exec()) { + return failureResponse(retriever.lastError()); + } + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store); + + if (!store->moveCollection(source, target)) { + return failureResponse("Unable to reparent collection"); + } + + if (!transaction.commit()) { + return failureResponse("Cannot commit transaction."); + } + + return successResponse(); +} diff --git a/src/server/handler/colmove.h b/src/server/handler/colmove.h new file mode 100644 index 0000000..a5cad4c --- /dev/null +++ b/src/server/handler/colmove.h @@ -0,0 +1,46 @@ +/* + 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. +*/ + +#ifndef AKONADI_COLMOVE_H +#define AKONADI_COLMOVE_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the COLMOVE command. + + This command is used to move a set of collections into another collection, including + all sub-collections and their content. +*/ +class ColMove : public Handler +{ + Q_OBJECT +public: + bool parseStream() Q_DECL_OVERRIDE; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/copy.cpp b/src/server/handler/copy.cpp new file mode 100644 index 0000000..7f4747c --- /dev/null +++ b/src/server/handler/copy.cpp @@ -0,0 +1,122 @@ +/* + 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 "copy.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "cachecleaner.h" +#include "storage/datastore.h" +#include "storage/itemqueryhelper.h" +#include "storage/itemretriever.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" +#include "storage/parthelper.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Copy::copyItem(const PimItem &item, const Collection &target) +{ +// akDebug() << "Copy::copyItem"; + + PimItem newItem = item; + newItem.setId(-1); + newItem.setRev(0); + newItem.setDatetime(QDateTime::currentDateTime()); + newItem.setAtime(QDateTime::currentDateTime()); + newItem.setRemoteId(QString()); + newItem.setRemoteRevision(QString()); + newItem.setCollectionId(target.id()); + Part::List parts; + parts.reserve(item.parts().count()); + Q_FOREACH (const Part &part, item.parts()) { + Part newPart(part); + newPart.setData(PartHelper::translateData(newPart.data(), part.external())); + newPart.setPimItemId(-1); + parts << newPart; + } + + DataStore *store = connection()->storageBackend(); + if (!store->appendPimItem(parts, item.mimeType(), target, QDateTime::currentDateTime(), QString(), QString(), item.gid(), newItem)) { + return false; + } + Q_FOREACH (const Flag &flag, item.flags()) { + if (!newItem.addFlag(flag)) { + return false; + } + } + return true; +} + +bool Copy::parseStream() +{ + Protocol::CopyItemsCommand cmd(m_command); + + if (!checkScopeConstraints(cmd.items(), Scope::Uid)) { + return failureResponse("Only UID copy is allowed"); + } + + if (cmd.items().isEmpty()) { + return failureResponse("No items specified"); + } + + CacheCleanerInhibitor inhibitor; + + ItemRetriever retriever(connection()); + retriever.setItemSet(cmd.items().uidSet()); + retriever.setRetrieveFullPayload(true); + if (!retriever.exec()) { + return failureResponse(retriever.lastError()); + } + + const Collection targetCollection = HandlerHelper::collectionFromScope(cmd.destination(), connection()); + if (!targetCollection.isValid()) { + return failureResponse("No valid target specified"); + } + if (targetCollection.isVirtual()) { + return failureResponse("Copying items into virtual collections is not allowed"); + } + + SelectQueryBuilder qb; + ItemQueryHelper::itemSetToQuery(cmd.items().uidSet(), qb); + if (!qb.exec()) { + return failureResponse("Unable to retrieve items"); + } + PimItem::List items = qb.result(); + qb.query().finish(); + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store); + + Q_FOREACH (const PimItem &item, items) { + if (!copyItem(item, targetCollection)) { + return failureResponse("Unable to copy item"); + } + } + + if (!transaction.commit()) { + return failureResponse("Cannot commit transaction."); + } + + return successResponse(); + return true; +} diff --git a/src/server/handler/copy.h b/src/server/handler/copy.h new file mode 100644 index 0000000..7a637c1 --- /dev/null +++ b/src/server/handler/copy.h @@ -0,0 +1,70 @@ +/* + 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_COPY_H +#define AKONADI_COPY_H + +#include "handler.h" +#include "entities.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the COPY command. + + This command is used to copy a set of items into the specific collection. It + is syntactically identical to the IMAP COPY command. + + The copied items differ in the following points from the originals: + - new unique id + - empty remote id + - possible located in a different collection (and thus resource) + +

Syntax

+ + Request: + @verbatim + request = tag " COPY " sequence-set " " collection-id + @endverbatim + + There is only the usual status response indicating success or failure of the + COPY command + */ +class Copy : public Handler +{ + Q_OBJECT +public: + + bool parseStream(); + +protected: + /** + Copy the given item and all its parts into the @p target. + The changes mentioned above are applied. + */ + bool copyItem(const PimItem &item, const Collection &target); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/create.cpp b/src/server/handler/create.cpp new file mode 100644 index 0000000..7cb29d3 --- /dev/null +++ b/src/server/handler/create.cpp @@ -0,0 +1,145 @@ +/*************************************************************************** + * Copyright (C) 2006 by Ingo Kloecker * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#include "create.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "storage/datastore.h" +#include "storage/transaction.h" +#include "storage/selectquerybuilder.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Create::parseStream() +{ + Protocol::CreateCollectionCommand cmd(m_command); + + if (cmd.name().isEmpty()) { + return failureResponse("Invalid collection name"); + } + + Collection parent; + qint64 resourceId = 0; + bool forceVirtual = false; + MimeType::List parentContentTypes; + + // Invalid or empty scope means we refer to root collection + if (cmd.parent().scope() != Scope::Invalid && !cmd.parent().isEmpty()) { + parent = HandlerHelper::collectionFromScope(cmd.parent(), connection()); + if (!parent.isValid()) { + return failureResponse("Invalid parent collection"); + } + + // check if parent can contain a sub-folder + parentContentTypes = parent.mimeTypes(); + bool found = false, foundVirtual = false; + Q_FOREACH (const MimeType &mt, parentContentTypes) { + if (mt.name() == QLatin1String("inode/directory")) { + found = true; + } else if (mt.name() == QLatin1String("application/x-vnd.akonadi.collection.virtual")) { + foundVirtual = true; + } + if (found && foundVirtual) { + break; + } + } + if (!found && !foundVirtual) { + return failureResponse("Parent collection can not contain sub-collections"); + } + + // If only virtual collections are supported, force every new collection to + // be virtual. Otherwise depend on VIRTUAL attribute in the command + if (foundVirtual && !found) { + forceVirtual = true; + } + + // inherit resource + resourceId = parent.resourceId(); + } else { + const QString sessionId = QString::fromUtf8(connection()->sessionId()); + Resource res = Resource::retrieveByName(sessionId); + if (!res.isValid()) { + return failureResponse("Cannot create top-level collection"); + } + resourceId = res.id(); + } + + Collection collection; + if (parent.isValid()) { + collection.setParentId(parent.id()); + } + collection.setName(cmd.name()); + collection.setResourceId(resourceId); + collection.setRemoteId(cmd.remoteId()); + collection.setRemoteRevision(cmd.remoteRevision()); + collection.setIsVirtual(cmd.isVirtual() || forceVirtual); + collection.setEnabled(cmd.enabled()); + collection.setSyncPref(cmd.syncPref()); + collection.setDisplayPref(cmd.displayPref()); + collection.setIndexPref(cmd.indexPref()); + const Protocol::CachePolicy &cp = cmd.cachePolicy(); + collection.setCachePolicyCacheTimeout(cp.cacheTimeout()); + collection.setCachePolicyCheckInterval(cp.checkInterval()); + collection.setCachePolicyInherit(cp.inherit()); + collection.setCachePolicyLocalParts(cp.localParts().join(QLatin1Char(' '))); + collection.setCachePolicySyncOnDemand(cp.syncOnDemand()); + + DataStore *db = connection()->storageBackend(); + Transaction transaction(db); + + if (!db->appendCollection(collection)) { + return failureResponse(QStringLiteral("Could not create collection ") % cmd.name() + % QStringLiteral(", resourceId: ") % QString::number(resourceId)); + } + + QStringList effectiveMimeTypes = cmd.mimeTypes(); + if (effectiveMimeTypes.isEmpty()) { + effectiveMimeTypes.reserve(parentContentTypes.count()); + Q_FOREACH (const MimeType &mt, parentContentTypes) { + effectiveMimeTypes << mt.name(); + } + } + if (!db->appendMimeTypeForCollection(collection.id(), effectiveMimeTypes)) { + return failureResponse(QStringLiteral("Unable to append mimetype for collection ") % cmd.name() + % QStringLiteral(" resourceId: ") % QString::number(resourceId)); + } + + // store user defined attributes + const QMap attrs = cmd.attributes(); + for (auto iter = attrs.constBegin(), end = attrs.constEnd(); iter != end; ++iter) { + if (!db->addCollectionAttribute(collection, iter.key(), iter.value())) { + return failureResponse("Unable to add collection attribute."); + } + } + + if (!transaction.commit()) { + return failureResponse("Unable to commit transaction."); + } + + db->activeCachePolicy(collection); + + + sendResponse( + HandlerHelper::fetchCollectionsResponse(collection)); + + return successResponse(); +} diff --git a/src/server/handler/create.h b/src/server/handler/create.h new file mode 100644 index 0000000..a3886bd --- /dev/null +++ b/src/server/handler/create.h @@ -0,0 +1,46 @@ +/*************************************************************************** + * Copyright (C) 2006 by Ingo Kloecker * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef AKONADICREATE_H +#define AKONADICREATE_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the CREATE command. CREATE is backward-compatible with RFC 3051, + except recursive collection creation. + + Response: + A untagged response identical to AkList is sent for every created collection. + */ +class Create : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/delete.cpp b/src/server/handler/delete.cpp new file mode 100644 index 0000000..f6cc3f3 --- /dev/null +++ b/src/server/handler/delete.cpp @@ -0,0 +1,77 @@ +/* + 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 "delete.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "storage/datastore.h" +#include "storage/transaction.h" +#include "storage/selectquerybuilder.h" +#include "storage/collectionqueryhelper.h" +#include "search/searchmanager.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Delete::deleteRecursive(Collection &col) +{ + Collection::List children = col.children(); + for (Collection &child : children) { + if (!deleteRecursive(child)) { + return false; + } + } + + DataStore *db = connection()->storageBackend(); + return db->cleanupCollection(col); +} + +bool Delete::parseStream() +{ + Protocol::DeleteCollectionCommand cmd(m_command); + + Collection collection = HandlerHelper::collectionFromScope(cmd.collection(), connection()); + if (!collection.isValid()) { + return failureResponse("No such collection."); + } + + // handle virtual folders + if (collection.resource().name() == QLatin1String(AKONADI_SEARCH_RESOURCE)) { + // don't delete virtual root + if (collection.parentId() == 0) { + return failureResponse("Cannot delete virtual root collection"); + } + } + + Transaction transaction(DataStore::self()); + + if (!deleteRecursive(collection)) { + return failureResponse("Unable to delete collection"); + } + + if (!transaction.commit()) { + return failureResponse("Unable to commit transaction"); + } + + return successResponse(); + return true; +} diff --git a/src/server/handler/delete.h b/src/server/handler/delete.h new file mode 100644 index 0000000..e32305b --- /dev/null +++ b/src/server/handler/delete.h @@ -0,0 +1,52 @@ +/* + 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. +*/ + +#ifndef AKONADI_DELETE_H +#define AKONADI_DELETE_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +class Collection; + +/** + @ingroup akonadi_server_handler + + Handler for the collection deletion command. + + This commands deletes the selected collections including all their content + and that of any child collection. +*/ +class Delete : public Handler +{ + Q_OBJECT +public: + bool parseStream(); + +private: + bool deleteRecursive(Collection &col); + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/fetch.cpp b/src/server/handler/fetch.cpp new file mode 100644 index 0000000..0543dca --- /dev/null +++ b/src/server/handler/fetch.cpp @@ -0,0 +1,52 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "fetch.h" + +#include "connection.h" +#include "fetchhelper.h" +#include "cachecleaner.h" +#include "storage/selectquerybuilder.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + + +bool Fetch::parseStream() +{ + Protocol::FetchItemsCommand cmd(m_command); + + if (!connection()->context()->setScopeContext(cmd.scopeContext())) { + return failureResponse("Invalid scope context"); + } + + // We require context in case we do RID fetch + if (connection()->context()->isEmpty() && cmd.scope().scope() == Scope::Rid) { + return failureResponse("No FETCH context specified"); + } + + CacheCleanerInhibitor inhibitor; + + FetchHelper fetchHelper(connection(), cmd.scope(), cmd.fetchScope()); + if (!fetchHelper.fetchItems()) { + return failureResponse("Failed to fetch items"); + } + + return successResponse(); +} diff --git a/src/server/handler/fetch.h b/src/server/handler/fetch.h new file mode 100644 index 0000000..85e6290 --- /dev/null +++ b/src/server/handler/fetch.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADIFETCH_H +#define AKONADIFETCH_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the fetch command. +*/ +class Fetch : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/fetchhelper.cpp b/src/server/handler/fetchhelper.cpp new file mode 100644 index 0000000..2ea7b06 --- /dev/null +++ b/src/server/handler/fetchhelper.cpp @@ -0,0 +1,718 @@ +/*************************************************************************** + * Copyright (C) 2006-2009 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "fetchhelper.h" + +#include "akonadi.h" +#include "connection.h" +#include "handler.h" +#include "handlerhelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/itemqueryhelper.h" +#include "storage/itemretrievalmanager.h" +#include "storage/itemretrievalrequest.h" +#include "storage/parthelper.h" +#include "storage/parttypehelper.h" +#include "storage/transaction.h" +#include "utils.h" +#include "intervalcheck.h" +#include "agentmanagerinterface.h" +#include "dbusconnectionpool.h" +#include "tagfetchhelper.h" +#include "relationfetch.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +#define ENABLE_FETCH_PROFILING 0 +#if ENABLE_FETCH_PROFILING + #define BEGIN_TIMER(name) \ + QElapsedTimer name##Timer; \ + name##Timer.start(); + + #define END_TIMER(name) \ + const double name##Elapsed = name##Timer.nsecsElapsed() / 1000000.0; + #define PROF_INC(name) \ + ++name; +#else + #define BEGIN_TIMER(name) + #define END_TIMER(name) + #define PROF_INC(name) +#endif + +FetchHelper::FetchHelper(Connection *connection, const Scope &scope, + const Protocol::FetchScope &fetchScope) + : mConnection(connection) + , mScope(scope) + , mFetchScope(fetchScope) +{ + std::fill(mItemQueryColumnMap, mItemQueryColumnMap + ItemQueryColumnCount, -1); +} + +enum PartQueryColumns { + PartQueryPimIdColumn, + PartQueryTypeIdColumn, + PartQueryDataColumn, + PartQueryExternalColumn, + PartQueryVersionColumn, + PartQueryDataSizeColumn +}; + +QSqlQuery FetchHelper::buildPartQuery(const QVector &partList, bool allPayload, bool allAttrs) +{ + ///TODO: merge with ItemQuery + QueryBuilder partQuery(PimItem::tableName()); + + if (!partList.isEmpty() || allPayload || allAttrs) { + partQuery.addJoin(QueryBuilder::InnerJoin, Part::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); + partQuery.addColumn(PimItem::idFullColumnName()); + partQuery.addColumn(Part::partTypeIdFullColumnName()); + partQuery.addColumn(Part::dataFullColumnName()); + partQuery.addColumn(Part::externalFullColumnName()); + partQuery.addColumn(Part::versionFullColumnName()); + partQuery.addColumn(Part::datasizeFullColumnName()); + + partQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + if (!partList.isEmpty() || allPayload || allAttrs) { + Query::Condition cond(Query::Or); + Q_FOREACH (const QByteArray &b, partList) { + if (b.startsWith("PLD") || b.startsWith("ATR")) { + cond.addValueCondition(Part::partTypeIdFullColumnName(), Query::Equals, PartTypeHelper::fromFqName(b).id()); + } + } + if (allPayload || allAttrs) { + partQuery.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); + if (allPayload) { + cond.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QStringLiteral("PLD")); + } + if (allAttrs) { + cond.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QStringLiteral("ATR")); + } + } + + partQuery.addCondition(cond); + } + + ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), partQuery); + + if (!partQuery.exec()) { + throw HandlerException("Unable to list item parts"); + } + partQuery.query().next(); + } + + return partQuery.query(); +} + +QSqlQuery FetchHelper::buildItemQuery() +{ + QueryBuilder itemQuery(PimItem::tableName()); + + int column = 0; +#define ADD_COLUMN(colName, colId) { itemQuery.addColumn( colName ); mItemQueryColumnMap[colId] = column++; } + ADD_COLUMN(PimItem::idFullColumnName(), ItemQueryPimItemIdColumn); + if (mFetchScope.fetchRemoteId()) { + ADD_COLUMN(PimItem::remoteIdFullColumnName(), ItemQueryPimItemRidColumn) + } + ADD_COLUMN(PimItem::mimeTypeIdFullColumnName(), ItemQueryMimeTypeIdColumn) + ADD_COLUMN(PimItem::revFullColumnName(), ItemQueryRevColumn) + if (mFetchScope.fetchRemoteRevision()) { + ADD_COLUMN(PimItem::remoteRevisionFullColumnName(), ItemQueryRemoteRevisionColumn) + } + if (mFetchScope.fetchSize()) { + ADD_COLUMN(PimItem::sizeFullColumnName(), ItemQuerySizeColumn) + } + if (mFetchScope.fetchMTime()) { + ADD_COLUMN(PimItem::datetimeFullColumnName(), ItemQueryDatetimeColumn) + } + ADD_COLUMN(PimItem::collectionIdFullColumnName(), ItemQueryCollectionIdColumn) + if (mFetchScope.fetchGID()) { + ADD_COLUMN(PimItem::gidFullColumnName(), ItemQueryPimItemGidColumn) + } +#undef ADD_COLUMN + + itemQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), itemQuery); + + if (mFetchScope.changedSince().isValid()) { + itemQuery.addValueCondition(PimItem::datetimeFullColumnName(), Query::GreaterOrEqual, mFetchScope.changedSince().toUTC()); + } + + if (!itemQuery.exec()) { + throw HandlerException("Unable to list items"); + } + + itemQuery.query().next(); + + return itemQuery.query(); +} + +enum FlagQueryColumns { + FlagQueryPimItemIdColumn, + FlagQueryFlagIdColumn +}; + +QSqlQuery FetchHelper::buildFlagQuery() +{ + QueryBuilder flagQuery(PimItem::tableName()); + flagQuery.addJoin(QueryBuilder::InnerJoin, PimItemFlagRelation::tableName(), + PimItem::idFullColumnName(), PimItemFlagRelation::leftFullColumnName()); + flagQuery.addColumn(PimItem::idFullColumnName()); + flagQuery.addColumn(PimItemFlagRelation::rightFullColumnName()); + + ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), flagQuery); + flagQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + if (!flagQuery.exec()) { + throw HandlerException("Unable to retrieve item flags"); + } + + flagQuery.query().next(); + + return flagQuery.query(); +} + +enum TagQueryColumns { + TagQueryItemIdColumn, + TagQueryTagIdColumn, +}; + +QSqlQuery FetchHelper::buildTagQuery() +{ + QueryBuilder tagQuery(PimItem::tableName()); + tagQuery.addJoin(QueryBuilder::InnerJoin, PimItemTagRelation::tableName(), + PimItem::idFullColumnName(), PimItemTagRelation::leftFullColumnName()); + tagQuery.addJoin(QueryBuilder::InnerJoin, Tag::tableName(), + Tag::idFullColumnName(), PimItemTagRelation::rightFullColumnName()); + tagQuery.addColumn(PimItem::idFullColumnName()); + tagQuery.addColumn(Tag::idFullColumnName()); + + ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), tagQuery); + tagQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + if (!tagQuery.exec()) { + throw HandlerException("Unable to retrieve item tags"); + } + + tagQuery.query().next(); + + return tagQuery.query(); +} + +enum VRefQueryColumns { + VRefQueryCollectionIdColumn, + VRefQueryItemIdColumn +}; + +QSqlQuery FetchHelper::buildVRefQuery() +{ + QueryBuilder vRefQuery(PimItem::tableName()); + vRefQuery.addJoin(QueryBuilder::LeftJoin, CollectionPimItemRelation::tableName(), + CollectionPimItemRelation::rightFullColumnName(), + PimItem::idFullColumnName()); + vRefQuery.addColumn(CollectionPimItemRelation::leftFullColumnName()); + vRefQuery.addColumn(CollectionPimItemRelation::rightFullColumnName()); + ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), vRefQuery); + vRefQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + if (!vRefQuery.exec()) { + throw HandlerException("Unable to retrieve virtual references"); + } + + vRefQuery.query().next(); + + return vRefQuery.query(); +} + +bool FetchHelper::isScopeLocal(const Scope &scope) +{ + // The only agent allowed to override local scope is the Baloo Indexer + if (!mConnection->sessionId().startsWith("akonadi_indexing_agent")) { + return false; + } + + // Get list of all resources that own all items in the scope + QueryBuilder qb(PimItem::tableName(), QueryBuilder::Select); + qb.setDistinct(true); + qb.addColumn(Resource::nameFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, Collection::tableName(), + PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, Resource::tableName(), + Collection::resourceIdFullColumnName(), Resource::idFullColumnName()); + ItemQueryHelper::scopeToQuery(scope, mConnection->context(), qb); + if (mConnection->context()->resource().isValid()) { + qb.addValueCondition(Resource::nameFullColumnName(), Query::NotEquals, + mConnection->context()->resource().name()); + } + + if (!qb.exec()) { + throw HandlerException("Failed to query database"); + return false; + } + + // If there is more than one resource, i.e. this is a fetch from multiple + // collections, then don't bother and just return FALSE. This case is aimed + // specifically on Baloo, which fetches items from each collection independently, + // so it will pass this check. + QSqlQuery query = qb.query(); + if (query.size() != 1) { + return false; + } + + query.next(); + const QString resourceName = query.value(0).toString(); + + org::freedesktop::Akonadi::AgentManager manager(DBus::serviceName(DBus::Control), + QStringLiteral("/AgentManager"), + DBusConnectionPool::threadConnection()); + const QString typeIdentifier = manager.agentInstanceType(resourceName); + const QVariantMap properties = manager.agentCustomProperties(typeIdentifier); + return properties.value(QStringLiteral("HasLocalStorage"), false).toBool(); +} + +bool FetchHelper::fetchItems() +{ + BEGIN_TIMER(fetch) + + // retrieve missing parts + // HACK: isScopeLocal() is a workaround for resources that have cache expiration + // because when the cache expires, Baloo is not able to content of the items. So + // we allow fetch of items that belong to local resources (like maildir) to ignore + // cacheOnly and retrieve missing parts from the resource. However ItemRetriever + // is painfully slow with many items and is generally designed to fetch a few + // messages, not all of them. In the long term, we need a better way to do this. + BEGIN_TIMER(itemRetriever) + BEGIN_TIMER(scopeLocal) + #if ENABLE_FETCH_PROFILING + double scopeLocalElapsed = 0; + #endif + if (!mFetchScope.cacheOnly() || isScopeLocal(mScope)) { + #if ENABLE_FETCH_PROFILING + scopeLocalElapsed = scopeLocalTimer.elapsed(); + #endif + + // trigger a collection sync if configured to do so + triggerOnDemandFetch(); + + // Prepare for a call to ItemRetriever::exec(); + // From a resource perspective the only parts that can be fetched are payloads. + ItemRetriever retriever(mConnection); + retriever.setScope(mScope); + retriever.setRetrieveParts(mFetchScope.requestedPayloads()); + retriever.setRetrieveFullPayload(mFetchScope.fullPayload()); + retriever.setChangedSince(mFetchScope.changedSince()); + if (!retriever.exec() && !mFetchScope.ignoreErrors()) { // There we go, retrieve the missing parts from the resource. + if (mConnection->context()->resource().isValid()) { + throw HandlerException(QStringLiteral("Unable to fetch item from backend (collection %1, resource %2) : %3") + .arg(mConnection->context()->collectionId()) + .arg(mConnection->context()->resource().id()) + .arg(QString::fromLatin1(retriever.lastError()))); + } else { + throw HandlerException(QStringLiteral("Unable to fetch item from backend (collection %1) : %2") + .arg(mConnection->context()->collectionId()) + .arg(QString::fromLatin1(retriever.lastError()))); + } + } + } + END_TIMER(itemRetriever) + + BEGIN_TIMER(items) + QSqlQuery itemQuery = buildItemQuery(); + END_TIMER(items) + + // error if query did not find any item and scope is not listing items but + // a request for a specific item + if (!itemQuery.isValid()) { + if (mFetchScope.ignoreErrors()) { + return true; + } + switch (mScope.scope()) { + case Scope::Uid: // fall through + case Scope::Rid: // fall through + case Scope::HierarchicalRid: // fall through + case Scope::Gid: + throw HandlerException("Item query returned empty result set"); + break; + default: + break; + } + } + // build part query if needed + BEGIN_TIMER(parts) + QSqlQuery partQuery; + if (!mFetchScope.requestedParts().isEmpty() || mFetchScope.fullPayload() || mFetchScope.allAttributes()) { + partQuery = buildPartQuery(mFetchScope.requestedParts(), mFetchScope.fullPayload(), mFetchScope.allAttributes()); + } + END_TIMER(parts) + + // build flag query if needed + BEGIN_TIMER(flags) + QSqlQuery flagQuery; + if (mFetchScope.fetchFlags()) { + flagQuery = buildFlagQuery(); + } + END_TIMER(flags) + + // build tag query if needed + BEGIN_TIMER(tags) + QSqlQuery tagQuery; + if (mFetchScope.fetchTags()) { + tagQuery = buildTagQuery(); + } + END_TIMER(tags) + + BEGIN_TIMER(vRefs) + QSqlQuery vRefQuery; + if (mFetchScope.fetchVirtualReferences()) { + vRefQuery = buildVRefQuery(); + } + END_TIMER(vRefs) + + #if ENABLE_FETCH_PROFILING + int itemsCount = 0; + int flagsCount = 0; + int partsCount = 0; + int tagsCount = 0; + int vRefsCount = 0; + #endif + + BEGIN_TIMER(processing) + QHash flagIdNameCache; + QHash mimeTypeIdNameCache; + QHash partTypeIdNameCache; + while (itemQuery.isValid()) { + PROF_INC(itemsCount) + + const qint64 pimItemId = extractQueryResult(itemQuery, ItemQueryPimItemIdColumn).toLongLong(); + const int pimItemRev = extractQueryResult(itemQuery, ItemQueryRevColumn).toInt(); + + Protocol::FetchItemsResponse response(pimItemId); + response.setRevision(pimItemRev); + const qint64 mimeTypeId = extractQueryResult(itemQuery, ItemQueryMimeTypeIdColumn).toLongLong(); + auto mtIter = mimeTypeIdNameCache.find(mimeTypeId); + if (mtIter == mimeTypeIdNameCache.end()) { + mtIter = mimeTypeIdNameCache.insert(mimeTypeId, MimeType::retrieveById(mimeTypeId).name()); + } + response.setMimeType(mtIter.value()); + if (mFetchScope.fetchRemoteId()) { + response.setRemoteId(extractQueryResult(itemQuery, ItemQueryPimItemRidColumn).toString()); + } + response.setParentId(extractQueryResult(itemQuery, ItemQueryCollectionIdColumn).toLongLong()); + + if (mFetchScope.fetchSize()) { + response.setSize(extractQueryResult(itemQuery, ItemQuerySizeColumn).toLongLong()); + } + if (mFetchScope.fetchMTime()) { + response.setMTime(Utils::variantToDateTime(extractQueryResult(itemQuery, ItemQueryDatetimeColumn))); + } + if (mFetchScope.fetchRemoteRevision()) { + response.setRemoteRevision(extractQueryResult(itemQuery, ItemQueryRemoteRevisionColumn).toString()); + } + if (mFetchScope.fetchGID()) { + response.setGid(extractQueryResult(itemQuery, ItemQueryPimItemGidColumn).toString()); + } + + if (mFetchScope.fetchFlags()) { + QVector flags; + while (flagQuery.isValid()) { + const qint64 id = flagQuery.value(FlagQueryPimItemIdColumn).toLongLong(); + if (id > pimItemId) { + flagQuery.next(); + continue; + } else if (id < pimItemId) { + break; + } + const qint64 flagId = flagQuery.value(FlagQueryFlagIdColumn).toLongLong(); + auto flagNameIter = flagIdNameCache.find(flagId); + if (flagNameIter == flagIdNameCache.end()) { + flagNameIter = flagIdNameCache.insert(flagId, Flag::retrieveById(flagId).name().toUtf8()); + } + flags << flagNameIter.value(); + flagQuery.next(); + } + response.setFlags(flags); + } + + if (mFetchScope.fetchTags()) { + QVector tagIds; + QVector tags; + //We don't take the fetch scope into account yet. It's either id only or the full tag. + const bool fullTagsRequested = !mFetchScope.tagFetchScope().isEmpty(); + while (tagQuery.isValid()) { + PROF_INC(tagsCount) + const qint64 id = tagQuery.value(TagQueryItemIdColumn).toLongLong(); + if (id > pimItemId) { + tagQuery.next(); + continue; + } else if (id < pimItemId) { + break; + } + tagIds << tagQuery.value(TagQueryTagIdColumn).toLongLong(); + tagQuery.next(); + } + + tags.reserve(tagIds.count()); + if (!fullTagsRequested) { + Q_FOREACH (qint64 tagId, tagIds) { + tags << Protocol::FetchTagsResponse(tagId); + } + } else { + Q_FOREACH (qint64 tagId, tagIds) { + tags << HandlerHelper::fetchTagsResponse(Tag::retrieveById(tagId)); + } + } + response.setTags(tags); + } + + if (mFetchScope.fetchVirtualReferences()) { + QVector vRefs; + while (vRefQuery.isValid()) { + PROF_INC(vRefsCount) + const qint64 id = vRefQuery.value(VRefQueryItemIdColumn).toLongLong(); + if (id > pimItemId) { + vRefQuery.next(); + continue; + } else if (id < pimItemId) { + break; + } + vRefs << vRefQuery.value(VRefQueryCollectionIdColumn).toLongLong(); + vRefQuery.next(); + } + response.setVirtualReferences(vRefs); + } + + if (mFetchScope.fetchRelations()) { + SelectQueryBuilder qb; + Query::Condition condition; + condition.setSubQueryMode(Query::Or); + condition.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, pimItemId); + condition.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, pimItemId); + qb.addCondition(condition); + qb.addGroupColumns(QStringList() << Relation::leftIdColumn() << Relation::rightIdColumn() << Relation::typeIdColumn() << Relation::remoteIdColumn()); + if (!qb.exec()) { + throw HandlerException("Unable to list item relations"); + } + QVector relations; + const auto result = qb.result(); + relations.reserve(result.size()); + Q_FOREACH (const Relation &rel, result) { + relations << HandlerHelper::fetchRelationsResponse(rel); + } + response.setRelations(relations); + } + + if (mFetchScope.ancestorDepth() != Protocol::Ancestor::NoAncestor) { + response.setAncestors(ancestorsForItem(response.parentId())); + } + + bool skipItem = false; + + QVector cachedParts; + QVector parts; + while (partQuery.isValid()) { + PROF_INC(partsCount) + const qint64 id = partQuery.value(PartQueryPimIdColumn).toLongLong(); + if (id > pimItemId) { + partQuery.next(); + continue; + } else if (id < pimItemId) { + break; + } + + const qint64 partTypeId = partQuery.value(PartQueryTypeIdColumn).toLongLong(); + auto ptIter = partTypeIdNameCache.find(partTypeId); + if (ptIter == partTypeIdNameCache.end()) { + ptIter = partTypeIdNameCache.insert(partTypeId, PartTypeHelper::fullName(PartType::retrieveById(partTypeId)).toUtf8()); + } + Protocol::PartMetaData metaPart; + Protocol::StreamPayloadResponse partData; + partData.setPayloadName(ptIter.value()); + metaPart.setName(ptIter.value()); + metaPart.setVersion(partQuery.value(PartQueryVersionColumn).toInt()); + metaPart.setSize(partQuery.value(PartQueryDataSizeColumn).toLongLong()); + + const QByteArray data = Utils::variantToByteArray(partQuery.value(PartQueryDataColumn)); + if (mFetchScope.checkCachedPayloadPartsOnly()) { + if (!data.isEmpty()) { + cachedParts << ptIter.value(); + } + partQuery.next(); + } else { + if (mFetchScope.ignoreErrors() && data.isEmpty()) { + //We wanted the payload, couldn't get it, and are ignoring errors. Skip the item. + //This is not an error though, it's fine to have empty payload parts (to denote existing but not cached parts) + //akDebug() << "item" << id << "has an empty payload part in parttable for part" << partName; + skipItem = true; + break; + } + metaPart.setIsExternal(partQuery.value(PartQueryExternalColumn).toBool()); + if (data.isEmpty()) { + partData.setData(QByteArray("")); + } else { + partData.setData(data); + } + partData.setMetaData(metaPart); + + if (mFetchScope.requestedParts().contains(ptIter.value()) || mFetchScope.fullPayload() || mFetchScope.allAttributes()) { + parts.append(partData); + } + + partQuery.next(); + } + } + response.setParts(parts); + + if (skipItem) { + itemQuery.next(); + continue; + } + + if (mFetchScope.checkCachedPayloadPartsOnly()) { + response.setCachedParts(cachedParts); + } + + mConnection->sendResponse(response); + + itemQuery.next(); + } + END_TIMER(processing) + + // update atime (only if the payload was actually requested, otherwise a simple resource sync prevents cache clearing) + BEGIN_TIMER(aTime) + if (needsAccessTimeUpdate(mFetchScope.requestedParts()) || mFetchScope.fullPayload()) { + updateItemAccessTime(); + } + END_TIMER(aTime) + + END_TIMER(fetch) + #if ENABLE_FETCH_PROFILING + qCDebug(AKONADISERVER_LOG) << "FetchHelper execution stats:"; + qCDebug(AKONADISERVER_LOG) << "\tItems query:" << itemsElapsed << "ms," << itemsCount << " items in total"; + qCDebug(AKONADISERVER_LOG) << "\tFlags query:" << flagsElapsed << "ms, " << flagsCount << " flags in total"; + qCDebug(AKONADISERVER_LOG) << "\tParts query:" << partsElapsed << "ms, " << partsCount << " parts in total"; + qCDebug(AKONADISERVER_LOG) << "\tTags query: " << tagsElapsed << "ms, " << tagsCount << " tags in total"; + qCDebug(AKONADISERVER_LOG) << "\tVRefs query:" << vRefsElapsed << "ms, " << vRefsCount << " vRefs in total"; + qCDebug(AKONADISERVER_LOG) << "\t------------"; + qCDebug(AKONADISERVER_LOG) << "\tItem retriever:" << itemRetrieverElapsed << "ms (scope local:" << scopeLocalElapsed << "ms)"; + qCDebug(AKONADISERVER_LOG) << "\tTotal query:" << (itemsElapsed + flagsElapsed + partsElapsed + tagsElapsed + vRefsElapsed) << "ms"; + qCDebug(AKONADISERVER_LOG) << "\tTotal processing: " << processingElapsed << "ms"; + qCDebug(AKONADISERVER_LOG) << "\tATime update:" << aTimeElapsed << "ms"; + qCDebug(AKONADISERVER_LOG) << "\t============"; + qCDebug(AKONADISERVER_LOG) << "\tTotal FETCH:" << fetchElapsed << "ms"; + qCDebug(AKONADISERVER_LOG); + qCDebug(AKONADISERVER_LOG); + #endif + + return true; +} + +bool FetchHelper::needsAccessTimeUpdate(const QVector &parts) +{ + // TODO technically we should compare the part list with the cache policy of + // the parent collection of the retrieved items, but that's kinda expensive + // Only updating the atime if the full payload was requested is a good + // approximation though. + return parts.contains(AKONADI_PARAM_PLD_RFC822); +} + +void FetchHelper::updateItemAccessTime() +{ + Transaction transaction(mConnection->storageBackend()); + QueryBuilder qb(PimItem::tableName(), QueryBuilder::Update); + qb.setColumnValue(PimItem::atimeColumn(), QDateTime::currentDateTimeUtc()); + ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), qb); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Unable to update item access time"; + } else { + transaction.commit(); + } +} + +void FetchHelper::triggerOnDemandFetch() +{ + if (mConnection->context()->collectionId() <= 0 || mFetchScope.cacheOnly()) { + return; + } + + Collection collection = mConnection->context()->collection(); + + // HACK: don't trigger on-demand syncing if the resource is the one triggering it + if (mConnection->sessionId() == collection.resource().name().toLatin1()) { + return; + } + + DataStore *store = mConnection->storageBackend(); + store->activeCachePolicy(collection); + if (!collection.cachePolicySyncOnDemand()) { + return; + } + + if (AkonadiServer::instance()->intervalChecker()) { + AkonadiServer::instance()->intervalChecker()->requestCollectionSync(collection); + } +} + +QVector FetchHelper::ancestorsForItem(Collection::Id parentColId) +{ + if (mFetchScope.ancestorDepth() == Protocol::Ancestor::NoAncestor || parentColId == 0) { + return QVector(); + } + if (mAncestorCache.contains(parentColId)) { + return mAncestorCache.value(parentColId); + } + + QVector ancestors; + Collection col = Collection::retrieveById(parentColId); + const int depthNum = mFetchScope.ancestorDepth() == Protocol::Ancestor::ParentAncestor ? 1 : INT_MAX; + for (int i = 0; i < depthNum; ++i) { + if (!col.isValid()) { + ancestors << Protocol::Ancestor(0); + break; + } + ancestors << Protocol::Ancestor(col.id(), col.remoteId()); + col = col.parent(); + } + mAncestorCache.insert(parentColId, ancestors); + return ancestors; +} + +QVariant FetchHelper::extractQueryResult(const QSqlQuery &query, FetchHelper::ItemQueryColumns column) const +{ + Q_ASSERT(mItemQueryColumnMap[column] >= 0); + return query.value(mItemQueryColumnMap[column]); +} diff --git a/src/server/handler/fetchhelper.h b/src/server/handler/fetchhelper.h new file mode 100644 index 0000000..e899871 --- /dev/null +++ b/src/server/handler/fetchhelper.h @@ -0,0 +1,93 @@ +/*************************************************************************** + * Copyright (C) 2006-2009 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADI_FETCHHELPER_H +#define AKONADI_FETCHHELPER_H + +#include + +#include "storage/countquerybuilder.h" +#include "storage/datastore.h" +#include "storage/itemretriever.h" + +#include +#include +#include + +class FetchHelperTest; + +namespace Akonadi { + +namespace Server { + +class Connection; +class Response; + +class FetchHelper : public QObject +{ + Q_OBJECT + +public: + FetchHelper(Connection *connection, const Scope &scope, const Protocol::FetchScope &fetchScope); + + bool fetchItems(); + +private: + enum ItemQueryColumns { + ItemQueryPimItemIdColumn, + ItemQueryPimItemRidColumn, + ItemQueryMimeTypeIdColumn, + ItemQueryRevColumn, + ItemQueryRemoteRevisionColumn, + ItemQuerySizeColumn, + ItemQueryDatetimeColumn, + ItemQueryCollectionIdColumn, + ItemQueryPimItemGidColumn, + ItemQueryColumnCount + }; + + void updateItemAccessTime(); + void triggerOnDemandFetch(); + QSqlQuery buildItemQuery(); + QSqlQuery buildPartQuery(const QVector &partList, bool allPayload, bool allAttrs); + QSqlQuery buildFlagQuery(); + QSqlQuery buildTagQuery(); + QSqlQuery buildVRefQuery(); + + QVector ancestorsForItem(Collection::Id parentColId); + static bool needsAccessTimeUpdate(const QVector &parts); + QVariant extractQueryResult(const QSqlQuery &query, ItemQueryColumns column) const; + bool isScopeLocal(const Scope &scope); + static QByteArray tagsToByteArray(const Tag::List &tags); + static QByteArray relationsToByteArray(const Relation::List &relations); + +private: + Connection *mConnection; + QHash> mAncestorCache; + Scope mScope; + Protocol::FetchScope mFetchScope; + int mItemQueryColumnMap[ItemQueryColumnCount]; + + friend class ::FetchHelperTest; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/link.cpp b/src/server/handler/link.cpp new file mode 100644 index 0000000..0014e2a --- /dev/null +++ b/src/server/handler/link.cpp @@ -0,0 +1,107 @@ +/* + 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 "link.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "storage/datastore.h" +#include "storage/itemqueryhelper.h" +#include "storage/transaction.h" +#include "storage/selectquerybuilder.h" +#include "storage/collectionqueryhelper.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Link::parseStream() +{ + Protocol::LinkItemsCommand cmd(m_command); + + const Collection collection = HandlerHelper::collectionFromScope(cmd.destination(), connection()); + if (!collection.isVirtual()) { + return failureResponse("Can't link items to non-virtual collections"); + } + + /* FIXME BIN + Resource originalContext; + Scope::SelectionScope itemSelectionScope = Scope::selectionScopeFromByteArray(m_streamParser->peekString()); + if (itemSelectionScope != Scope::None) { + m_streamParser->readString(); + // Unset Resource context if destination collection is specified using HRID/RID, + // because otherwise the Resource context is relative to the destination collection + // instead of the source collection (collection context) + if ((mDestinationScope.scope() == Scope::HierarchicalRid || mDestinationScope.scope() == Scope::Rid) && itemSelectionScope == Scope::Rid) { + originalContext = connection()->context()->resource(); + connection()->context()->setResource(Resource()); + } + } + Scope itemScope(itemSelectionScope); + itemScope.parseScope(m_streamParser); + */ + + SelectQueryBuilder qb; + ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb); + + /* + if (originalContext.isValid()) { + connection()->context()->setResource(originalContext); + } + */ + + if (!qb.exec()) { + return failureResponse("Unable to execute item query"); + } + + const PimItem::List items = qb.result(); + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store); + + PimItem::List toLink, toUnlink; + const bool createLinks = (cmd.action() == Protocol::LinkItemsCommand::Link); + Q_FOREACH (const PimItem &item, items) { + const bool alreadyLinked = collection.relatesToPimItem(item); + bool result = true; + if (createLinks && !alreadyLinked) { + result = collection.addPimItem(item); + toLink << item; + } else if (!createLinks && alreadyLinked) { + result = collection.removePimItem(item); + toUnlink << item; + } + if (!result) { + return failureResponse("Failed to modify item reference"); + } + } + + if (!toLink.isEmpty()) { + store->notificationCollector()->itemsLinked(toLink, collection); + } else if (!toUnlink.isEmpty()) { + store->notificationCollector()->itemsUnlinked(toUnlink, collection); + } + + if (!transaction.commit()) { + return failureResponse("Cannot commit transaction."); + } + + return successResponse(); +} diff --git a/src/server/handler/link.h b/src/server/handler/link.h new file mode 100644 index 0000000..eaeaf54 --- /dev/null +++ b/src/server/handler/link.h @@ -0,0 +1,46 @@ +/* + 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_LINK_H +#define AKONADI_LINK_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + * @ingroup akonadi_server_handler + * + * Handler for the LINK and UNLINK commands. + * + * These commands are used to add and remove references of a set of items to a + * virtual collection. + */ +class Link : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/list.cpp b/src/server/handler/list.cpp new file mode 100644 index 0000000..7092d2d --- /dev/null +++ b/src/server/handler/list.cpp @@ -0,0 +1,623 @@ +/* + 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 "list.h" +#include "akonadiserver_debug.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "collectionreferencemanager.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" +#include "storage/collectionqueryhelper.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +template +static bool intersect(const QVector &l1, const QVector &l2) +{ + Q_FOREACH (const T &e2, l2) { + if (l1.contains(e2.id())) { + return true; + } + } + return false; +} + +List::List() + : Handler() + , mAncestorDepth(0) + , mIncludeStatistics(false) + , mEnabledCollections(false) + , mCollectionsToDisplay(false) + , mCollectionsToSynchronize(false) + , mCollectionsToIndex(false) +{ +} + +QStack List::ancestorsForCollection(const Collection &col) +{ + if (mAncestorDepth <= 0) { + return QStack(); + } + QStack ancestors; + Collection parent = col; + for (int i = 0; i < mAncestorDepth; ++i) { + if (parent.parentId() == 0) { + break; + } + if (mAncestors.contains(parent.parentId())) { + parent = mAncestors.value(parent.parentId()); + } else { + parent = mCollections.value(parent.parentId()); + } + if (!parent.isValid()) { + qCWarning(AKONADISERVER_LOG) << col.id(); + throw HandlerException("Found invalid parent in ancestors"); + } + ancestors.prepend(parent); + } + return ancestors; +} + +CollectionAttribute::List List::getAttributes(const Collection &col, const QSet &filter) +{ + CollectionAttribute::List attributes; + auto it = mCollectionAttributes.find(col.id()); + while (it != mCollectionAttributes.end() && it.key() == col.id()) { + if (filter.isEmpty() || filter.contains(it.value().type())) { + attributes << it.value(); + } + ++it; + } + + // We need the reference and enabled status, to not need to request the server mutliple times. + // Mostly interessting for ancestors for i.e. updates provided by the monitor. + const bool isReferenced = connection()->collectionReferenceManager()->isReferenced(col.id(), connection()->sessionId()); + if (isReferenced) { + CollectionAttribute attr; + attr.setType(AKONADI_PARAM_REFERENCED); + attr.setValue("TRUE"); + attributes << attr; + } + { + CollectionAttribute attr; + attr.setType(AKONADI_PARAM_ENABLED); + attr.setValue(col.enabled() ? "TRUE" : "FALSE"); + attributes << attr; + } + + return attributes; +} + +void List::listCollection(const Collection &root, const QStack &ancestors, + const QStringList &mimeTypes, + const CollectionAttribute::List &attributes) +{ + const bool isReferencedFromSession = connection()->collectionReferenceManager()->isReferenced(root.id(), connection()->sessionId()); + //We always expose referenced collections to the resource as referenced (although it's a different session) + //Otherwise syncing wouldn't work. + const bool resourceIsSynchronizing = root.referenced() && mCollectionsToSynchronize && connection()->context()->resource().isValid(); + + QStack ancestorAttributes; + //backwards compatibility, collectionToByteArray will automatically fall-back to id + remoteid + if (!mAncestorAttributes.isEmpty()) { + ancestorAttributes.reserve(ancestors.size()); + Q_FOREACH (const Collection &col, ancestors) { + ancestorAttributes.push(getAttributes(col, mAncestorAttributes)); + } + } + + // write out collection details + Collection dummy = root; + DataStore *db = connection()->storageBackend(); + db->activeCachePolicy(dummy); + + sendResponse(HandlerHelper::fetchCollectionsResponse(dummy, attributes, mIncludeStatistics, + mAncestorDepth, ancestors, + ancestorAttributes, + isReferencedFromSession || resourceIsSynchronizing, + mimeTypes)); +} + +static Query::Condition filterCondition(const QString &column) +{ + Query::Condition orCondition(Query::Or); + orCondition.addValueCondition(column, Query::Equals, (int)Tristate::True); + Query::Condition andCondition(Query::And); + andCondition.addValueCondition(column, Query::Equals, (int)Tristate::Undefined); + andCondition.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true); + orCondition.addCondition(andCondition); + orCondition.addValueCondition(Collection::referencedFullColumnName(), Query::Equals, true); + return orCondition; +} + +bool List::checkFilterCondition(const Collection &col) const +{ + //Don't include the collection when only looking for enabled collections + if (mEnabledCollections && !col.enabled()) { + return false; + } + //Don't include the collection when only looking for collections to display/index/sync + if (mCollectionsToDisplay && + (((col.displayPref() == Tristate::Undefined) && !col.enabled()) || + (col.displayPref() == Tristate::False))) { + return false; + } + if (mCollectionsToIndex && + (((col.indexPref() == Tristate::Undefined) && !col.enabled()) || + (col.indexPref() == Tristate::False))) { + return false; + } + //Single collection sync will still work since that is using a base fetch + if (mCollectionsToSynchronize && + (((col.syncPref() == Tristate::Undefined) && !col.enabled()) || + (col.syncPref() == Tristate::False))) { + return false; + } + return true; +} + +static QSqlQuery getAttributeQuery(const QVariantList &ids, const QSet &requestedAttributes) +{ + QueryBuilder qb(CollectionAttribute::tableName()); + + qb.addValueCondition(CollectionAttribute::collectionIdFullColumnName(), Query::In, ids); + + qb.addColumn(CollectionAttribute::collectionIdFullColumnName()); + qb.addColumn(CollectionAttribute::typeFullColumnName()); + qb.addColumn(CollectionAttribute::valueFullColumnName()); + + if (!requestedAttributes.isEmpty()) { + QVariantList attributes; + attributes.reserve(requestedAttributes.size()); + Q_FOREACH (const QByteArray &type, requestedAttributes) { + attributes << type; + } + qb.addValueCondition(CollectionAttribute::typeFullColumnName(), Query::In, attributes); + } + + qb.addSortColumn(CollectionAttribute::collectionIdFullColumnName(), Query::Ascending); + + if (!qb.exec()) { + throw HandlerException("Unable to retrieve attributes for listing"); + } + return qb.query(); +} + +void List::retrieveAttributes(const QVariantList &collectionIds) +{ + //We are querying for the attributes in batches because something can't handle WHERE IN queries with sets larger than 999 + int start = 0; + const int size = 999; + while (start < collectionIds.size()) { + const QVariantList ids = collectionIds.mid(start, size); + QSqlQuery attributeQuery = getAttributeQuery(ids, mAncestorAttributes); + while (attributeQuery.next()) { + CollectionAttribute attr; + attr.setType(attributeQuery.value(1).toByteArray()); + attr.setValue(attributeQuery.value(2).toByteArray()); + // qCDebug(AKONADISERVER_LOG) << "found attribute " << attr.type() << attr.value(); + mCollectionAttributes.insert(attributeQuery.value(0).toLongLong(), attr); + } + start += size; + } + } + +static QSqlQuery getMimeTypeQuery(const QVariantList &ids) +{ + QueryBuilder qb(CollectionMimeTypeRelation::tableName()); + + qb.addJoin(QueryBuilder::LeftJoin, MimeType::tableName(), MimeType::idFullColumnName(), CollectionMimeTypeRelation::rightFullColumnName()); + qb.addValueCondition(CollectionMimeTypeRelation::leftFullColumnName(), Query::In, ids); + + qb.addColumn(CollectionMimeTypeRelation::leftFullColumnName()); + qb.addColumn(CollectionMimeTypeRelation::rightFullColumnName()); + qb.addColumn(MimeType::nameFullColumnName()); + qb.addSortColumn(CollectionMimeTypeRelation::leftFullColumnName(), Query::Ascending); + + if (!qb.exec()) { + throw HandlerException("Unable to retrieve mimetypes for listing"); + } + return qb.query(); +} + +void List::retrieveCollections(const Collection &topParent, int depth) +{ + /* + * Retrieval of collections: + * The aim is to reduce the amount of queries as much as possible, as this has the largest performance impact for large queries. + * * First all collections that match the given criteria are queried + * * We then filter the false positives: + * ** all collections out that are not part of the tree we asked for are filtered + * ** all collections that are referenced but not by this session or by the owning resource are filtered + * * Finally we complete the tree by adding missing collections + * + * Mimetypes and attributes are also retrieved in single queries to avoid spawning two queries per collection (the N+1 problem). + * Note that we're not querying attributes and mimetypes for the collections that are only included to complete the tree, + * this results in no items being queried for those collections. + */ + + const qint64 parentId = topParent.isValid() ? topParent.id() : 0; + { + SelectQueryBuilder qb; + + if (depth == 0) { + qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, parentId); + } else if (depth == 1) { + if (topParent.isValid()) { + qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Equals, parentId); + } else { + qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Is, QVariant()); + } + } else { + if (topParent.isValid()) { + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, topParent.resourceId()); + } else { + // Gimme gimme gimme...everything! + } + } + + //Base listings should succeed always + if (depth != 0) { + if (mCollectionsToSynchronize) { + qb.addCondition(filterCondition(Collection::syncPrefFullColumnName())); + } else if (mCollectionsToDisplay) { + qCDebug(AKONADISERVER_LOG) << "only display"; + qb.addCondition(filterCondition(Collection::displayPrefFullColumnName())); + } else if (mCollectionsToIndex) { + qb.addCondition(filterCondition(Collection::indexPrefFullColumnName())); + } else if (mEnabledCollections) { + Query::Condition orCondition(Query::Or); + orCondition.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true); + orCondition.addValueCondition(Collection::referencedFullColumnName(), Query::Equals, true); + qb.addCondition(orCondition); + } + if (mResource.isValid()) { + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, mResource.id()); + } + + if (!mMimeTypes.isEmpty()) { + qb.addJoin(QueryBuilder::LeftJoin, CollectionMimeTypeRelation::tableName(), CollectionMimeTypeRelation::leftColumn(), Collection::idFullColumnName()); + QVariantList mimeTypeFilter; + mimeTypeFilter.reserve(mMimeTypes.size()); + Q_FOREACH(MimeType::Id mtId, mMimeTypes) { + mimeTypeFilter << mtId; + } + qb.addValueCondition(CollectionMimeTypeRelation::rightColumn(), Query::In, mimeTypeFilter); + qb.addGroupColumn(Collection::idFullColumnName()); + } + } + + if (!qb.exec()) { + throw HandlerException("Unable to retrieve collection for listing"); + } + Q_FOREACH (const Collection &col, qb.result()) { + mCollections.insert(col.id(), col); + } + } + + //Post filtering that we couldn't do as part of the sql query + if (depth > 0) { + auto it = mCollections.begin(); + while (it != mCollections.end()) { + + if (topParent.isValid()) { + //Check that each collection is linked to the root collection + bool foundParent = false; + //We iterate over parents to link it to topParent if possible + Collection::Id id = it->parentId(); + while (id > 0) { + if (id == parentId) { + foundParent = true; + break; + } + Collection col = mCollections.value(id); + if (!col.isValid()) { + col = Collection::retrieveById(id); + } + id = col.parentId(); + } + if (!foundParent) { + it = mCollections.erase(it); + continue; + } + } + + //If we matched referenced collections we need to ensure the collection was referenced from this session + const bool isReferencedFromSession = connection()->collectionReferenceManager()->isReferenced(it->id(), connection()->sessionId()); + //The collection is referenced, but not from this session. We need to reevaluate the filter condition + if (it->referenced() && !isReferencedFromSession) { + //Don't include the collection when only looking for enabled collections. + //However, a referenced collection should be still synchronized by the resource, so we exclude this case. + if (!checkFilterCondition(*it) && !(mCollectionsToSynchronize && connection()->context()->resource().isValid())) { + it = mCollections.erase(it); + continue; + } + } + + ++it; + } + } + + QVariantList mimeTypeIds; + QVariantList attributeIds; + mimeTypeIds.reserve(mCollections.size()); + attributeIds.reserve(mCollections.size()); + for (auto it = mCollections.cbegin(), end = mCollections.cend(); it != end; ++it) { + mimeTypeIds << it.key(); + attributeIds << it.key(); + } + + QVariantList ancestorIds; + //We'd only require the non-leaf collections, but we don't know which those are, so we take all. + ancestorIds.reserve(mCollections.size()); + for (auto it = mCollections.cbegin(), end = mCollections.cend(); it != end; ++it) { + ancestorIds << it.key(); + } + if (mAncestorDepth > 0 && topParent.isValid()) { + //unless depth is 0 the base collection is not part of the listing + mAncestors.insert(topParent.id(), topParent); + ancestorIds << topParent.id(); + //We need to retrieve additional ancestors to what we already have in the tree + Collection parent = topParent; + for (int i = 0; i < mAncestorDepth; ++i) { + if (parent.parentId() == 0) { + break; + } + parent = parent.parent(); + mAncestors.insert(parent.id(), parent); + //We also require the attributes + ancestorIds << parent.id(); + } + } + + QSet missingCollections; + if (depth > 0) { + Q_FOREACH (const Collection &col, mCollections) { + if (col.parentId() != parentId && !mCollections.contains(col.parentId())) { + missingCollections.insert(col.parentId()); + } + } + } + + /* + QSet knownIds; + for (const Collection &col : mCollections) { + knownIds.insert(col.id()); + } + qCDebug(AKONADISERVER_LOG) << "HAS:" << knownIds; + qCDebug(AKONADISERVER_LOG) << "MISSING:" << missingCollections; + */ + + //Fetch missing collections that are part of the tree + while (!missingCollections.isEmpty()) { + SelectQueryBuilder qb; + QVariantList ids; + ids.reserve(missingCollections.size()); + Q_FOREACH (qint64 id, missingCollections) { + ids << id; + } + qb.addValueCondition(Collection::idFullColumnName(), Query::In, ids); + if (!qb.exec()) { + throw HandlerException("Unable to retrieve collections for listing"); + } + + missingCollections.clear(); + Q_FOREACH (const Collection &missingCol, qb.result()) { + mCollections.insert(missingCol.id(), missingCol); + ancestorIds << missingCol.id(); + attributeIds << missingCol.id(); + //We have to do another round if the parents parent is missing + if (missingCol.parentId() != parentId && !mCollections.contains(missingCol.parentId())) { + missingCollections.insert(missingCol.parentId()); + } + } + } + + //Since we don't know when we'll need the ancestor attributes, we have to fetch them all together. + //The alternative would be to query for each collection which would reintroduce the N+1 query performance problem. + if (!mAncestorAttributes.isEmpty()) { + retrieveAttributes(ancestorIds); + } + + //We are querying in batches because something can't handle WHERE IN queries with sets larger than 999 + const int querySizeLimit = 999; + int mimetypeQueryStart = 0; + int attributeQueryStart = 0; + QSqlQuery mimeTypeQuery; + QSqlQuery attributeQuery; + auto it = mCollections.begin(); + while (it != mCollections.end()) { + const Collection col = it.value(); + // qCDebug(AKONADISERVER_LOG) << "col " << col.id(); + + QStringList mimeTypes; + { + //Get new query if necessary + if (!mimeTypeQuery.isValid() && mimetypeQueryStart < mimeTypeIds.size()) { + const QVariantList ids = mimeTypeIds.mid(mimetypeQueryStart, querySizeLimit); + mimetypeQueryStart += querySizeLimit; + mimeTypeQuery = getMimeTypeQuery(ids); + mimeTypeQuery.next(); //place at first record + } + + // qCDebug(AKONADISERVER_LOG) << mimeTypeQuery.isValid() << mimeTypeQuery.value(0).toLongLong(); + while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() < col.id()) { + qCDebug(AKONADISERVER_LOG) << "skipped: " << mimeTypeQuery.value(0).toLongLong() << mimeTypeQuery.value(2).toString(); + if (!mimeTypeQuery.next()) { + break; + } + } + //Advance query while a mimetype for this collection is returned + while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() == col.id()) { + mimeTypes << mimeTypeQuery.value(2).toString(); + if (!mimeTypeQuery.next()) { + break; + } + } + } + + CollectionAttribute::List attributes; + { + //Get new query if necessary + if (!attributeQuery.isValid() && attributeQueryStart < attributeIds.size()) { + const QVariantList ids = attributeIds.mid(attributeQueryStart, querySizeLimit); + attributeQueryStart += querySizeLimit; + attributeQuery = getAttributeQuery(ids, QSet()); + attributeQuery.next(); //place at first record + } + + // qCDebug(AKONADISERVER_LOG) << attributeQuery.isValid() << attributeQuery.value(0).toLongLong(); + while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() < col.id()) { + qCDebug(AKONADISERVER_LOG) << "skipped: " << attributeQuery.value(0).toLongLong() << attributeQuery.value(1).toByteArray(); + if (!attributeQuery.next()) { + break; + } + } + //Advance query while a mimetype for this collection is returned + while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() == col.id()) { + CollectionAttribute attr; + attr.setType(attributeQuery.value(1).toByteArray()); + attr.setValue(attributeQuery.value(2).toByteArray()); + attributes << attr; + + if (!attributeQuery.next()) { + break; + } + } + } + + listCollection(col, ancestorsForCollection(col), mimeTypes, attributes); + it++; + } +} + +bool List::parseStream() +{ + Protocol::FetchCollectionsCommand cmd(m_command); + + if (!cmd.resource().isEmpty()) { + mResource = Resource::retrieveByName(cmd.resource()); + if (!mResource.isValid()) { + return failureResponse("Unknown resource"); + } + } + Q_FOREACH (const QString &mtName, cmd.mimeTypes()) { + const MimeType mt = MimeType::retrieveByName(mtName); + if (mt.isValid()) { + mMimeTypes.append(mt.id()); + } else { + MimeType mt(mtName); + if (!mt.insert()) { + return failureResponse("Failed to create mimetype record"); + } + mMimeTypes.append(mt.id()); + } + } + + mEnabledCollections = cmd.enabled(); + mCollectionsToSynchronize = cmd.syncPref(); + mCollectionsToDisplay = cmd.displayPref(); + mCollectionsToIndex = cmd.indexPref(); + mIncludeStatistics = cmd.fetchStats(); + + int depth = 0; + switch (cmd.depth()) { + case Protocol::FetchCollectionsCommand::BaseCollection: + depth = 0; + break; + case Protocol::FetchCollectionsCommand::ParentCollection: + depth = 1; + break; + case Protocol::FetchCollectionsCommand::AllCollections: + depth = INT_MAX; + break; + } + + switch (cmd.ancestorsDepth()) { + case Protocol::Ancestor::NoAncestor: + mAncestorDepth = 0; + break; + case Protocol::Ancestor::ParentAncestor: + mAncestorDepth = 1; + break; + case Protocol::Ancestor::AllAncestors: + mAncestorDepth = INT_MAX; + break; + } + mAncestorAttributes = cmd.ancestorsAttributes(); + + Scope scope = cmd.collections(); + if (!scope.isEmpty()) { // not root + Collection col; + if (scope.scope() == Scope::Uid) { + col = Collection::retrieveById(scope.uid()); + } else if (scope.scope() == Scope::Rid) { + SelectQueryBuilder qb; + qb.addValueCondition(Collection::remoteIdFullColumnName(), Query::Equals, scope.rid()); + qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), + Collection::resourceIdFullColumnName(), Resource::idFullColumnName()); + if (mCollectionsToSynchronize) { + qb.addCondition(filterCondition(Collection::syncPrefFullColumnName())); + } else if (mCollectionsToDisplay) { + qb.addCondition(filterCondition(Collection::displayPrefFullColumnName())); + } else if (mCollectionsToIndex) { + qb.addCondition(filterCondition(Collection::indexPrefFullColumnName())); + } + if (mResource.isValid()) { + qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, mResource.id()); + } else if (connection()->context()->resource().isValid()) { + qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, connection()->context()->resource().id()); + } else { + return failureResponse("Cannot retrieve collection based on remote identifier without a resource context"); + } + if (!qb.exec()) { + return failureResponse("Unable to retrieve collection for listing"); + } + Collection::List results = qb.result(); + if (results.count() != 1) { + return failureResponse(QString::number(results.count()) + QStringLiteral(" collections found")); + } + col = results.first(); + } else if (scope.scope() == Scope::HierarchicalRid) { + if (!connection()->context()->resource().isValid()) { + return failureResponse("Cannot retrieve collection based on hierarchical remote identifier without a resource context"); + } + col = CollectionQueryHelper::resolveHierarchicalRID(scope.hridChain(), connection()->context()->resource().id()); + } else { + return failureResponse("Unexpected error"); + } + + if (!col.isValid()) { + return failureResponse("Collection does not exist"); + } + + retrieveCollections(col, depth); + } else { //Root folder listing + if (depth != 0) { + retrieveCollections(Collection(), depth); + } + } + + return successResponse(); +} diff --git a/src/server/handler/list.h b/src/server/handler/list.h new file mode 100644 index 0000000..f66b80d --- /dev/null +++ b/src/server/handler/list.h @@ -0,0 +1,125 @@ +/* + 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_LIST_H +#define AKONADI_LIST_H + +#include "entities.h" +#include "handler.h" + +template class QStack; + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the LIST command. + + This command is used to get a (limited) listing of the available collections. + It is different from the LIST command and is more similar to FETCH. + +

Syntax

+ + Request: + @verbatim + request = tag " " command " " collection-id " " depth " (" filter-list ")" " (" option-list ")" + command = "LIST" | "LSUB" | "RID LIST" | "RID LSUB" + depth = number | "INF" + filter-list = *(filter-key " " filter-value) + filter-key = "RESOURCE" | "MIMETYPE" | "ENABLED" | "SYNC" | "DISPLAY" | "INDEX" + option-list = *(option-key " " option-value) + option-key = "STATISTICS" + @endverbatim + + @c LIST will include all known collections, @c LSUB only those that are + subscribed or contains subscribed collections (cf. RFC 3501, LIST vs. LSUB). + + The @c RID command prefix indicates that @c collection-id is a remote identifier + instead of a unique identifier. In this case a resource context has to be specified + previously using the @c RESSELECT command. + + @c depths chooses between recursive (@c INF), flat (1) and local (0, ie. just the + base collection) listing, 0 indicates the root collection. + + The @c filter-list is used to restrict the listing to collection of a specific + resource or content type. + + The @c option-list allows to specify the response content to some extend: + - @c STATISTICS (boolean) allows to include the collection statistics (see Status) + - @c ANCESTORDEPTH (numeric) allows you to specify the number of ancestor nodes that + should be included additionally to the @c parent-id included anyway. + Possible values are @c 0 (the default), @c 1 for the direct parent node and @c INF for all, + terminating with the root collection. + + Response: + @verbatim + response = "*" collection-id " " parent-id " ("attribute-list")" + attribute-list = *(attribute-identifier " " attribute-value) + attribute-identifier = "NAME" | "MIMETYPE" | "REMOTEID" | "REMOTEREVISION" | "RESOURCE" | "VIRTUAL" | "MESSAGES" | "UNSEEN" | "SIZE" | "ANCESTORS" | "custom-attr-identifier + @endverbatim + + The name is encoded as an quoted UTF-8 string. There is no order defined for the + single responses. + + The ancestors property is encoded as a list of UID/RID pairs. +*/ +class List : public Handler +{ + Q_OBJECT + +public: + List(); + + bool parseStream(); + +private: + void listCollection(const Collection &root, + const QStack &ancestors, + const QStringList &mimeTypes, + const CollectionAttribute::List &attributes); + QStack ancestorsForCollection(const Collection &col); + void retrieveCollections(const Collection &topParent, int depth); + bool checkFilterCondition(const Collection &col) const; + bool checkChildrenForMimeTypes(const QHash &collectionsMap, + const QHash &parentMap, + const Collection &col); + CollectionAttribute::List getAttributes(const Collection &colId, + const QSet &filter = QSet()); + void retrieveAttributes(const QVariantList &collectionIds); + + Resource mResource; + QVector mMimeTypes; + int mAncestorDepth; + bool mIncludeStatistics; + bool mEnabledCollections; + bool mCollectionsToDisplay; + bool mCollectionsToSynchronize; + bool mCollectionsToIndex; + QSet mAncestorAttributes; + QMap mCollections; + QHash mAncestors; + QMultiHash mCollectionAttributes; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/login.cpp b/src/server/handler/login.cpp new file mode 100644 index 0000000..96b55f4 --- /dev/null +++ b/src/server/handler/login.cpp @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "login.h" + +#include "connection.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Login::parseStream() +{ + Protocol::LoginCommand cmd(m_command); + + if (cmd.sessionId().isEmpty()) { + return failureResponse("Missing session identifier"); + } + + connection()->setSessionId(cmd.sessionId()); + if (cmd.sessionMode() == Protocol::LoginCommand::NotificationBus) { + connection()->setIsNotificationBus(true); + } + + Q_EMIT connectionStateChange(Server::Authenticated); + + return successResponse(); +} diff --git a/src/server/handler/login.h b/src/server/handler/login.h new file mode 100644 index 0000000..c30f570 --- /dev/null +++ b/src/server/handler/login.h @@ -0,0 +1,42 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef AKONADILOGIN_H +#define AKONADILOGIN_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the login command. +*/ +class Login : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/logout.cpp b/src/server/handler/logout.cpp new file mode 100644 index 0000000..6af9082 --- /dev/null +++ b/src/server/handler/logout.cpp @@ -0,0 +1,33 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "logout.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Logout::parseStream() +{ + Protocol::LogoutCommand cmd(m_command); + + sendResponse(); + + Q_EMIT connectionStateChange(LoggingOut); + return true; +} diff --git a/src/server/handler/logout.h b/src/server/handler/logout.h new file mode 100644 index 0000000..4a70e75 --- /dev/null +++ b/src/server/handler/logout.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef AKONADILOGOUT_H +#define AKONADILOGOUT_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the logout command. + */ +class Logout : public Handler +{ + Q_OBJECT +public: + bool parseStream(); + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/modify.cpp b/src/server/handler/modify.cpp new file mode 100644 index 0000000..686a725 --- /dev/null +++ b/src/server/handler/modify.cpp @@ -0,0 +1,309 @@ +/* + 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 "modify.h" + +#include "akonadi.h" +#include "connection.h" +#include "handlerhelper.h" +#include "cachecleaner.h" +#include "collectionreferencemanager.h" +#include "intervalcheck.h" +#include "storage/datastore.h" +#include "storage/transaction.h" +#include "storage/itemretriever.h" +#include "storage/selectquerybuilder.h" +#include "storage/collectionqueryhelper.h" +#include "search/searchmanager.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Modify::parseStream() +{ + Protocol::ModifyCollectionCommand cmd(m_command); + + Collection collection = HandlerHelper::collectionFromScope(cmd.collection(), connection()); + if (!collection.isValid()) { + return failureResponse("No such collection"); + } + + CacheCleanerInhibitor inhibitor(false); + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ParentID) { + const Collection newParent = Collection::retrieveById(cmd.parentId()); + if (newParent.isValid() && collection.parentId() != newParent.id() + && collection.resourceId() != newParent.resourceId()) { + inhibitor.inhibit(); + ItemRetriever retriever(connection()); + retriever.setCollection(collection, true); + retriever.setRetrieveFullPayload(true); + if (!retriever.exec()) { + throw HandlerException(retriever.lastError()); + } + } + } + + DataStore *db = connection()->storageBackend(); + Transaction transaction(db); + QList changes; + bool referencedChanged = false; + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::MimeTypes) { + QStringList mts = cmd.mimeTypes(); + const MimeType::List currentMts = collection.mimeTypes(); + bool equal = true; + Q_FOREACH (const MimeType ¤tMt, currentMts) { + if (mts.contains(currentMt.name())) { + mts.removeAll(currentMt.name()); + continue; + } + equal = false; + if (!collection.removeMimeType(currentMt)) { + return failureResponse("Unable to remove collection mimetype"); + } + } + if (!db->appendMimeTypeForCollection(collection.id(), mts)) { + return failureResponse("Unable to add collection mimetypes"); + } + if (!equal || !mts.isEmpty()) { + changes.append(AKONADI_PARAM_MIMETYPE); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::CachePolicy) { + bool changed = false; + const Protocol::CachePolicy newCp = cmd.cachePolicy(); + if (collection.cachePolicyCacheTimeout() != newCp.cacheTimeout()) { + collection.setCachePolicyCacheTimeout(newCp.cacheTimeout()); + changed = true; + } + if (collection.cachePolicyCheckInterval() != newCp.checkInterval()) { + collection.setCachePolicyCheckInterval(newCp.checkInterval()); + changed = true; + } + if (collection.cachePolicyInherit() != newCp.inherit()) { + collection.setCachePolicyInherit(newCp.inherit()); + changed = true; + } + + QStringList parts = newCp.localParts(); + qSort(parts); + const QString localParts = parts.join(QLatin1Char(' ')); + if (collection.cachePolicyLocalParts() != localParts) { + collection.setCachePolicyLocalParts(localParts); + changed = true; + } + if (collection.cachePolicySyncOnDemand() != newCp.syncOnDemand()) { + collection.setCachePolicySyncOnDemand(newCp.syncOnDemand()); + changed = true; + } + + if (changed) { + changes.append(AKONADI_PARAM_CACHEPOLICY); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::Name) { + if (cmd.name() != collection.name()) { + if (!CollectionQueryHelper::hasAllowedName(collection, cmd.name(), collection.parentId())) { + return failureResponse("Collection with the same name exists already"); + } + collection.setName(cmd.name()); + changes.append(AKONADI_PARAM_NAME); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ParentID) { + if (collection.parentId() != cmd.parentId()) { + if (!db->moveCollection(collection, Collection::retrieveById(cmd.parentId()))) { + return failureResponse("Unable to reparent collection"); + } + changes.append(AKONADI_PARAM_PARENT); + } + } + + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemoteID) { + if (cmd.remoteId() != collection.remoteId()) { + if (!connection()->isOwnerResource(collection)) { + return failureResponse("Only resources can modify remote identifiers"); + } + collection.setRemoteId(cmd.remoteId()); + changes.append(AKONADI_PARAM_REMOTEID); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemoteRevision) { + if (cmd.remoteRevision() != collection.remoteRevision()) { + collection.setRemoteRevision(cmd.remoteRevision()); + changes.append(AKONADI_PARAM_REMOTEREVISION); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::PersistentSearch) { + bool changed = false; + if (cmd.persistentSearchQuery() != collection.queryString()) { + collection.setQueryString(cmd.persistentSearchQuery()); + changed = true; + } + + QList queryAttributes = collection.queryAttributes().toUtf8().split(' '); + if (cmd.persistentSearchRemote() != queryAttributes.contains(AKONADI_PARAM_REMOTE)) { + if (cmd.persistentSearchRemote()) { + queryAttributes.append(AKONADI_PARAM_REMOTE); + } else { + queryAttributes.removeOne(AKONADI_PARAM_REMOTE); + } + changed = true; + } + if (cmd.persistentSearchRecursive() != queryAttributes.contains(AKONADI_PARAM_RECURSIVE)) { + if (cmd.persistentSearchRecursive()) { + queryAttributes.append(AKONADI_PARAM_RECURSIVE); + } else { + queryAttributes.removeOne(AKONADI_PARAM_REMOTE); + } + changed = true; + } + if (changed) { + collection.setQueryAttributes(QString::fromLatin1(queryAttributes.join(" "))); + } + + QStringList cols; + cols.reserve(cmd.persistentSearchCollections().size()); + QVector inCols = cmd.persistentSearchCollections(); + qSort(inCols); + Q_FOREACH (qint64 col, cmd.persistentSearchCollections()) { + cols.append(QString::number(col)); + } + const QString colStr = cols.join(QLatin1Char(' ')); + if (colStr != collection.queryCollections()) { + collection.setQueryCollections(colStr); + changed = true; + } + + if (changed || cmd.modifiedParts() & Protocol::ModifyCollectionCommand::MimeTypes) { + SearchManager::instance()->updateSearch(collection); + if (changed) { + changes.append(AKONADI_PARAM_PERSISTENTSEARCH); + } + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ListPreferences) { + if (cmd.enabled() != collection.enabled()) { + collection.setEnabled(cmd.enabled()); + changes.append(AKONADI_PARAM_ENABLED); + } + if (cmd.syncPref() != collection.syncPref()) { + collection.setSyncPref(cmd.syncPref()); + changes.append(AKONADI_PARAM_SYNC); + } + if (cmd.displayPref() != collection.displayPref()) { + collection.setDisplayPref(cmd.displayPref()); + changes.append(AKONADI_PARAM_DISPLAY); + } + if (cmd.indexPref() != collection.indexPref()) { + collection.setIndexPref(cmd.indexPref()); + changes.append(AKONADI_PARAM_INDEX); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::Referenced) { + const bool wasReferencedFromSession = connection()->collectionReferenceManager()->isReferenced(collection.id(), connection()->sessionId()); + connection()->collectionReferenceManager()->referenceCollection(connection()->sessionId(), collection, cmd.referenced()); + const bool referenced = connection()->collectionReferenceManager()->isReferenced(collection.id()); + if (cmd.referenced() != wasReferencedFromSession) { + changes.append(AKONADI_PARAM_REFERENCED); + } + if (referenced != collection.referenced()) { + referencedChanged = true; + collection.setReferenced(referenced); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemovedAttributes) { + Q_FOREACH (const QByteArray &attr, cmd.removedAttributes()) { + if (db->removeCollectionAttribute(collection, attr)) { + changes.append(attr); + } + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::Attributes) { + const QMap attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + SelectQueryBuilder qb; + qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, collection.id()); + qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, iter.key()); + if (!qb.exec()) { + return failureResponse("Unable to retrieve collection attribute"); + } + + + const CollectionAttribute::List attrs = qb.result(); + if (attrs.isEmpty()) { + CollectionAttribute newAttr; + newAttr.setCollectionId(collection.id()); + newAttr.setType(iter.key()); + newAttr.setValue(iter.value()); + if (!newAttr.insert()) { + return failureResponse("Unable to add collection attribute"); + } + changes.append(iter.key()); + } else if (attrs.size() == 1) { + CollectionAttribute currAttr = attrs.first(); + if (currAttr.value() == iter.value()) { + continue; + } + currAttr.setValue(iter.value()); + if (!currAttr.update()) { + return failureResponse("Unable to update collection attribute"); + } + changes.append(iter.key()); + } else { + return failureResponse("WTF: more than one attribute with the same name"); + } + } + } + + if (!changes.isEmpty()) { + if (collection.hasPendingChanges() && !collection.update()) { + return failureResponse("Unable to update collection"); + } + //This must be after the collection was updated in the db. The resource will immediately request a copy of the collection. + if (AkonadiServer::instance()->intervalChecker() && collection.referenced() && referencedChanged) { + AkonadiServer::instance()->intervalChecker()->requestCollectionSync(collection); + } + db->notificationCollector()->collectionChanged(collection, changes); + //For backwards compatibility. Must be after the changed notification (otherwise the compression removes it). + if (changes.contains(AKONADI_PARAM_ENABLED)) { + if (collection.enabled()) { + db->notificationCollector()->collectionSubscribed(collection); + } else { + db->notificationCollector()->collectionUnsubscribed(collection); + } + } + if (!transaction.commit()) { + return failureResponse("Unable to commit transaction"); + } + } + + return successResponse(); +} diff --git a/src/server/handler/modify.h b/src/server/handler/modify.h new file mode 100644 index 0000000..660e002 --- /dev/null +++ b/src/server/handler/modify.h @@ -0,0 +1,46 @@ +/* + 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. +*/ + +#ifndef AKONADI_MODIFY_H +#define AKONADI_MODIFY_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the MODIFY command (not in RFC 3501). + + This command is used to modify collections. Its syntax is similar to the STORE + command. +*/ +class Modify : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/move.cpp b/src/server/handler/move.cpp new file mode 100644 index 0000000..cd93c70 --- /dev/null +++ b/src/server/handler/move.cpp @@ -0,0 +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 "move.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "cachecleaner.h" +#include "storage/datastore.h" +#include "storage/itemretriever.h" +#include "storage/itemqueryhelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" +#include "storage/collectionqueryhelper.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Move::parseStream() +{ + Protocol::MoveItemsCommand cmd(m_command); + + const Collection destination = HandlerHelper::collectionFromScope(cmd.destination(), connection()); + if (destination.isVirtual()) { + return failureResponse("Moving items into virtual collection is not allowed"); + } + if (!destination.isValid()) { + return failureResponse("Invalid destination collection"); + } + + connection()->context()->setScopeContext(cmd.itemsContext()); + if (cmd.items().scope() == Scope::Rid) { + if (!connection()->context()->collection().isValid()) { + return failureResponse("RID move requires valid source collection"); + } + } + + CacheCleanerInhibitor inhibitor; + + // make sure all the items we want to move are in the cache + ItemRetriever retriever(connection()); + retriever.setScope(cmd.items()); + retriever.setRetrieveFullPayload(true); + if (!retriever.exec()) { + return failureResponse(retriever.lastError()); + } + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store); + + SelectQueryBuilder qb; + ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb); + qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::NotEquals, destination.id()); + + const QDateTime mtime = QDateTime::currentDateTime(); + + if (qb.exec()) { + const QVector items = qb.result(); + if (items.isEmpty()) { + return successResponse(); + } + + // Split the list by source collection + QMap toMove; + QMap sources; + Q_FOREACH (/*sic!*/ PimItem item, items) { + const Collection source = items.at(0).collection(); + if (!source.isValid()) { + return failureResponse("Item without collection found!?"); + } + if (!sources.contains(source.id())) { + sources.insert(source.id(), source); + } + + if (!item.isValid()) { + return failureResponse("Invalid item in result set!?"); + } + Q_ASSERT(item.collectionId() != destination.id()); + + item.setCollectionId(destination.id()); + item.setAtime(mtime); + item.setDatetime(mtime); + // if the resource moved itself, we assume it did so because the change happend in the backend + if (connection()->context()->resource().id() != destination.resourceId()) { + item.setDirty(true); + } + + toMove.insertMulti(source.id(), item); + } + + // Emit notification for each source collection separately + Q_FOREACH (const Entity::Id &sourceId, toMove.uniqueKeys()) { + PimItem::List itemsToMove; + for (auto it = toMove.cbegin(), end = toMove.cend(); it != end; ++it) { + if (it.key() == sourceId) + itemsToMove.push_back(it.value()); + } + + const Collection &source = sources.value(sourceId); + store->notificationCollector()->itemsMoved(itemsToMove, source, destination); + + for (auto iter = toMove.find(sourceId), end = toMove.end(); iter != end; ++iter) { + // reset RID on inter-resource moves, but only after generating the change notification + // so that this still contains the old one for the source resource + const bool isInterResourceMove = (*iter).collection().resource().id() != source.resource().id(); + if (isInterResourceMove) { + (*iter).setRemoteId(QString()); + } + + // FIXME Could we aggregate the changes to a single SQL query? + if (!(*iter).update()) { + return failureResponse("Unable to update item"); + } + } + } + } else { + return failureResponse("Unable to execute query"); + } + + if (!transaction.commit()) { + return failureResponse("Unable to commit transaction."); + } + + return successResponse(); +} diff --git a/src/server/handler/move.h b/src/server/handler/move.h new file mode 100644 index 0000000..cfd5ff5 --- /dev/null +++ b/src/server/handler/move.h @@ -0,0 +1,51 @@ +/* + 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. +*/ + +#ifndef AKONADI_MOVE_H +#define AKONADI_MOVE_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the item move command. + +

Semantics

+ Moves the selected items. Item selection can happen within the usual three scopes: + - based on a uid set relative to the currently selected collection + - based on a global uid set (UID) + - based on a list of remote identifiers within the currently selected collection (RID) + + Destination is a collection id. +*/ +class Move : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/relationfetch.cpp b/src/server/handler/relationfetch.cpp new file mode 100644 index 0000000..0b31658 --- /dev/null +++ b/src/server/handler/relationfetch.cpp @@ -0,0 +1,87 @@ +/*************************************************************************** + * Copyright (C) 2014 by Christian Mollekopf * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "relationfetch.h" + +#include "connection.h" +#include "storage/selectquerybuilder.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool RelationFetch::parseStream() +{ + Protocol::FetchRelationsCommand cmd(m_command); + + SelectQueryBuilder relationQuery; + if (cmd.side() > 0) { + Query::Condition c; + c.setSubQueryMode(Query::Or); + c.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, cmd.side()); + c.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, cmd.side()); + relationQuery.addCondition(c); + } else { + if (cmd.left() > 0) { + relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, cmd.left()); + } + if (cmd.right() > 0) { + relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, cmd.right()); + } + } + if (!cmd.types().isEmpty()) { + relationQuery.addJoin(QueryBuilder::InnerJoin, RelationType::tableName(), Relation::typeIdFullColumnName(), RelationType::idFullColumnName()); + QStringList types; + types.reserve(cmd.types().size()); + Q_FOREACH (const QByteArray &type, cmd.types()) { + types << QString::fromUtf8(type); + } + relationQuery.addValueCondition(RelationType::nameFullColumnName(), Query::In, types); + } + if (!cmd.resource().isEmpty()) { + Resource res = Resource::retrieveByName(cmd.resource()); + if (!res.isValid()) { + return failureResponse("Invalid resource"); + } + Query::Condition condition; + condition.setSubQueryMode(Query::Or); + condition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, Relation::leftIdFullColumnName()); + condition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, Relation::rightIdFullColumnName()); + relationQuery.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), condition); + + relationQuery.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + + relationQuery.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, res.id()); + relationQuery.addGroupColumns(QStringList() << Relation::leftIdFullColumnName() << Relation::rightIdFullColumnName() << Relation::typeIdFullColumnName()); + } + + if (!relationQuery.exec()) { + return failureResponse("Failed to query for existing relation"); + } + const Relation::List existingRelations = relationQuery.result(); + Q_FOREACH (const Relation &relation, existingRelations) { + sendResponse(Protocol::FetchRelationsResponse(relation.leftId(), relation.left().mimeType().name().toUtf8(), + relation.rightId(), relation.right().mimeType().name().toUtf8(), + relation.relationType().name().toUtf8(), + relation.remoteId().toUtf8())); + } + + return successResponse(); +} diff --git a/src/server/handler/relationfetch.h b/src/server/handler/relationfetch.h new file mode 100644 index 0000000..6b39301 --- /dev/null +++ b/src/server/handler/relationfetch.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) 2014 by Christian Mollekopf * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADIFETCHRELATION_H +#define AKONADIFETCHRELATION_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the RELATIONFETCH command. + */ +class RelationFetch : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/relationremove.cpp b/src/server/handler/relationremove.cpp new file mode 100644 index 0000000..ea28c20 --- /dev/null +++ b/src/server/handler/relationremove.cpp @@ -0,0 +1,88 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "relationremove.h" + +#include "connection.h" +#include "storage/querybuilder.h" +#include "storage/selectquerybuilder.h" +#include "storage/queryhelper.h" +#include "storage/datastore.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool RelationRemove::parseStream() +{ + Protocol::RemoveRelationsCommand cmd(m_command); + + if (cmd.left() < 0 || cmd.right() < 0) { + return failureResponse("Invalid relation id's provided"); + } + + RelationType relType; + if (!cmd.type().isEmpty()) { + relType = RelationType::retrieveByName(QString::fromUtf8(cmd.type())); + if (!relType.isValid()) { + return failureResponse("Failed to load relation type"); + } + } + + SelectQueryBuilder relationQuery; + relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, cmd.left()); + relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, cmd.right()); + if (relType.isValid()) { + relationQuery.addValueCondition(Relation::typeIdFullColumnName(), Query::Equals, relType.id()); + } + + if (!relationQuery.exec()) { + return failureResponse("Failed to obtain relations"); + } + const Relation::List relations = relationQuery.result(); + Q_FOREACH (const Relation &relation, relations) { + DataStore::self()->notificationCollector()->relationRemoved(relation); + } + + // Get all PIM items that that are part of the relation + SelectQueryBuilder itemsQuery; + itemsQuery.setSubQueryMode(Query::Or); + itemsQuery.addValueCondition(PimItem::idColumn(), Query::Equals, cmd.left()); + itemsQuery.addValueCondition(PimItem::idColumn(), Query::Equals, cmd.right()); + + if (!itemsQuery.exec()) { + throw failureResponse("Removing relation failed"); + } + const PimItem::List items = itemsQuery.result(); + if (!items.isEmpty()) { + DataStore::self()->notificationCollector()->itemsRelationsChanged(items, Relation::List(), relations); + } + + QueryBuilder qb(Relation::tableName(), QueryBuilder::Delete); + qb.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, cmd.left()); + qb.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, cmd.right()); + if (relType.isValid()) { + qb.addValueCondition(Relation::typeIdFullColumnName(), Query::Equals, relType.id()); + } + if (!qb.exec()) { + return failureResponse("Failed to remove relations"); + } + + return successResponse(); +} + diff --git a/src/server/handler/relationremove.h b/src/server/handler/relationremove.h new file mode 100644 index 0000000..7f4faee --- /dev/null +++ b/src/server/handler/relationremove.h @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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_RELATIONREMOVE_H +#define AKONADI_RELATIONREMOVE_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +class RelationRemove : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_RELATIONREMOVE_H diff --git a/src/server/handler/relationstore.cpp b/src/server/handler/relationstore.cpp new file mode 100644 index 0000000..4198afa --- /dev/null +++ b/src/server/handler/relationstore.cpp @@ -0,0 +1,121 @@ +/* + Copyright (c) 2014 Christian Mollekopf + + 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 "relationstore.h" + +#include "connection.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" +#include "storage/selectquerybuilder.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +Relation RelationStore::fetchRelation(qint64 leftId, qint64 rightId, qint64 typeId) +{ + SelectQueryBuilder relationQuery; + relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, leftId); + relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, rightId); + relationQuery.addValueCondition(Relation::typeIdFullColumnName(), Query::Equals, typeId); + if (!relationQuery.exec()) { + throw HandlerException("Failed to query for existing relation"); + } + const Relation::List existingRelations = relationQuery.result(); + if (!existingRelations.isEmpty()) { + if (existingRelations.size() == 1) { + return existingRelations.at(0); + } else { + throw HandlerException("Matched more than 1 relation"); + } + } + + return Relation(); +} + +bool RelationStore::parseStream() +{ + Protocol::ModifyRelationCommand cmd(m_command); + + if (cmd.type().isEmpty()) { + return failureResponse("Relation type not specified"); + } + + if (cmd.left() < 0 || cmd.right() < 0) { + return failureResponse("Invalid relation specified"); + } + + if (!cmd.remoteId().isEmpty() && !connection()->context()->resource().isValid()) { + return failureResponse("RemoteID can only be set by Resources"); + } + + const QString typeName = QString::fromUtf8(cmd.type()); + RelationType relationType = RelationType::retrieveByName(typeName); + if (!relationType.isValid()) { + RelationType t(typeName); + if (!t.insert()) { + return failureResponse(QStringLiteral("Unable to create relation type '") % typeName % QStringLiteral("'")); + } + relationType = t; + } + + Relation existingRelation = fetchRelation(cmd.left(), cmd.right(), relationType.id()); + if (existingRelation.isValid()) { + existingRelation.setRemoteId(QLatin1String(cmd.remoteId())); + if (!existingRelation.update()) { + return failureResponse("Failed to update relation"); + } + } + + // Can't use insert(), does not work here (no "id" column) + QueryBuilder inQb(Relation::tableName(), QueryBuilder::Insert); + inQb.setIdentificationColumn(QString()); // omit "RETURING xyz" with PSQL + inQb.setColumnValue(Relation::leftIdColumn(), cmd.left()); + inQb.setColumnValue(Relation::rightIdColumn(), cmd.right()); + inQb.setColumnValue(Relation::typeIdColumn(), relationType.id()); + if (!inQb.exec()) { + throw HandlerException("Failed to store relation"); + } + + Relation insertedRelation = fetchRelation(cmd.left(), cmd.right(), relationType.id()); + + // Get all PIM items that are part of the relation + SelectQueryBuilder itemsQuery; + itemsQuery.setSubQueryMode(Query::Or); + itemsQuery.addValueCondition(PimItem::idColumn(), Query::Equals, cmd.left()); + itemsQuery.addValueCondition(PimItem::idColumn(), Query::Equals, cmd.right()); + + if (!itemsQuery.exec()) { + return failureResponse("Adding relation failed"); + } + const PimItem::List items = itemsQuery.result(); + + if (items.size() != 2) { + return failureResponse("Couldn't find items for relation"); + } + + /* if (items[0].collection().resourceId() != items[1].collection().resourceId()) { + throw HandlerException("Relations can only be created for items within the same resource"); + } */ + + DataStore::self()->notificationCollector()->relationAdded(insertedRelation); + DataStore::self()->notificationCollector()->itemsRelationsChanged(items, Relation::List() << insertedRelation, Relation::List()); + + return successResponse(); +} + diff --git a/src/server/handler/relationstore.h b/src/server/handler/relationstore.h new file mode 100644 index 0000000..9f22489 --- /dev/null +++ b/src/server/handler/relationstore.h @@ -0,0 +1,46 @@ +/* + Copyright (c) 2014 Christian Mollekop + + 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_RELATIONSTORE_H +#define AKONADI_RELATIONSTORE_H + +#include "handler.h" + + +namespace Akonadi { +namespace Server { + +class Relation; + +class RelationStore : public Handler +{ + Q_OBJECT + +public: + bool parseStream(); + +private: + Relation fetchRelation(qint64 leftId, qint64 rightId, qint64 typeId); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_RELATIONSTORE_H diff --git a/src/server/handler/remove.cpp b/src/server/handler/remove.cpp new file mode 100644 index 0000000..419bd90 --- /dev/null +++ b/src/server/handler/remove.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 "remove.h" + +#include "connection.h" +#include "storage/datastore.h" +#include "storage/itemqueryhelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Remove::parseStream() +{ + Protocol::DeleteItemsCommand cmd(m_command); + + connection()->context()->setScopeContext(cmd.scopeContext()); + + SelectQueryBuilder qb; + ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb); + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store); + + if (!qb.exec()) { + return failureResponse("Unable to execute query"); + } + + + const QVector items = qb.result(); + if (items.isEmpty()) { + return failureResponse("No items found"); + } + if (!store->cleanupPimItems(items)) { + return failureResponse("Deletion failed"); + } + + if (!transaction.commit()) { + return failureResponse("Unable to commit transaction"); + } + + return successResponse(); +} diff --git a/src/server/handler/remove.h b/src/server/handler/remove.h new file mode 100644 index 0000000..31c1b2f --- /dev/null +++ b/src/server/handler/remove.h @@ -0,0 +1,57 @@ +/* + 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. +*/ + +#ifndef AKONADI_REMOVE_H +#define AKONADI_REMOVE_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the item deletion command. + +

Syntax

+ One of the following three: + @verbatim + REMOVE + UID REMOVE + RID REMOVE + @endverbatim + +

Semantics

+ Removes the selected items. Item selection can happen within the usual three scopes: + - based on a uid set relative to the currently selected collection + - based on a global uid set (UID) + - based on a remote identifier within the currently selected collection (RID) +*/ +class Remove : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/resourceselect.cpp b/src/server/handler/resourceselect.cpp new file mode 100644 index 0000000..a5585cf --- /dev/null +++ b/src/server/handler/resourceselect.cpp @@ -0,0 +1,45 @@ +/* + 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 "resourceselect.h" + +#include "connection.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + + +bool ResourceSelect::parseStream() +{ + Protocol::SelectResourceCommand cmd(m_command); + + if (cmd.resourceId().isEmpty()) { + connection()->context()->setResource(Resource()); + return successResponse(); + } + + const Resource res = Resource::retrieveByName(cmd.resourceId()); + if (!res.isValid()) { + return failureResponse(cmd.resourceId() % QStringLiteral(" is not a valid resource identifier")); + } + + connection()->context()->setResource(res); + + return successResponse(); +} diff --git a/src/server/handler/resourceselect.h b/src/server/handler/resourceselect.h new file mode 100644 index 0000000..7f36ed1 --- /dev/null +++ b/src/server/handler/resourceselect.h @@ -0,0 +1,48 @@ +/* + 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. +*/ + +#ifndef AKONADI_RESOURCESELECT_H +#define AKONADI_RESOURCESELECT_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the resource selection command. + +

Semantics

+ Limits the scope of remote id based operations. Remote ids of collections are only guaranteed + to be unique per resource, so this command should be issued before running any RID based + collection commands. +*/ +class ResourceSelect : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/search.cpp b/src/server/handler/search.cpp new file mode 100644 index 0000000..37cda17 --- /dev/null +++ b/src/server/handler/search.cpp @@ -0,0 +1,99 @@ +/*************************************************************************** + * Copyright (C) 2009 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "search.h" + +#include "connection.h" +#include "fetchhelper.h" +#include "handlerhelper.h" +#include "searchhelper.h" +#include "search/searchrequest.h" +#include "search/searchmanager.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Search::parseStream() +{ + Protocol::SearchCommand cmd(m_command); + + if (cmd.query().isEmpty()) { + return failureResponse("No query specified"); + } + + QVector collectionIds; + bool recursive = cmd.recursive(); + + if (cmd.collections().isEmpty() || cmd.collections() == QVector{ 0ll }) { + collectionIds << 0; + recursive = true; + } + + QVector collections = collectionIds; + if (recursive) { + collections += SearchHelper::matchSubcollectionsByMimeType(collectionIds, cmd.mimeTypes()); + } + + akDebug() << "SEARCH:"; + akDebug() << "\tQuery:" << cmd.query(); + akDebug() << "\tMimeTypes:" << cmd.mimeTypes(); + akDebug() << "\tCollections:" << collections; + akDebug() << "\tRemote:" << cmd.remote(); + akDebug() << "\tRecursive" << recursive; + + if (collections.isEmpty()) { + return successResponse(); + } + + mFetchScope = cmd.fetchScope(); + + SearchRequest request(connection()->sessionId()); + request.setCollections(collections); + request.setMimeTypes(cmd.mimeTypes()); + request.setQuery(cmd.query()); + request.setRemoteSearch(cmd.remote()); + connect(&request, &SearchRequest::resultsAvailable, + this, &Search::slotResultsAvailable); + request.exec(); + + //akDebug() << "\tResult:" << uids; + akDebug() << "\tResult:" << mAllResults.count() << "matches"; + + return successResponse(); +} + +void Search::slotResultsAvailable(const QSet &results) +{ + QSet newResults = results; + newResults.subtract(mAllResults); + mAllResults.unite(newResults); + + if (newResults.isEmpty()) { + return; + } + + ImapSet imapSet; + imapSet.add(newResults); + + Scope scope; + scope.setUidSet(imapSet); + + FetchHelper fetchHelper(connection(), scope, mFetchScope); + fetchHelper.fetchItems(); +} diff --git a/src/server/handler/search.h b/src/server/handler/search.h new file mode 100644 index 0000000..7a74c98 --- /dev/null +++ b/src/server/handler/search.h @@ -0,0 +1,54 @@ +/*************************************************************************** + * Copyright (C) 2009 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADISEARCH_H +#define AKONADISEARCH_H + +#include "handler.h" + +#include +#include + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the search commands. +*/ +class Search : public Handler +{ + Q_OBJECT + +public: + bool parseStream(); + +private Q_SLOTS: + void slotResultsAvailable(const QSet &results); + +private: + Protocol::FetchScope mFetchScope; + QSet mAllResults; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/searchhelper.cpp b/src/server/handler/searchhelper.cpp new file mode 100644 index 0000000..c1aaf79 --- /dev/null +++ b/src/server/handler/searchhelper.cpp @@ -0,0 +1,104 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * Copyright (C) 2014 by Daniel Vrátil * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "searchhelper.h" + +#include "entities.h" +#include "storage/countquerybuilder.h" + +#include + +using namespace Akonadi::Server; + +static qint64 parentCollectionId(qint64 collectionId) +{ + QueryBuilder qb(Collection::tableName(), QueryBuilder::Select); + qb.addColumn(Collection::parentIdColumn()); + qb.addValueCondition(Collection::idColumn(), Query::Equals, collectionId); + if (!qb.exec()) { + return -1; + } + if (!qb.query().next()) { + return -1; + } + return qb.query().value(0).toLongLong(); +} + +QVector SearchHelper::matchSubcollectionsByMimeType(const QVector &ancestors, const QStringList &mimeTypes) +{ + // Get all collections with given mime types + QueryBuilder qb(Collection::tableName(), QueryBuilder::Select); + qb.setDistinct(true); + qb.addColumn(Collection::idFullColumnName()); + qb.addColumn(Collection::parentIdFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, CollectionMimeTypeRelation::tableName(), + CollectionMimeTypeRelation::leftFullColumnName(), Collection::idFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, MimeType::tableName(), + CollectionMimeTypeRelation::rightFullColumnName(), MimeType::idFullColumnName()); + Query::Condition cond(Query::Or); + Q_FOREACH (const QString &mt, mimeTypes) { + cond.addValueCondition(MimeType::nameFullColumnName(), Query::Equals, mt); + } + if (!cond.isEmpty()) { + qb.addCondition(cond); + } + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to query search collections"; + return QVector(); + } + + QMap /* collectionIds */> candidateCollections; + while (qb.query().next()) { + candidateCollections[qb.query().value(1).toLongLong()].append(qb.query().value(0).toLongLong()); + } + + // If the ancestors list contains root, then return what we got, since everything + // is sub collection of root + QVector results; + if (ancestors.contains(0)) { + Q_FOREACH (const QVector &res, candidateCollections) { + results += res; + } + return results; + } + + // Try to resolve direct descendants + Q_FOREACH (qint64 ancestor, ancestors) { + const QVector cols = candidateCollections.take(ancestor); + if (!cols.isEmpty()) { + results += cols; + } + } + + for (auto iter = candidateCollections.begin(); iter != candidateCollections.end(); ++iter) { + // Traverse the collection chain up to root + qint64 parentId = iter.key(); + while (!ancestors.contains(parentId) && parentId > 0) { + parentId = parentCollectionId(parentId); + } + // Ok, we found a requested ancestor in the parent chain + if (parentId > 0) { + results += iter.value(); + } + } + + return results; +} diff --git a/src/server/handler/searchhelper.h b/src/server/handler/searchhelper.h new file mode 100644 index 0000000..55740c5 --- /dev/null +++ b/src/server/handler/searchhelper.h @@ -0,0 +1,38 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADISEARCHHELPER_H +#define AKONADISEARCHHELPER_H + +#include +#include + +namespace Akonadi { +namespace Server { + +class SearchHelper +{ +public: + static QVector matchSubcollectionsByMimeType(const QVector &ancestors, const QStringList &mimeTypes); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/searchpersistent.cpp b/src/server/handler/searchpersistent.cpp new file mode 100644 index 0000000..f07fb89 --- /dev/null +++ b/src/server/handler/searchpersistent.cpp @@ -0,0 +1,102 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "searchpersistent.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/transaction.h" +#include "search/searchmanager.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool SearchPersistent::parseStream() +{ + Protocol::StoreSearchCommand cmd(m_command); + + if (cmd.name().isEmpty()) { + return failureResponse("No name specified"); + } + + if (cmd.query().isEmpty()) { + return failureResponse("No query specified"); + } + + DataStore *db = connection()->storageBackend(); + Transaction transaction(db); + + QList mimeTypes; + QStringList queryAttributes; + + if (cmd.remote()) { + queryAttributes << QStringLiteral(AKONADI_PARAM_REMOTE); + } + if (cmd.recursive()) { + queryAttributes << QStringLiteral(AKONADI_PARAM_RECURSIVE); + } + + QStringList queryCollections; + QVector queryColIds = cmd.queryCollections(); + qSort(queryColIds); + queryCollections.reserve(queryColIds.size()); + Q_FOREACH (qint64 col, queryColIds) { + queryCollections.append(QString::number(col)); + } + + Collection col; + col.setQueryString(cmd.query()); + col.setQueryAttributes(queryAttributes.join(QLatin1Char(' '))); + col.setQueryCollections(queryCollections.join(QLatin1Char(' '))); + col.setParentId(1); // search root + col.setResourceId(1); // search resource + col.setName(cmd.name()); + col.setIsVirtual(true); + if (!db->appendCollection(col)) { + return failureResponse("Unable to create persistent search"); + } + + if (!db->addCollectionAttribute(col, "AccessRights", "luD")) { + return failureResponse("Unable to set rights attribute on persistent search"); + } + + Q_FOREACH (const QString &mimeType, cmd.mimeTypes()) { + MimeType mt = MimeType::retrieveByName(mimeType); + if (!mt.isValid()) { + mt.setName(mimeType); + if (!mt.insert()) { + return failureResponse("Failed to create new mimetype"); + } + } + col.addMimeType(mt); + } + + if (!transaction.commit()) { + return failureResponse("Unable to commit transaction"); + } + + SearchManager::instance()->updateSearch(col); + + sendResponse(HandlerHelper::fetchCollectionsResponse(col)); + return successResponse(); +} diff --git a/src/server/handler/searchpersistent.h b/src/server/handler/searchpersistent.h new file mode 100644 index 0000000..90b2b30 --- /dev/null +++ b/src/server/handler/searchpersistent.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADISEARCHPERSISTENT_H +#define AKONADISEARCHPERSISTENT_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the search_store search_delete commands. +*/ +class SearchPersistent : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/searchresult.cpp b/src/server/handler/searchresult.cpp new file mode 100644 index 0000000..07545a3 --- /dev/null +++ b/src/server/handler/searchresult.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2013 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "searchresult.h" + +#include "connection.h" +#include "storage/selectquerybuilder.h" +#include "storage/itemqueryhelper.h" +#include "search/searchtaskmanager.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool SearchResult::parseStream() +{ + Protocol::SearchResultCommand cmd(m_command); + + if (!checkScopeConstraints(cmd.result(), Scope::Uid | Scope::Rid)) { + return fail(cmd.searchId(), QStringLiteral("Only UID or RID scopes are allowed in SEARECH_RESULT")); + } + + QSet ids; + if (cmd.result().scope() == Scope::Rid && !cmd.result().isEmpty()) { + QueryBuilder qb(PimItem::tableName()); + qb.addColumn(PimItem::idFullColumnName()); + ItemQueryHelper::remoteIdToQuery(cmd.result().ridSet(), connection()->context(), qb); + qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::Equals, cmd.collectionId()); + + if (!qb.exec()) { + return fail(cmd.searchId(), QStringLiteral("Failed to convert RID to UID")); + } + + QSqlQuery query = qb.query(); + while (query.next()) { + ids << query.value(0).toLongLong(); + } + } else if (cmd.result().scope() == Scope::Uid && !cmd.result().isEmpty()) { + const ImapSet result = cmd.result().uidSet(); + Q_FOREACH (const ImapInterval &interval, result.intervals()) { + for (qint64 i = interval.begin(); i <= interval.end(); ++i) { + ids.insert(i); + } + } + } + SearchTaskManager::instance()->pushResults(cmd.searchId(), ids, connection()); + + return successResponse(); +} + +bool SearchResult::fail(const QByteArray &searchId, const QString &error) +{ + SearchTaskManager::instance()->pushResults(searchId, QSet(), connection()); + return failureResponse(error); +} diff --git a/src/server/handler/searchresult.h b/src/server/handler/searchresult.h new file mode 100644 index 0000000..308cdfa --- /dev/null +++ b/src/server/handler/searchresult.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef AKONADI_SEARCHRESULT_H +#define AKONADI_SEARCHRESULT_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the search_result command +*/ +class SearchResult : public Handler +{ + Q_OBJECT +public: + bool parseStream(); + +private: + bool fail(const QByteArray &searchId, const QString &error); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SEARCHRESULT_H diff --git a/src/server/handler/status.cpp b/src/server/handler/status.cpp new file mode 100644 index 0000000..77a2cc9 --- /dev/null +++ b/src/server/handler/status.cpp @@ -0,0 +1,50 @@ +/*************************************************************************** + * Copyright (C) 2006 by Ingo Kloecker * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "status.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "storage/datastore.h" +#include "storage/countquerybuilder.h" +#include "storage/collectionstatistics.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool Status::parseStream() +{ + Protocol::FetchCollectionStatsCommand cmd(m_command); + + const Collection col = HandlerHelper::collectionFromScope(cmd.collection(), connection()); + if (!col.isValid()) { + return failureResponse("No status for this folder"); + } + + const CollectionStatistics::Statistics stats = CollectionStatistics::self()->statistics(col); + if (stats.count == -1) { + return failureResponse("Failed to query statistics."); + } + + return successResponse(Protocol::FetchCollectionStatsResponse(stats.count, + stats.count - stats.read, + stats.size)); +} diff --git a/src/server/handler/status.h b/src/server/handler/status.h new file mode 100644 index 0000000..60280d0 --- /dev/null +++ b/src/server/handler/status.h @@ -0,0 +1,42 @@ +/*************************************************************************** + * Copyright (C) 2006 by Ingo Kloecker * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef AKONADISTATUS_H +#define AKONADISTATUS_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the STATUS command. + */ +class Status : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/store.cpp b/src/server/handler/store.cpp new file mode 100644 index 0000000..b987cf3 --- /dev/null +++ b/src/server/handler/store.cpp @@ -0,0 +1,376 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "store.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "storage/datastore.h" +#include "storage/transaction.h" +#include "storage/itemqueryhelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/parthelper.h" +#include "storage/dbconfig.h" +#include "storage/itemretriever.h" +#include "storage/parttypehelper.h" +#include "storage/partstreamer.h" +#include + +#include +#include + +#include "akonadiserver_debug.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +static bool payloadChanged(const QSet &changes) +{ + Q_FOREACH (const QByteArray &change, changes) { + if (change.startsWith(AKONADI_PARAM_PLD)) { + return true; + } + } + return false; +} + + +bool Store::replaceFlags(const PimItem::List &item, const QSet &flags, bool &flagsChanged) +{ + Flag::List flagList = HandlerHelper::resolveFlags(flags); + DataStore *store = connection()->storageBackend(); + + if (!store->setItemsFlags(item, flagList, &flagsChanged)) { + akDebug() << "Store::replaceFlags: Unable to replace flags"; + return false; + } + + return true; +} + +bool Store::addFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged) +{ + const Flag::List flagList = HandlerHelper::resolveFlags(flags); + DataStore *store = connection()->storageBackend(); + + if (!store->appendItemsFlags(items, flagList, &flagsChanged)) { + akDebug() << "Store::addFlags: Unable to add new item flags"; + return false; + } + return true; +} + +bool Store::deleteFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged) +{ + DataStore *store = connection()->storageBackend(); + + QVector flagList; + flagList.reserve(flags.size()); + for (auto iter = flags.cbegin(), end = flags.cend(); iter != end; ++iter) { + Flag flag = Flag::retrieveByName(QString::fromUtf8(*iter)); + if (!flag.isValid()) { + continue; + } + + flagList.append(flag); + } + + if (!store->removeItemsFlags(items, flagList, &flagsChanged)) { + akDebug() << "Store::deleteFlags: Unable to remove item flags"; + return false; + } + return true; +} + +bool Store::replaceTags(const PimItem::List &item, const Scope &tags, bool &tagsChanged) +{ + const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()); + if (!connection()->storageBackend()->setItemsTags(item, tagList, &tagsChanged)) { + akDebug() << "Store::replaceTags: unable to replace tags"; + return false; + } + return true; +} + +bool Store::addTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged) +{ + const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()); + if (!connection()->storageBackend()->appendItemsTags(items, tagList, &tagsChanged)) { + akDebug() << "Store::addTags: Unable to add new item tags"; + return false; + } + return true; +} + +bool Store::deleteTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged) +{ + const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()); + if (!connection()->storageBackend()->removeItemsTags(items, tagList, &tagsChanged)) { + akDebug() << "Store::deleteTags: Unable to remove item tags"; + return false; + } + return true; +} + +bool Store::parseStream() +{ + Protocol::ModifyItemsCommand cmd(m_command); + + //parseCommand(); + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store); + ExternalPartStorageTransaction storageTrx; + // Set the same modification time for each item. + const QDateTime modificationtime = QDateTime::currentDateTimeUtc(); + + // retrieve selected items + SelectQueryBuilder qb; + ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb); + if (!qb.exec()) { + return failureResponse("Unable to retrieve items"); + } + PimItem::List pimItems = qb.result(); + if (pimItems.isEmpty()) { + return failureResponse("No items found"); + } + + for (int i = 0; i < pimItems.size(); ++i) { + if (cmd.oldRevision() > -1) { + // check for conflicts if a resources tries to overwrite an item with dirty payload + const PimItem &pimItem = pimItems.at(i); + if (connection()->isOwnerResource(pimItem)) { + if (pimItem.dirty()) { + const QString error = QStringLiteral("[LRCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with dirty payload, aborting STORE."); + return failureResponse( + error.arg(pimItem.collection().resource().name()) + .arg(pimItem.id()) + .arg(pimItem.remoteId()).arg(pimItem.collectionId())); + } + } + + // check and update revisions + if (pimItems.at(i).rev() != (int) cmd.oldRevision()) { + return failureResponse("[LLCONFLICT] Item was modified elsewhere, aborting STORE."); + } + } + } + + PimItem &item = pimItems.first(); + + QSet changes; + qint64 partSizes = 0; + qint64 size = 0; + + bool flagsChanged = false; + bool tagsChanged = false; + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedFlags) { + if (!addFlags(pimItems, cmd.addedFlags(), flagsChanged)) { + return failureResponse("Unable to add item flags"); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedFlags) { + if (!deleteFlags(pimItems, cmd.removedFlags(), flagsChanged)) { + return failureResponse("Unable to remove item flags"); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Flags) { + if (!replaceFlags(pimItems, cmd.flags(), flagsChanged)) { + return failureResponse("Unable to reset flags"); + } + } + + if (flagsChanged) { + changes << AKONADI_PARAM_FLAGS; + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedTags) { + if (!addTags(pimItems, cmd.addedTags(), tagsChanged)) { + return failureResponse("Unable to add item tags"); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedTags) { + if (!deleteTags(pimItems, cmd.removedTags(), tagsChanged)) { + return failureResponse("Unabel to remove item tags"); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Tags) { + if (!replaceTags(pimItems, cmd.tags(), tagsChanged)) { + return failureResponse("Unable to reset item tags"); + } + } + + if (tagsChanged) { + changes << AKONADI_PARAM_TAGS; + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteID) { + if (item.remoteId() != cmd.remoteId()) { + if (!connection()->isOwnerResource(item)) { + return failureResponse("Only resources can modify remote identifiers"); + } + item.setRemoteId(cmd.remoteId()); + changes << AKONADI_PARAM_REMOTEID; + } + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::GID) { + if (item.gid() != cmd.gid()) { + item.setGid(cmd.gid()); + } + changes << AKONADI_PARAM_GID; + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteRevision) { + if (item.remoteRevision() != cmd.remoteRevision()) { + if (!connection()->isOwnerResource(item)) { + return failureResponse("Only resources can modify remote revisions"); + } + item.setRemoteRevision(cmd.remoteRevision()); + changes << AKONADI_PARAM_REMOTEREVISION; + } + } + + if (item.isValid() && !cmd.dirty()) { + item.setDirty(false); + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Size) { + size = cmd.itemSize(); + changes << AKONADI_PARAM_SIZE; + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedParts) { + if (!cmd.removedParts().isEmpty()) { + if (!store->removeItemParts(item, cmd.removedParts())) { + return failureResponse("Unable to remove item parts"); + } + Q_FOREACH (const QByteArray &part, cmd.removedParts()) { + changes.insert(part); + } + } + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Parts) { + PartStreamer streamer(connection(), item, this); + connect(&streamer, &PartStreamer::responseAvailable, + this, static_cast(&Handler::sendResponse)); + Q_FOREACH (const QByteArray &partName, cmd.parts()) { + qint64 partSize = 0; + if (!streamer.stream(true, partName, partSize)) { + return failureResponse(streamer.error()); + } + + changes.insert(partName); + partSizes += partSize; + } + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Attributes) { + PartStreamer streamer(connection(), item, this); + connect(&streamer, &PartStreamer::responseAvailable, + this, static_cast(&Handler::sendResponse)); + const Protocol::Attributes attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + bool changed = false; + if (!streamer.streamAttribute(true, iter.key(), iter.value(), &changed)) { + return failureResponse(streamer.error()); + } + + if (changed) { + changes.insert(iter.key()); + } + } + } + + QDateTime datetime; + if (!changes.isEmpty() || cmd.invalidateCache() || !cmd.dirty()) { + + // update item size + if (pimItems.size() == 1 && (size > 0 || partSizes > 0)) { + pimItems.first().setSize(qMax(size, partSizes)); + } + + const bool onlyRemoteIdChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEID)); + const bool onlyRemoteRevisionChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEREVISION)); + const bool onlyRemoteIdAndRevisionChanged = (changes.size() == 2 && changes.contains(AKONADI_PARAM_REMOTEID) + && changes.contains(AKONADI_PARAM_REMOTEREVISION)); + const bool onlyFlagsChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_FLAGS)); + const bool onlyGIDChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_GID)); + // If only the remote id and/or the remote revision changed, we don't have to increase the REV, + // because these updates do not change the payload and can only be done by the owning resource -> no conflicts possible + const bool revisionNeedsUpdate = (!changes.isEmpty() && !onlyRemoteIdChanged && !onlyRemoteRevisionChanged && !onlyRemoteIdAndRevisionChanged && !onlyGIDChanged); + + // run update query and prepare change notifications + for (int i = 0; i < pimItems.count(); ++i) { + + if (revisionNeedsUpdate) { + pimItems[i].setRev(pimItems[i].rev() + 1); + } + + PimItem &item = pimItems[i]; + item.setDatetime(modificationtime); + item.setAtime(modificationtime); + if (!connection()->isOwnerResource(item) && payloadChanged(changes)) { + item.setDirty(true); + } + if (!item.update()) { + return failureResponse("Unable to write item changes into the database"); + } + + if (cmd.invalidateCache()) { + if (!store->invalidateItemCache(item)) { + return failureResponse("Unable to invalidate item cache in the database"); + } + } + + // flags change notification went separatly during command parsing + // GID-only changes are ignored to prevent resources from updating their storage when no actual change happened + if (cmd.notify() && !changes.isEmpty() && !onlyFlagsChanged && !onlyGIDChanged) { + // Don't send FLAGS notification in itemChanged + changes.remove(AKONADI_PARAM_FLAGS); + store->notificationCollector()->itemChanged(item, changes); + } + + if (!cmd.noResponse()) { + sendResponse(Protocol::ModifyItemsResponse(item.id(), item.rev())); + } + } + + if (!transaction.commit()) { + return failureResponse("Cannot commit transaction."); + } + // Always commit storage changes (deletion) after DB transaction + storageTrx.commit(); + + datetime = modificationtime; + } else { + datetime = pimItems.first().datetime(); + } + + return successResponse(datetime); +} diff --git a/src/server/handler/store.h b/src/server/handler/store.h new file mode 100644 index 0000000..17d6731 --- /dev/null +++ b/src/server/handler/store.h @@ -0,0 +1,113 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * Copyright (C) 2009 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADISTORE_H +#define AKONADISTORE_H + +#include "handler.h" +#include "entities.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the item modification command. + +

Syntax

+ One of the following three: + @verbatim + STORE + UID MOVE [] + RID MOVE [] + @endverbatim + + @c revision-check is one of the following and allowed iff one item was selected for modification: + @verbatim + NOREV + REV + @endverbatim + + @c modifcations is a parenthesized list containing any of the following: + @verbatim + SIZE + [+-]FLAGS + REMOTEID + REMOTEREVISION + GID + DIRTY false + INVALIDATECACHE + + + @endverbatim + +

Semantics

+ Modifies the selected items. Item selection can happen within the usual three scopes: + - based on a uid set relative to the currently selected collection + - based on a uid set (UID) + - based on a list of remote identifiers within the currently selected collection (RID) + + The following item properties can be modified: + - the remote identifier (@c REMOTEID) + - the remote revision (@c REMOTEREVISION) + - the global identifier (@c GID) + - resetting the dirty flag indication local changes not yet replicated to the backend (@c DIRTY) + - adding/deleting/setting item flags (@c FLAGS) + - setting the item size hint (@c SIZE) + - changing item attributes + - changing item payload parts + + If multiple items are selected only the following operations are valid: + - adding flags + - removing flags + - settings flags + + The following operations are only allowed by resources: + - resetting the dirty flag + - invalidating the cache + - modifying the remote identifier + - modifying the remote revision + + Conflict detection: + - only available when modifying a single item + - requires the previous item revision to be provided (@c REV) +*/ + +class Store : public Handler +{ + Q_OBJECT + +public: + bool parseStream(); + +private: + bool replaceFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged); + bool addFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged); + bool deleteFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged); + bool replaceTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged); + bool addTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged); + bool deleteTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/tagappend.cpp b/src/server/handler/tagappend.cpp new file mode 100644 index 0000000..42448d5 --- /dev/null +++ b/src/server/handler/tagappend.cpp @@ -0,0 +1,138 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "tagappend.h" + +#include "tagfetchhelper.h" +#include "connection.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" +#include "storage/countquerybuilder.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool TagAppend::parseStream() +{ + Protocol::CreateTagCommand cmd(m_command); + + if (!cmd.remoteId().isEmpty() && !connection()->context()->resource().isValid()) { + return failureResponse("Only resources can create tags with remote ID"); + } + + TagType tagType; + if (!cmd.type().isEmpty()) { + const QString typeName = QString::fromUtf8(cmd.type()); + tagType = TagType::retrieveByName(typeName); + if (!tagType.isValid()) { + TagType t(typeName); + if (!t.insert()) { + return failureResponse(QStringLiteral("Unable to create tagtype '") % typeName % QStringLiteral("'")); + } + tagType = t; + } + } + + qint64 tagId = -1; + const QString gid = QString::fromUtf8(cmd.gid()); + if (cmd.merge()) { + QueryBuilder qb(Tag::tableName()); + qb.addColumn(Tag::idColumn()); + qb.addValueCondition(Tag::gidColumn(), Query::Equals, gid); + if (!qb.exec()) { + return failureResponse("Unable to list tags"); + } + if (qb.query().next()) { + tagId = qb.query().value(0).toLongLong(); + } + } + if (tagId < 0) { + Tag insertedTag; + insertedTag.setGid(gid); + if (cmd.parentId() >= 0) { + insertedTag.setParentId(cmd.parentId()); + } + if (tagType.isValid()) { + insertedTag.setTypeId(tagType.id()); + } + if (!insertedTag.insert(&tagId)) { + return failureResponse("Failed to store tag"); + } + + const Protocol::Attributes attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + TagAttribute attribute; + attribute.setTagId(tagId); + attribute.setType(iter.key()); + attribute.setValue(iter.value()); + if (!attribute.insert()) { + return failureResponse("Failed to store tag attribute"); + } + } + + DataStore::self()->notificationCollector()->tagAdded(insertedTag); + } + + if (!cmd.remoteId().isEmpty()) { + const qint64 resourceId = connection()->context()->resource().id(); + + CountQueryBuilder qb(TagRemoteIdResourceRelation::tableName()); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, tagId); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), Query::Equals, resourceId); + if (!qb.exec()) { + return failureResponse("Failed to query for existing TagRemoteIdResourceRelation entries"); + } + const bool exists = (qb.result() > 0); + + //If the relation is already existing simply update it (can happen if a resource simply creates the tag again while enabling merge) + bool ret = false; + if (exists) { + //Simply using update() doesn't work since TagRemoteIdResourceRelation only takes the tagId for identification of the column + QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Update); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, tagId); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), Query::Equals, resourceId); + qb.setColumnValue(TagRemoteIdResourceRelation::remoteIdColumn(), cmd.remoteId()); + ret = qb.exec(); + } else { + TagRemoteIdResourceRelation rel; + rel.setTagId(tagId); + rel.setResourceId(resourceId); + rel.setRemoteId(QString::fromUtf8(cmd.remoteId())); + ret = rel.insert(); + } + if (!ret) { + return failureResponse("Failed to store tag remote ID"); + } + } + + // FIXME BIN + Scope scope; + ImapSet set; + set.add(QVector() << tagId); + scope.setUidSet(set); + TagFetchHelper helper(connection(), scope); + if (!helper.fetchTags()) { + return failureResponse("Failed to fetch the new tag"); + } + + return successResponse(); +} diff --git a/src/server/handler/tagappend.h b/src/server/handler/tagappend.h new file mode 100644 index 0000000..3fdedec --- /dev/null +++ b/src/server/handler/tagappend.h @@ -0,0 +1,39 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_TAGAPPEND_H +#define AKONADI_TAGAPPEND_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +class TagAppend : public Handler +{ + Q_OBJECT + +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_TAGAPPEND_H diff --git a/src/server/handler/tagfetch.cpp b/src/server/handler/tagfetch.cpp new file mode 100644 index 0000000..228c907 --- /dev/null +++ b/src/server/handler/tagfetch.cpp @@ -0,0 +1,41 @@ +/*************************************************************************** + * Copyright (C) 2014 by Daniel Vrátil * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "tagfetch.h" +#include "connection.h" +#include "tagfetchhelper.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool TagFetch::parseStream() +{ + Protocol::FetchTagsCommand cmd(m_command); + + if (!checkScopeConstraints(cmd.scope(), Scope::Uid)) { + return failureResponse("Only UID-based TAGFETCH is supported"); + } + + TagFetchHelper helper(connection(), cmd.scope()); + if (!helper.fetchTags()) { + return failureResponse("Failed to fetch tags"); + } + + return successResponse(); +} diff --git a/src/server/handler/tagfetch.h b/src/server/handler/tagfetch.h new file mode 100644 index 0000000..dac9405 --- /dev/null +++ b/src/server/handler/tagfetch.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) 2014 by Daniel Vrátil * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADIFETCHTAG_H +#define AKONADIFETCHTAG_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the FETCHTAG command. + */ +class TagFetch : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handler/tagfetchhelper.cpp b/src/server/handler/tagfetchhelper.cpp new file mode 100644 index 0000000..33daca4 --- /dev/null +++ b/src/server/handler/tagfetchhelper.cpp @@ -0,0 +1,156 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "tagfetchhelper.h" +#include "handler.h" +#include "connection.h" +#include "utils.h" +#include "storage/querybuilder.h" +#include "storage/tagqueryhelper.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +TagFetchHelper::TagFetchHelper(Connection *connection, const Scope &scope) + : QObject() + , mConnection(connection) + , mScope(scope) +{ +} + +QSqlQuery TagFetchHelper::buildAttributeQuery() const +{ + QueryBuilder qb(TagAttribute::tableName()); + qb.addColumn(TagAttribute::tagIdFullColumnName()); + qb.addColumn(TagAttribute::typeFullColumnName()); + qb.addColumn(TagAttribute::valueFullColumnName()); + qb.addSortColumn(TagAttribute::tagIdFullColumnName(), Query::Descending); + qb.addJoin(QueryBuilder::InnerJoin, Tag::tableName(), + TagAttribute::tagIdFullColumnName(), Tag::idFullColumnName()); + TagQueryHelper::scopeToQuery(mScope, mConnection->context(), qb); + + if (!qb.exec()) { + throw HandlerException("Unable to list tag attributes"); + } + + qb.query().next(); + return qb.query(); +} + +QSqlQuery TagFetchHelper::buildAttributeQuery(qint64 id) +{ + QueryBuilder qb(TagAttribute::tableName()); + qb.addColumn(TagAttribute::tagIdColumn()); + qb.addColumn(TagAttribute::typeColumn()); + qb.addColumn(TagAttribute::valueColumn()); + qb.addSortColumn(TagAttribute::tagIdColumn(), Query::Descending); + + qb.addValueCondition(TagAttribute::tagIdColumn(), Query::Equals, id); + + if (!qb.exec()) { + throw HandlerException("Unable to list tag attributes"); + } + + qb.query().next(); + return qb.query(); +} + +QSqlQuery TagFetchHelper::buildTagQuery() +{ + QueryBuilder qb(Tag::tableName()); + qb.addColumns(Tag::fullColumnNames()); + + qb.addJoin(QueryBuilder::InnerJoin, TagType::tableName(), + Tag::typeIdFullColumnName(), TagType::idFullColumnName()); + qb.addColumn(TagType::nameFullColumnName()); + + // Expose tag's remote ID only to resources + if (mConnection->context()->resource().isValid()) { + qb.addColumn(TagRemoteIdResourceRelation::remoteIdFullColumnName()); + Query::Condition joinCondition; + joinCondition.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), + Query::Equals, mConnection->context()->resource().id()); + joinCondition.addColumnCondition(TagRemoteIdResourceRelation::tagIdFullColumnName(), + Query::Equals, Tag::idFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, TagRemoteIdResourceRelation::tableName(), joinCondition); + } + + qb.addSortColumn(Tag::idFullColumnName(), Query::Descending); + TagQueryHelper::scopeToQuery(mScope, mConnection->context(), qb); + if (!qb.exec()) { + throw HandlerException("Unable to list tags"); + } + + qb.query().next(); + return qb.query(); +} + +QMap TagFetchHelper::fetchTagAttributes(qint64 tagId) +{ + QMap attributes; + + QSqlQuery attributeQuery = buildAttributeQuery(tagId); + while (attributeQuery.isValid()) { + attributes.insert(Utils::variantToByteArray(attributeQuery.value(1)), + Utils::variantToByteArray(attributeQuery.value(2))); + attributeQuery.next(); + } + return attributes; +} + +bool TagFetchHelper::fetchTags() +{ + + QSqlQuery tagQuery = buildTagQuery(); + QSqlQuery attributeQuery = buildAttributeQuery(); + + while (tagQuery.isValid()) { + const qint64 tagId = tagQuery.value(0).toLongLong(); + Protocol::FetchTagsResponse response(tagId); + response.setGid(Utils::variantToByteArray(tagQuery.value(1))); + response.setParentId(tagQuery.value(2).toLongLong()); + response.setType(Utils::variantToByteArray(tagQuery.value(4))); + if (mConnection->context()->resource().isValid()) { + response.setRemoteId(Utils::variantToByteArray(tagQuery.value(5))); + } + + QMap tagAttributes; + while (attributeQuery.isValid()) { + const qint64 id = attributeQuery.value(0).toLongLong(); + if (id > tagId) { + attributeQuery.next(); + continue; + } else if (id < tagId) { + break; + } + + tagAttributes.insert(Utils::variantToByteArray(attributeQuery.value(1)), + Utils::variantToByteArray(attributeQuery.value(2))); + attributeQuery.next(); + } + + response.setAttributes(tagAttributes); + + mConnection->sendResponse(response); + + tagQuery.next(); + } + + return true; +} diff --git a/src/server/handler/tagfetchhelper.h b/src/server/handler/tagfetchhelper.h new file mode 100644 index 0000000..bc51cb7 --- /dev/null +++ b/src/server/handler/tagfetchhelper.h @@ -0,0 +1,61 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_TAGFETCHHELPER_H +#define AKONADI_TAGFETCHHELPER_H + +#include +#include + +#include + +namespace Akonadi { + +class ImapSet; + +namespace Server { + +class Connection; +class Response; + +class TagFetchHelper : public QObject +{ + Q_OBJECT + +public: + TagFetchHelper(Connection *connection, const Scope &scope); + + bool fetchTags(); + + static QMap fetchTagAttributes(qint64 tagId); + +private: + QSqlQuery buildTagQuery(); + QSqlQuery buildAttributeQuery() const; + static QSqlQuery buildAttributeQuery(qint64 id); + +private: + Connection *mConnection; + Scope mScope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_TAGFETCHHELPER_H diff --git a/src/server/handler/tagremove.cpp b/src/server/handler/tagremove.cpp new file mode 100644 index 0000000..d74ab9e --- /dev/null +++ b/src/server/handler/tagremove.cpp @@ -0,0 +1,53 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "tagremove.h" + +#include "storage/selectquerybuilder.h" +#include "storage/queryhelper.h" +#include "storage/datastore.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool TagRemove::parseStream() +{ + Protocol::DeleteTagCommand cmd(m_command); + + if (!checkScopeConstraints(cmd.tag(), Scope::Uid)) { + return failureResponse("Only UID-based TAGREMOVE is supported"); + } + + SelectQueryBuilder tagQuery; + QueryHelper::setToQuery(cmd.tag().uidSet(), Tag::idFullColumnName(), tagQuery); + if (!tagQuery.exec()) { + return failureResponse("Failed to obtain tags"); + } + + const Tag::List tags = tagQuery.result(); + + if (!DataStore::self()->removeTags(tags)) { + return failureResponse("Failed to remove tags"); + } + + return successResponse(); +} diff --git a/src/server/handler/tagremove.h b/src/server/handler/tagremove.h new file mode 100644 index 0000000..b264210 --- /dev/null +++ b/src/server/handler/tagremove.h @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_TAGREMOVE_H +#define AKONADI_TAGREMOVE_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +class TagRemove : public Handler +{ + Q_OBJECT +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_TAGREMOVE_H diff --git a/src/server/handler/tagstore.cpp b/src/server/handler/tagstore.cpp new file mode 100644 index 0000000..afd6131 --- /dev/null +++ b/src/server/handler/tagstore.cpp @@ -0,0 +1,161 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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 "tagstore.h" + +#include "tagfetchhelper.h" +#include "connection.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool TagStore::parseStream() +{ + Protocol::ModifyTagCommand cmd(m_command); + + Tag changedTag = Tag::retrieveById(cmd.tagId()); + if (!changedTag.isValid()) { + return failureResponse("No such tag"); + } + + QSet changes; + + // Retrieve all tag's attributes + const TagAttribute::List attributes = TagAttribute::retrieveFiltered(TagAttribute::tagIdFullColumnName(), cmd.tagId()); + QMap attributesMap; + Q_FOREACH (const TagAttribute &attribute, attributes) { + attributesMap.insert(attribute.type(), attribute); + } + + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::ParentId) { + if (cmd.parentId() != changedTag.parentId()) { + changedTag.setParentId(cmd.parentId()); + changes << AKONADI_PARAM_PARENT; + } + } + + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::Type) { + TagType type = TagType::retrieveById(changedTag.typeId()); + const QString newTypeName = QString::fromUtf8(cmd.type()); + if (newTypeName != type.name()) { + TagType newType = TagType::retrieveByName(newTypeName); + if (!newType.isValid()) { + newType.setName(newTypeName); + if (!newType.insert()) { + return failureResponse("Failed to create new tag type"); + } + } + changedTag.setTagType(newType); + changes << AKONADI_PARAM_MIMETYPE; + } + } + + bool tagRemoved = false; + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::RemoteId) { + if (!connection()->context()->resource().isValid()) { + return failureResponse("Only resources can change tag remote ID"); + } + + //Simply using remove() doesn't work since we need two arguments + QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Delete); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, cmd.tagId()); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), Query::Equals,connection()->context()->resource().id()); + qb.exec(); + + if (!cmd.remoteId().isEmpty()) { + TagRemoteIdResourceRelation remoteIdRelation; + remoteIdRelation.setRemoteId(QString::fromUtf8(cmd.remoteId())); + remoteIdRelation.setResourceId(connection()->context()->resource().id()); + remoteIdRelation.setTag(changedTag); + if (!remoteIdRelation.insert()) { + return failureResponse("Failed to insert remotedid resource relation"); + } + } else { + const int tagRidsCount = TagRemoteIdResourceRelation::count(TagRemoteIdResourceRelation::tagIdColumn(), changedTag.id()); + // We just removed the last RID of the tag, which means that no other + // resource owns this tag, so we have to remove it to simulate tag + // removal + if (tagRidsCount == 0) { + if (!DataStore::self()->removeTags(Tag::List() << changedTag)) { + return failureResponse("Failed to remove tag"); + } + tagRemoved = true; + } + } + // Do not notify about remoteid changes, otherwise we bounce back and forth + // between resources recording it's change and updating the remote id. + } + + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::RemovedAttributes) { + Q_FOREACH (const QByteArray &attrName, cmd.removedAttributes()) { + TagAttribute attribute = attributesMap.value(attrName); + TagAttribute::remove(attribute.id()); + changes << attrName; + } + } + + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::Attributes) { + const QMap attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + if (attributesMap.contains(iter.key())) { + TagAttribute attribute = attributesMap.value(iter.key()); + attribute.setValue(iter.value()); + if (!attribute.update()) { + return failureResponse("Failed to update attribute"); + } + } else { + TagAttribute attribute; + attribute.setTagId(cmd.tagId()); + attribute.setType(iter.key()); + attribute.setValue(iter.value()); + if (!attribute.insert()) { + return failureResponse("Failed to insert attribute"); + } + } + changes << iter.key(); + } + } + + if (!tagRemoved) { + if (!changedTag.update()) { + return failureResponse("Failed to store changes"); + } + if (!changes.isEmpty()) { + DataStore::self()->notificationCollector()->tagChanged(changedTag); + } + + ImapSet set; + set.add(QVector() << cmd.tagId()); + + Scope scope; + scope.setUidSet(set); + TagFetchHelper helper(connection(), scope); + if (!helper.fetchTags()) { + return failureResponse("Failed to fetch response"); + } + } else { + successResponse(); + } + + return successResponse(); +} diff --git a/src/server/handler/tagstore.h b/src/server/handler/tagstore.h new file mode 100644 index 0000000..f9950ea --- /dev/null +++ b/src/server/handler/tagstore.h @@ -0,0 +1,39 @@ +/* + Copyright (c) 2014 Daniel Vrátil + + 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_TAGSTORE_H +#define AKONADI_TAGSTORE_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +class TagStore : public Handler +{ + Q_OBJECT + +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_TAGSTORE_H diff --git a/src/server/handler/transaction.cpp b/src/server/handler/transaction.cpp new file mode 100644 index 0000000..52c84a8 --- /dev/null +++ b/src/server/handler/transaction.cpp @@ -0,0 +1,60 @@ +/* + 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 "transaction.h" +#include "connection.h" +#include "storage/datastore.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool TransactionHandler::parseStream() +{ + Protocol::TransactionCommand cmd(m_command); + + DataStore *store = connection()->storageBackend(); + + switch (cmd.mode()) { + case Protocol::TransactionCommand::Invalid: + return failureResponse("Invalid operation"); + case Protocol::TransactionCommand::Begin: + if (!store->beginTransaction()) { + return failureResponse("Unable to begin transaction."); + } + break; + case Protocol::TransactionCommand::Rollback: + if (!store->inTransaction()) { + return failureResponse("There is no transaction in progress."); + } + if (!store->rollbackTransaction()) { + return failureResponse("Unable to roll back transaction."); + } + break; + case Protocol::TransactionCommand::Commit: + if (!store->inTransaction()) { + return failureResponse("There is no transaction in progress."); + } + if (!store->commitTransaction()) { + return failureResponse("Unable to commit transaction."); + } + break; + } + + return successResponse(); +} diff --git a/src/server/handler/transaction.h b/src/server/handler/transaction.h new file mode 100644 index 0000000..b3e5336 --- /dev/null +++ b/src/server/handler/transaction.h @@ -0,0 +1,45 @@ +/* + 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. +*/ + +#ifndef AKONADI_TRANSACTION_HANDLER_H +#define AKONADI_TRANSACTION_HANDLER_H + +#include "handler.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for transaction commands (BEGIN, COMMIT, ROLLBACK). +*/ +class TransactionHandler : public Handler +{ + Q_OBJECT + Q_ENUMS(Mode) + +public: + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/handlerhelper.cpp b/src/server/handlerhelper.cpp new file mode 100644 index 0000000..156b15d --- /dev/null +++ b/src/server/handlerhelper.cpp @@ -0,0 +1,410 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "handlerhelper.h" +#include "storage/countquerybuilder.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" +#include "storage/collectionstatistics.h" +#include "storage/queryhelper.h" +#include "storage/collectionqueryhelper.h" +#include "commandcontext.h" +#include "handler.h" +#include "connection.h" +#include "utils.h" + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Collection HandlerHelper::collectionFromIdOrName(const QByteArray &id) +{ + // id is a number + bool ok = false; + qint64 collectionId = id.toLongLong(&ok); + if (ok) { + return Collection::retrieveById(collectionId); + } + + // id is a path + QString path = QString::fromUtf8(id); // ### should be UTF-7 for real IMAP compatibility + + const QStringList pathParts = path.split(QLatin1Char('/'), QString::SkipEmptyParts); + Collection col; + Q_FOREACH (const QString &part, pathParts) { + SelectQueryBuilder qb; + qb.addValueCondition(Collection::nameColumn(), Query::Equals, part); + if (col.isValid()) { + qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, col.id()); + } else { + qb.addValueCondition(Collection::parentIdColumn(), Query::Is, QVariant()); + } + if (!qb.exec()) { + return Collection(); + } + Collection::List list = qb.result(); + if (list.count() != 1) { + return Collection(); + } + col = list.first(); + } + return col; +} + +QString HandlerHelper::pathForCollection(const Collection &col) +{ + QStringList parts; + Collection current = col; + while (current.isValid()) { + parts.prepend(current.name()); + current = current.parent(); + } + return parts.join(QStringLiteral("/")); +} + +Protocol::CachePolicy HandlerHelper::cachePolicyResponse(const Collection& col) +{ + Protocol::CachePolicy cachePolicy; + cachePolicy.setInherit(col.cachePolicyInherit()); + cachePolicy.setCacheTimeout(col.cachePolicyCacheTimeout()); + cachePolicy.setCheckInterval(col.cachePolicyCheckInterval()); + cachePolicy.setLocalParts(col.cachePolicyLocalParts().split(QLatin1Char(' '))); + cachePolicy.setSyncOnDemand(col.cachePolicySyncOnDemand()); + return cachePolicy; +} + +Protocol::FetchCollectionsResponse HandlerHelper::fetchCollectionsResponse(const Collection &col) +{ + QStringList mimeTypes; + mimeTypes.reserve(col.mimeTypes().size()); + Q_FOREACH (const MimeType &mt, col.mimeTypes()) { + mimeTypes << mt.name(); + } + + return fetchCollectionsResponse(col, col.attributes(), false, 0, QStack(), + QStack(), false, mimeTypes); +} + +Protocol::FetchCollectionsResponse HandlerHelper::fetchCollectionsResponse(const Collection &col, + const CollectionAttribute::List &attrs, + bool includeStatistics, + int ancestorDepth, + const QStack &ancestors, + const QStack &ancestorAttributes, + bool isReferenced, + const QStringList &mimeTypes) +{ + Protocol::FetchCollectionsResponse response(col.id()); + response.setParentId(col.parentId()); + response.setName(col.name()); + response.setMimeTypes(mimeTypes); + response.setRemoteId(col.remoteId()); + response.setRemoteRevision(col.remoteRevision()); + response.setResource(col.resource().name()); + response.setIsVirtual(col.isVirtual()); + + if (includeStatistics) { + const CollectionStatistics::Statistics stats = CollectionStatistics::self()->statistics(col); + if (stats.count > -1) { + Protocol::FetchCollectionStatsResponse statsResponse(stats.count, + stats.count - stats.read, + stats.size); + response.setStatistics(statsResponse); + } + } + + if (!col.queryString().isEmpty()) { + response.setSearchQuery(col.queryString()); + QVector searchCols; + const QStringList searchColIds = col.queryCollections().split(QLatin1Char(' ')); + searchCols.reserve(searchColIds.size()); + Q_FOREACH (const QString &searchColId, searchColIds) { + searchCols << searchColId.toLongLong(); + } + response.setSearchCollections(searchCols); + } + + Protocol::CachePolicy cachePolicy = cachePolicyResponse(col); + response.setCachePolicy(cachePolicy); + + if (ancestorDepth) { + QVector ancestorList + = HandlerHelper::ancestorsResponse(ancestorDepth, ancestors, ancestorAttributes); + response.setAncestors(ancestorList); + } + + response.setReferenced(isReferenced); + response.setEnabled(col.enabled()); + response.setDisplayPref(col.displayPref()); + response.setSyncPref(col.syncPref()); + response.setIndexPref(col.indexPref()); + + QMap ra; + for (const CollectionAttribute &attr : attrs) { + ra.insert(attr.type(), attr.value()); + } + response.setAttributes(ra); + + return response; +} + +QVector HandlerHelper::ancestorsResponse(int ancestorDepth, + const QStack &_ancestors, + const QStack &_ancestorsAttributes) +{ + QVector rv; + if (ancestorDepth > 0) { + QStack ancestors(_ancestors); + QStack ancestorAttributes(_ancestorsAttributes); + for (int i = 0; i < ancestorDepth; ++i) { + if (ancestors.isEmpty()) { + rv << Protocol::Ancestor(0); + break; + } + const Collection c = ancestors.pop(); + Protocol::Ancestor a(c.id()); + a.setRemoteId(c.remoteId()); + a.setName(c.name()); + if (!ancestorAttributes.isEmpty()) { + QMap attrs; + Q_FOREACH (const CollectionAttribute &attr, ancestorAttributes.pop()) { + attrs.insert(attr.type(), attr.value()); + } + a.setAttributes(attrs); + } + + rv << a; + } + } + + return rv; +} + +Protocol::FetchTagsResponse HandlerHelper::fetchTagsResponse(const Tag &tag, + bool withRID, + Connection *connection) +{ + Protocol::FetchTagsResponse response(tag.id()); + response.setType(tag.tagType().name().toUtf8()); + response.setParentId(tag.parentId()); + response.setGid(tag.gid().toUtf8()); + + if (withRID && connection) { + // Fail silently if retrieving tag RID is not allowed in current context + if (!connection->context()->resource().isValid()) { + return response; + } + + QueryBuilder qb(TagRemoteIdResourceRelation::tableName()); + qb.addColumn(TagRemoteIdResourceRelation::remoteIdColumn()); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), + Query::Equals, + connection->context()->resource().id()); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), + Query::Equals, + tag.id()); + if (!qb.exec()) { + throw HandlerException("Unable to query Tag Remote ID"); + } + QSqlQuery query = qb.query(); + // No RID for this tag + if (!query.next()) { + return response; + } + response.setRemoteId(Utils::variantToByteArray(query.value(0))); + } + + return response; +} + +Protocol::FetchRelationsResponse HandlerHelper::fetchRelationsResponse(const Relation &relation) +{ + return Protocol::FetchRelationsResponse(relation.leftId(), + relation.left().mimeType().name().toUtf8(), + relation.rightId(), + relation.right().mimeType().name().toUtf8(), + relation.relationType().name().toUtf8()); +} + + +Flag::List HandlerHelper::resolveFlags(const QSet &flagNames) +{ + Flag::List flagList; + flagList.reserve(flagNames.size()); + Q_FOREACH (const QByteArray &flagName, flagNames) { + Flag flag = Flag::retrieveByName(QString::fromUtf8(flagName)); + if (!flag.isValid()) { + flag = Flag(QString::fromUtf8(flagName)); + if (!flag.insert()) { + throw HandlerException("Unable to create flag"); + } + } + flagList.append(flag); + } + return flagList; +} + +Tag::List HandlerHelper::resolveTagsByUID(const ImapSet &tags) +{ + if (tags.isEmpty()) { + return Tag::List(); + } + SelectQueryBuilder qb; + QueryHelper::setToQuery(tags, Tag::idFullColumnName(), qb); + if (!qb.exec()) { + throw HandlerException("Unable to resolve tags"); + } + const Tag::List result = qb.result(); + if (result.isEmpty()) { + throw HandlerException("No tags found"); + } + return result; +} + +Tag::List HandlerHelper::resolveTagsByGID(const QStringList &tagsGIDs) +{ + Tag::List tagList; + if (tagsGIDs.isEmpty()) { + return tagList; + } + + for (const QString &tagGID : tagsGIDs) { + Tag::List tags = Tag::retrieveFiltered(Tag::gidColumn(), tagGID); + Tag tag; + if (tags.isEmpty()) { + tag.setGid(tagGID); + tag.setParentId(0); + + TagType type = TagType::retrieveByName(QStringLiteral("PLAIN")); + if (!type.isValid()) { + type.setName(QStringLiteral("PLAIN")); + if (!type.insert()) { + throw HandlerException("Unable to create tag type"); + } + } + tag.setTagType(type); + if (!tag.insert()) { + throw HandlerException("Unable to create tag"); + } + } else if (tags.count() == 1) { + tag = tags[0]; + } else { + // Should not happen + throw HandlerException("Tag GID is not unique"); + } + + tagList.append(tag); + } + + return tagList; +} + +Tag::List HandlerHelper::resolveTagsByRID(const QStringList &tagsRIDs, CommandContext *context) +{ + Tag::List tags; + if (tagsRIDs.isEmpty()) { + return tags; + } + + if (!context->resource().isValid()) { + throw HandlerException("Tags can be resolved by their RID only in resource context"); + } + + tags.reserve(tagsRIDs.size()); + for (const QString &tagRID : tagsRIDs) { + SelectQueryBuilder qb; + Query::Condition cond; + cond.addColumnCondition(Tag::idFullColumnName(), Query::Equals, TagRemoteIdResourceRelation::tagIdFullColumnName()); + cond.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, context->resource().id()); + qb.addJoin(QueryBuilder::LeftJoin, TagRemoteIdResourceRelation::tableName(), cond); + qb.addValueCondition(TagRemoteIdResourceRelation::remoteIdFullColumnName(), Query::Equals, tagRID); + if (!qb.exec()) { + throw HandlerException("Unable to resolve tags"); + } + + Tag tag; + Tag::List results = qb.result(); + if (results.isEmpty()) { + // If the tag does not exist, we create a new one with GID matching RID + Tag::List tags = resolveTagsByGID(QStringList() << tagRID); + if (tags.count() != 1) { + throw HandlerException("Unable to resolve tag"); + } + tag = tags[0]; + TagRemoteIdResourceRelation rel; + rel.setRemoteId(tagRID); + rel.setTagId(tag.id()); + rel.setResourceId(context->resource().id()); + if (!rel.insert()) { + throw HandlerException("Unable to create tag"); + } + } else if (results.count() == 1) { + tag = results[0]; + } else { + throw HandlerException("Tag RID is not unique within this resource context"); + } + + tags.append(tag); + } + + return tags; +} + +Collection HandlerHelper::collectionFromScope(const Scope &scope, Connection *connection) +{ + if (scope.scope() == Scope::Invalid || scope.scope() == Scope::Gid) { + throw HandlerException("Invalid collection scope"); + } + + SelectQueryBuilder qb; + CollectionQueryHelper::scopeToQuery(scope, connection, qb); + if (!qb.exec()) { + throw HandlerException("Failed to execute SQL query"); + } + + const Collection::List c = qb.result(); + if (c.count() == 0) { + return Collection(); + } else if (c.count() == 1) { + return c.at(0); + } else { + throw HandlerException("Query returned more than one reslut"); + } +} + +Tag::List HandlerHelper::tagsFromScope(const Scope &scope, Connection* connection) +{ + if (scope.scope() == Scope::Invalid || scope.scope() == Scope::HierarchicalRid) { + throw HandlerException("Invalid tag scope"); + } + + if (scope.scope() == Scope::Uid) { + return resolveTagsByUID(scope.uidSet()); + } else if (scope.scope() == Scope::Gid) { + return resolveTagsByGID(scope.gidSet()); + } else if (scope.scope() == Scope::Rid) { + return resolveTagsByRID(scope.ridSet(), connection->context()); + } + + Q_ASSERT(false); + return Tag::List(); +} diff --git a/src/server/handlerhelper.h b/src/server/handlerhelper.h new file mode 100644 index 0000000..0005481 --- /dev/null +++ b/src/server/handlerhelper.h @@ -0,0 +1,128 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADIHANDLERHELPER_H +#define AKONADIHANDLERHELPER_H + +#include "entities.h" + +#include +#include +#include +#include + +namespace Akonadi { + +class Scope; +class ImapSet; + +namespace Protocol { +class Ancestor; +class CachePolicy; +class FetchCollectionsResponse; +class FetchTagsResponse; +class FetchRelationsResponse; +} + +namespace Server { + +class CommandContext; +class Connection; + +/** + Helper functions for command handlers. +*/ +class HandlerHelper +{ +public: + /** + Returns the collection identified by the given id or path. + */ + static Collection collectionFromIdOrName(const QByteArray &id); + + /** + Returns the full path for the given collection. + */ + static QString pathForCollection(const Collection &col); + + /** + Returns the protocol representation of the cache policy of the given + Collection object. + */ + static Protocol::CachePolicy cachePolicyResponse(const Collection &col); + + /** + Returns the protocol representation of the given collection. + Make sure DataStore::activeCachePolicy() has been called before to include + the effective cache policy + */ + static Protocol::FetchCollectionsResponse fetchCollectionsResponse(const Collection &col); + + /** + Returns the protocol representation of the given collection. + Make sure DataStore::activeCachePolicy() has been called before to include + the effective cache policy + */ + static Protocol::FetchCollectionsResponse fetchCollectionsResponse(const Collection &col, + const CollectionAttribute::List &attributeList, + bool includeStatistics = false, + int ancestorDepth = 0, + const QStack &ancestors = QStack(), + const QStack &ancestorAttributes = QStack(), + bool isReferenced = false, + const QStringList &mimeTypes = QStringList()); + + /** + Returns the protocol representation of a collection ancestor chain. + */ + static QVector ancestorsResponse(int ancestorDepth, + const QStack &ancestors, + const QStack &_ancestorsAttributes = QStack()); + + static Protocol::FetchTagsResponse fetchTagsResponse(const Tag &tag, + bool withRID = false, + Connection *connection = Q_NULLPTR); + + static Protocol::FetchRelationsResponse fetchRelationsResponse(const Relation &relation); + + /** + Converts a bytearray list of flag names into flag records. + @throws HandlerException on errors during database operations + */ + static Flag::List resolveFlags(const QSet &flagNames); + + /** + Converts a imap set of tags into tag records. + @throws HandlerException on errors during database operations + */ + static Tag::List resolveTagsByUID(const ImapSet &tags); + + static Tag::List resolveTagsByGID(const QStringList &tagsGIDs); + + static Tag::List resolveTagsByRID(const QStringList &tagsRIDs, CommandContext *context); + + static Collection collectionFromScope(const Scope &scope, Connection *connection); + + static Tag::List tagsFromScope(const Scope &scope, Connection *connection); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/intervalcheck.cpp b/src/server/intervalcheck.cpp new file mode 100644 index 0000000..833cae1 --- /dev/null +++ b/src/server/intervalcheck.cpp @@ -0,0 +1,97 @@ +/* + Copyright (c) 2008 Volker Krause + Copyright (C) 2014 Daniel Vrátil + + 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 "intervalcheck.h" +#include "storage/datastore.h" +#include "storage/itemretrievalmanager.h" +#include "storage/entity.h" + +using namespace Akonadi::Server; + +static const int MINIMUM_AUTOSYNC_INTERVAL = 5; // minutes +static const int MINIMUM_COLTREESYNC_INTERVAL = 5; // minutes + +IntervalCheck::IntervalCheck(QObject *parent) + : CollectionScheduler(QThread::IdlePriority, parent) +{ + setObjectName(QStringLiteral("IntervalCheck")); +} + +IntervalCheck::~ IntervalCheck() +{ + quitThread(); +} + +void IntervalCheck::requestCollectionSync(const Collection &collection) +{ + QMetaObject::invokeMethod(this, "collectionExpired", + Qt::QueuedConnection, + Q_ARG(Collection, collection)); +} + +int IntervalCheck::collectionScheduleInterval(const Collection &collection) +{ + return collection.cachePolicyCheckInterval(); +} + +bool IntervalCheck::hasChanged(const Collection &collection, const Collection &changed) +{ + return collection.cachePolicyCheckInterval() != changed.cachePolicyCheckInterval() + || collection.enabled() != changed.enabled() + || collection.syncPref() != changed.syncPref(); +} + +bool IntervalCheck::shouldScheduleCollection(const Collection &collection) +{ + return collection.cachePolicyCheckInterval() > 0 + && ((collection.syncPref() == Tristate::True) || ((collection.syncPref() == Tristate::Undefined) && collection.enabled())); +} + +void IntervalCheck::collectionExpired(const Collection &collection) +{ + const QDateTime now(QDateTime::currentDateTime()); + + if (collection.parentId() == 0) { + const QString resourceName = collection.resource().name(); + + const int interval = qMax(MINIMUM_COLTREESYNC_INTERVAL, collection.cachePolicyCheckInterval()); + + const QDateTime lastExpectedCheck = now.addSecs(interval * -60); + if (!mLastCollectionTreeSyncs.contains(resourceName) || mLastCollectionTreeSyncs.value(resourceName) < lastExpectedCheck) { + mLastCollectionTreeSyncs.insert(resourceName, now); + QMetaObject::invokeMethod(ItemRetrievalManager::instance(), "triggerCollectionTreeSync", + Qt::QueuedConnection, + Q_ARG(QString, resourceName)); + } + } + + // now on to the actual collection syncing + const int interval = qMax(MINIMUM_AUTOSYNC_INTERVAL, collection.cachePolicyCheckInterval()); + + const QDateTime lastExpectedCheck = now.addSecs(interval * -60); + if (mLastChecks.contains(collection.id()) && mLastChecks.value(collection.id()) > lastExpectedCheck) { + return; + } + mLastChecks.insert(collection.id(), now); + QMetaObject::invokeMethod(ItemRetrievalManager::instance(), "triggerCollectionSync", + Qt::QueuedConnection, + Q_ARG(QString, collection.resource().name()), + Q_ARG(qint64, collection.id())); +} diff --git a/src/server/intervalcheck.h b/src/server/intervalcheck.h new file mode 100644 index 0000000..c8faf75 --- /dev/null +++ b/src/server/intervalcheck.h @@ -0,0 +1,68 @@ +/* + 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 INTERVALCHECK_H +#define INTERVALCHECK_H + +#include "collectionscheduler.h" + +#include +#include + +namespace Akonadi { +namespace Server { + +/** + Interval checking thread. +*/ +class IntervalCheck : public CollectionScheduler +{ + Q_OBJECT + +public: + IntervalCheck(QObject *parent = 0); + ~IntervalCheck(); + + /** + * Requests the given collection to be synced. + * Executed from any thread, forwards to triggerCollectionXSync() in the + * retrieval thread. + * A minimum time interval between two sync requests is ensured. + */ + void requestCollectionSync(const Collection &collection); + +protected: + int collectionScheduleInterval(const Collection &collection); + bool hasChanged(const Collection &collection, const Collection &changed); + bool shouldScheduleCollection(const Collection &collection); + +protected Q_SLOTS: + void collectionExpired(const Collection &collection); + +private: + QHash mLastChecks; + QHash mLastCollectionTreeSyncs; + + static IntervalCheck *s_instance; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/main.cpp b/src/server/main.cpp new file mode 100644 index 0000000..4c3a5bd --- /dev/null +++ b/src/server/main.cpp @@ -0,0 +1,85 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "akonadi.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#ifdef QT_STATICPLUGIN +#include + +Q_IMPORT_PLUGIN(qsqlite3) +#endif + +void shutdownHandler(int) +{ + akDebug() << "Shutting down AkonadiServer..."; + + Akonadi::Server::AkonadiServer::instance()->quit(); + + exit(255); +} + +int main(int argc, char **argv) +{ + Q_INIT_RESOURCE(akonadidb); + AkCoreApplication app(argc, argv); + app.setDescription(QStringLiteral("Akonadi Server\nDo not run manually, use 'akonadictl' instead to start/stop Akonadi.")); + +#if !defined(NDEBUG) + const QCommandLineOption startWithoutControlOption( + QStringLiteral("start-without-control"), + QStringLiteral("Allow to start the Akonadi server without the Akonadi control process being available")); + app.addCommandLineOptions(startWithoutControlOption); + +#endif + + app.parseCommandLine(); + + if (!app.commandLineArguments().isSet(QStringLiteral("start-without-control")) && + !QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock))) { + akError() << "Akonadi control process not found - aborting."; + akFatal() << "If you started akonadiserver manually, try 'akonadictl start' instead."; + } + + // Make sure we do initialization from eventloop, otherwise + // org.freedesktop.Akonadi.upgrading service won't be registered to DBus at all + QTimer::singleShot(0, Akonadi::Server::AkonadiServer::instance(), &Akonadi::Server::AkonadiServer::init); + AkonadiCrash::setShutdownMethod(shutdownHandler); + + const int result = app.exec(); + + Akonadi::Server::AkonadiServer::instance()->quit(); + + Q_CLEANUP_RESOURCE(akonadidb); + + return result; +} diff --git a/src/server/notificationmanager.cpp b/src/server/notificationmanager.cpp new file mode 100644 index 0000000..7af478c --- /dev/null +++ b/src/server/notificationmanager.cpp @@ -0,0 +1,238 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + Copyright (c) 2010 Michael Jansen + + 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 "notificationmanager.h" +#include "notificationmanageradaptor.h" +#include "notificationsource.h" +#include "tracer.h" +#include "storage/datastore.h" +#include "connection.h" + +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +NotificationManager *NotificationManager::mSelf = 0; + +Q_DECLARE_METATYPE(QVector) + +NotificationManager::NotificationManager() + : QObject(0) + , mDebug(false) +{ + qRegisterMetaType>(); + qDBusRegisterMetaType>(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType>(); + qDBusRegisterMetaType>(); + + new NotificationManagerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/notifications"), + this, QDBusConnection::ExportAdaptors); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/notifications/debug"), + this, QDBusConnection::ExportScriptableSlots | + QDBusConnection::ExportScriptableSignals); + + const QString serverConfigFile = StandardDirs::serverConfigFile(XdgBaseDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + + mTimer.setInterval(settings.value(QStringLiteral("NotificationManager/Interval"), 50).toInt()); + mTimer.setSingleShot(true); + connect(&mTimer, &QTimer::timeout, this, &NotificationManager::emitPendingNotifications); +} + +NotificationManager::~NotificationManager() +{ +} + +NotificationManager *NotificationManager::self() +{ + if (!mSelf) { + mSelf = new NotificationManager(); + } + + return mSelf; +} + +void NotificationManager::connectNotificationCollector(NotificationCollector *collector) +{ + connect(collector, &NotificationCollector::notify, + this, &NotificationManager::slotNotify); +} + +void NotificationManager::registerConnection(Connection *connection) +{ + QMutexLocker locker(&mSourcesLock); + auto source = std::find_if(mNotificationSources.cbegin(), mNotificationSources.cend(), + [connection](NotificationSource *source) { + return connection->sessionId() == source->dbusPath().path().toLatin1(); + }); + if (source == mNotificationSources.cend()) { + qWarning() << "Received request to register Notification bus connection, but there's no such subscriber"; + return; + } + + connect(const_cast(*source), &NotificationSource::notify, + connection, static_cast(&Connection::sendResponse), + Qt::QueuedConnection); +} + +void NotificationManager::unregisterConnection(Connection *connection) +{ + QMutexLocker locker(&mSourcesLock); + auto source = std::find_if(mNotificationSources.cbegin(), mNotificationSources.cend(), + [connection](NotificationSource *source) { + return connection->sessionId() == source->dbusPath().path().toLatin1(); + }); + if (source != mNotificationSources.cend()) { + (*source)->disconnect(connection); + } +} + + + +void NotificationManager::slotNotify(const Akonadi::Protocol::ChangeNotification::List &msgs) +{ + //akDebug() << Q_FUNC_INFO << "Appending" << msgs.count() << "notifications to current list of " << mNotifications.count() << "notifications"; + Q_FOREACH (const Protocol::ChangeNotification &msg, msgs) { + Protocol::ChangeNotification::appendAndCompress(mNotifications, msg); + } + //akDebug() << Q_FUNC_INFO << "We have" << mNotifications.count() << "notifications queued in total after appendAndCompress()"; + + if (!mTimer.isActive()) { + mTimer.start(); + } +} + +void NotificationManager::emitPendingNotifications() +{ + if (mNotifications.isEmpty()) { + return; + } + + if (mDebug) { + QVector bas; + bas.reserve(mNotifications.size()); + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + Q_FOREACH (const Protocol::ChangeNotification ¬ification, mNotifications) { + Tracer::self()->signal("NotificationManager::notify", notification.debugString()); + Protocol::serialize(&buffer, notification); + bas << buffer.data(); + buffer.buffer().clear(); + buffer.seek(0); + } + Q_EMIT debugNotify(bas); + } else { + Q_FOREACH (const Protocol::ChangeNotification ¬ification, mNotifications) { + Tracer::self()->signal("NotificationManager::notify", notification.debugString()); + } + } + + Q_FOREACH (NotificationSource *source, mNotificationSources) { + Protocol::ChangeNotification::List acceptedNotifications; + Q_FOREACH (const Protocol::ChangeNotification ¬ification, mNotifications) { + if (source->acceptsNotification(notification)) { + acceptedNotifications << notification; + } + } + + if (!acceptedNotifications.isEmpty()) { + source->emitNotification(acceptedNotifications); + } + } + + mNotifications.clear(); +} + +QDBusObjectPath NotificationManager::subscribe(const QString &identifier, bool exclusive) +{ + akDebug() << Q_FUNC_INFO << this << identifier << exclusive; + NotificationSource *source = mNotificationSources.value(identifier); + if (source) { + akDebug() << "Known subscriber" << identifier << "subscribes again"; + source->addClientServiceName(message().service()); + } else { + source = new NotificationSource(identifier, message().service(), this); + } + + registerSource(source); + source->setExclusive(exclusive); + + // FIXME KF5: Emit the QDBusObjectPath instead of the identifier + Q_EMIT subscribed(source->dbusPath()); + + return source->dbusPath(); +} + +void NotificationManager::registerSource(NotificationSource *source) +{ + // Protect write operations because of registerConnection() + QMutexLocker locker(&mSourcesLock); + mNotificationSources.insert(source->identifier(), source); +} + +void NotificationManager::unsubscribe(const QString &identifier) +{ + NotificationSource *source = mNotificationSources.value(identifier); + if (source) { + unregisterSource(source); + source->deleteLater(); + Q_EMIT unsubscribed(source->dbusPath()); + } else { + akDebug() << "Attempt to unsubscribe unknown subscriber" << identifier; + } +} + +void NotificationManager::unregisterSource(NotificationSource *source) +{ + // Protect write operations because of registerConnection() + QMutexLocker locker(&mSourcesLock); + mNotificationSources.remove(source->identifier()); +} + +QList NotificationManager::subscribers() const +{ + QList identifiers; + identifiers.reserve(mNotificationSources.count()); + Q_FOREACH (NotificationSource *source, mNotificationSources) { + identifiers << source->dbusPath(); + } + + return identifiers; +} + +void NotificationManager::enableDebug(bool enable) +{ + mDebug = enable; +} + +bool NotificationManager::debugEnabled() const +{ + return mDebug; +} diff --git a/src/server/notificationmanager.h b/src/server/notificationmanager.h new file mode 100644 index 0000000..079f52e --- /dev/null +++ b/src/server/notificationmanager.h @@ -0,0 +1,123 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_NOTIFICATIONMANAGER_H +#define AKONADI_NOTIFICATIONMANAGER_H + +#include +#include "storage/entity.h" + +#include +#include +#include +#include +#include +#include + +class NotificationManagerTest; + +namespace Akonadi { +namespace Server { + +class NotificationCollector; +class NotificationSource; +class Connection; + +/** + Notification manager D-Bus interface. +*/ +class NotificationManager : public QObject, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.NotificationManager") + +public: + static NotificationManager *self(); + + virtual ~NotificationManager(); + + void connectNotificationCollector(NotificationCollector *collector); + + void registerConnection(Connection *connection); + void unregisterConnection(Connection *connection); + +public Q_SLOTS: + Q_SCRIPTABLE void emitPendingNotifications(); + + /** + * Subscribe to notifications emitted by this manager. + * + * @param identifier Identifier to use for our subscription. + * @param exclusive Exclusive subscribers also receive notifications on referenced collections + * @return The path we got assigned. Contains identifier. + */ + QDBusObjectPath subscribe(const QString &identifier, bool exclusive); + + /** + * Unsubscribe from this manager. + * + * This method is for your inconvenience only. It's advisable to use the unsubscribe method + * provided by the NotificationSource. + * + * @param identifier The identifier used for subscription. + */ + void unsubscribe(const QString &identifier); + + /** + * Returns identifiers of currently subscribed sources + */ + Q_SCRIPTABLE QList subscribers() const; + + Q_SCRIPTABLE void enableDebug(bool enable); + Q_SCRIPTABLE bool debugEnabled() const; + +Q_SIGNALS: + Q_SCRIPTABLE void debugNotify(const QVector &msg); + + void subscribed(const QDBusObjectPath &path); + void unsubscribed(const QDBusObjectPath &path); + +private Q_SLOTS: + void slotNotify(const Akonadi::Protocol::ChangeNotification::List &msgs); + +private: + NotificationManager(); + +private: + void registerSource(NotificationSource *source); + void unregisterSource(NotificationSource *source); + + static NotificationManager *mSelf; + Protocol::ChangeNotification::List mNotifications; + QTimer mTimer; + + //! One message source for each subscribed process + QMutex mSourcesLock; + QHash mNotificationSources; + + bool mDebug; + + friend class NotificationSource; + friend class ::NotificationManagerTest; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/notificationsource.cpp b/src/server/notificationsource.cpp new file mode 100644 index 0000000..0d14878 --- /dev/null +++ b/src/server/notificationsource.cpp @@ -0,0 +1,485 @@ +/* + Copyright (c) 2010 Michael Jansen + + 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 "notificationsource.h" + +#include + +#include "notificationsourceadaptor.h" +#include "notificationmanager.h" +#include "collectionreferencemanager.h" +#include "connection.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +template +QVector setToVector(const QSet &set) +{ + QVector v; + v.reserve(set.size()); + Q_FOREACH (const T &val, set) { + v << val; + } + return v; +} + +NotificationSource::NotificationSource(const QString &identifier, const QString &clientServiceName, NotificationManager *parent) + : QObject(parent) + , mManager(parent) + , mIdentifier(identifier) + , mDBusIdentifier(identifier) + , mClientWatcher(0) + , mAllMonitored(false) + , mExclusive(false) +{ + new NotificationSourceAdaptor(this); + + // Clean up for dbus usage: any non-alphanumeric char should be turned into '_' + const int len = mDBusIdentifier.length(); + for (int i = 0; i < len; ++i) { + if (!mDBusIdentifier[i].isLetterOrNumber()) { + mDBusIdentifier[i] = QLatin1Char('_'); + } + } + + QDBusConnection::sessionBus().registerObject( + dbusPath().path(), + this, + QDBusConnection::ExportAdaptors); + + mClientWatcher = new QDBusServiceWatcher(clientServiceName, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForUnregistration, this); + connect(mClientWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &NotificationSource::serviceUnregistered); +} + +NotificationSource::~NotificationSource() +{ +} + +QDBusObjectPath NotificationSource::dbusPath() const +{ + return QDBusObjectPath(QLatin1String("/subscriber/") + mDBusIdentifier); +} + +void NotificationSource::emitNotification(const Protocol::ChangeNotification::List ¬ifications) +{ + Q_FOREACH (const auto ¬ification, notifications) { + Q_EMIT notify(notification); + } +} + +QString NotificationSource::identifier() const +{ + return mIdentifier; +} + +void NotificationSource::unsubscribe() +{ + mManager->unsubscribe(mIdentifier); +} + +bool NotificationSource::isExclusive() const +{ + return mExclusive; +} + +void NotificationSource::setExclusive(bool enabled) +{ + mExclusive = enabled; +} + +void NotificationSource::addClientServiceName(const QString &clientServiceName) +{ + if (mClientWatcher->watchedServices().contains(clientServiceName)) { + return; + } + + mClientWatcher->addWatchedService(clientServiceName); + akDebug() << Q_FUNC_INFO << "Notification source" << mIdentifier << "now serving:" << mClientWatcher->watchedServices(); +} + +void NotificationSource::serviceUnregistered(const QString &serviceName) +{ + mClientWatcher->removeWatchedService(serviceName); + akDebug() << Q_FUNC_INFO << "Notification source" << mIdentifier << "now serving:" << mClientWatcher->watchedServices(); + + if (mClientWatcher->watchedServices().isEmpty()) { + unsubscribe(); + } +} + +void NotificationSource::setMonitoredCollection(Entity::Id id, bool monitored) +{ + if (id < 0) { + return; + } + + if (monitored && !mMonitoredCollections.contains(id)) { + mMonitoredCollections.insert(id); + Q_EMIT monitoredCollectionsChanged(); + } else if (!monitored) { + mMonitoredCollections.remove(id); + Q_EMIT monitoredCollectionsChanged(); + } +} + +QVector NotificationSource::monitoredCollections() const +{ + return setToVector(mMonitoredCollections); +} + +void NotificationSource::setMonitoredItem(Entity::Id id, bool monitored) +{ + if (id < 0) { + return; + } + + if (monitored && !mMonitoredItems.contains(id)) { + mMonitoredItems.insert(id); + Q_EMIT monitoredItemsChanged(); + } else if (!monitored) { + mMonitoredItems.remove(id); + Q_EMIT monitoredItemsChanged(); + } +} + +QVector NotificationSource::monitoredItems() const +{ + return setToVector(mMonitoredItems); +} + +void NotificationSource::setMonitoredTag(Entity::Id id, bool monitored) +{ + if (id < 0) { + return; + } + + if (monitored && !mMonitoredTags.contains(id)) { + mMonitoredTags.insert(id); + Q_EMIT monitoredTagsChanged(); + } else if (!monitored) { + mMonitoredTags.remove(id); + Q_EMIT monitoredTagsChanged(); + } +} + +QVector NotificationSource::monitoredTags() const +{ + return setToVector(mMonitoredTags); +} + +void NotificationSource::setMonitoredResource(const QByteArray &resource, bool monitored) +{ + if (monitored && !mMonitoredResources.contains(resource)) { + mMonitoredResources.insert(resource); + Q_EMIT monitoredResourcesChanged(); + } else if (!monitored) { + mMonitoredResources.remove(resource); + Q_EMIT monitoredResourcesChanged(); + } +} + +QVector NotificationSource::monitoredResources() const +{ + return setToVector(mMonitoredResources); +} + +void NotificationSource::setMonitoredMimeType(const QString &mimeType, bool monitored) +{ + if (mimeType.isEmpty()) { + return; + } + + if (monitored && !mMonitoredMimeTypes.contains(mimeType)) { + mMonitoredMimeTypes.insert(mimeType); + Q_EMIT monitoredMimeTypesChanged(); + } else if (!monitored) { + mMonitoredMimeTypes.remove(mimeType); + Q_EMIT monitoredMimeTypesChanged(); + } +} + +QStringList NotificationSource::monitoredMimeTypes() const +{ + return mMonitoredMimeTypes.toList(); +} + +void NotificationSource::setAllMonitored(bool allMonitored) +{ + if (allMonitored && !mAllMonitored) { + mAllMonitored = true; + Q_EMIT isAllMonitoredChanged(); + } else if (!allMonitored) { + mAllMonitored = false; + Q_EMIT isAllMonitoredChanged(); + } +} + +bool NotificationSource::isAllMonitored() const +{ + return mAllMonitored; +} + +void NotificationSource::setSession(const QByteArray &sessionId) +{ + mSession = sessionId; +} + +void NotificationSource::setIgnoredSession(const QByteArray &sessionId, bool ignored) +{ + if (ignored && !mIgnoredSessions.contains(sessionId)) { + mIgnoredSessions.insert(sessionId); + Q_EMIT ignoredSessionsChanged(); + } else if (!ignored) { + mIgnoredSessions.remove(sessionId); + Q_EMIT ignoredSessionsChanged(); + } +} + +QVector NotificationSource::ignoredSessions() const +{ + return setToVector(mIgnoredSessions); +} + +bool NotificationSource::isCollectionMonitored(Entity::Id id) const +{ + if (id < 0) { + return false; + } else if (mMonitoredCollections.contains(id)) { + return true; + } else if (mMonitoredCollections.contains(0)) { + return true; + } + return false; +} + +bool NotificationSource::isMimeTypeMonitored(const QString &mimeType) const +{ + return mMonitoredMimeTypes.contains(mimeType); + + // FIXME: Handle mimetype aliases +} + +bool NotificationSource::isMoveDestinationResourceMonitored(const Protocol::ChangeNotification &msg) const +{ + if (msg.operation() != Protocol::ChangeNotification::Move) { + return false; + } + return mMonitoredResources.contains(msg.destinationResource()); +} + +void NotificationSource::setMonitoredType(Protocol::ChangeNotification::Type type, bool monitored) +{ + if (monitored && !mMonitoredTypes.contains(type)) { + mMonitoredTypes.insert(type); + Q_EMIT monitoredTypesChanged(); + } else if (!monitored) { + mMonitoredTypes.remove(type); + Q_EMIT monitoredTypesChanged(); + } +} + +QVector NotificationSource::monitoredTypes() const +{ + return setToVector(mMonitoredTypes); +} + +bool NotificationSource::acceptsNotification(const Protocol::ChangeNotification ¬ification) +{ + // session is ignored + if (mIgnoredSessions.contains(notification.sessionId())) { + return false; + } + + if (notification.entities().count() == 0 && notification.type() != Protocol::ChangeNotification::Relations) { + return false; + } + + //Only emit notifications for referenced collections if the subscriber is exclusive or monitors the collection + if (notification.type() == Protocol::ChangeNotification::Collections) { + // HACK: We need to dispatch notifications about disabled collections to SOME + // agents (that's what we have the exclusive subscription for) - but because + // querying each Collection from database would be expensive, we use the + // metadata hack to transfer this information from NotificationCollector + if (notification.metadata().contains("DISABLED") && (notification.operation() != Protocol::ChangeNotification::Unsubscribe) && !notification.itemParts().contains("ENABLED")) { + // Exclusive subscriber always gets it + if (mExclusive) { + return true; + } + + + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, notification.entities()) { + //Deliver the notification if referenced from this session + if (CollectionReferenceManager::instance()->isReferenced(entity.id, mSession)) { + return true; + } + //Exclusive subscribers still want the notification + if (mExclusive && CollectionReferenceManager::instance()->isReferenced(entity.id)) { + return true; + } + } + + //The session belonging to this monitor referenced or dereferenced the collection. We always want this notification. + //The referencemanager no longer holds a reference, so we have to check this way. + if (notification.itemParts().contains("REFERENCED") && mSession == notification.sessionId()) { + return true; + } + + // If the collection is not referenced, monitored or the subscriber is not + // exclusive (i.e. if we got here), then the subscriber does not care about + // this one, so drop it + return false; + } + } else if (notification.type() == Protocol::ChangeNotification::Items) { + //We always want notifications that affect the parent resource (like an item added to a referenced collection) + const bool notificationForParentResource = (mSession == notification.resource()); + if (CollectionReferenceManager::instance()->isReferenced(notification.parentCollection())) { + return (mExclusive || isCollectionMonitored(notification.parentCollection()) || isMoveDestinationResourceMonitored(notification) || notificationForParentResource); + } + } else if (notification.type() == Protocol::ChangeNotification::Tags) { + // Special handling for Tag removal notifications: When a Tag is removed, + // a notification is emitted for each Resource that owns the tag (i.e. + // each resource that owns a Tag RID - Tag RIDs are resource-specific). + // Additionally then we send one more notification without any RID that is + // destined for regular applications (which don't know anything about Tag RIDs) + if (notification.operation() == Protocol::ChangeNotification::Remove) { + // HACK: Since have no way to determine which resource this NotificationSource + // belongs to, we are abusing the fact that each resource ignores it's own + // main session, which is called the same name as the resource. + + // If there are any ignored sessions, but this notification does not have + // a specific resource set, then we ignore it, as this notification is + // for clients, not resources (does not have tag RID) + if (!mIgnoredSessions.isEmpty() && notification.resource().isEmpty()) { + return false; + } + + // If this source ignores a session (i.e. we assume it is a resource), + // but this notification is for another resource, then we ignore it + if (!notification.resource().isEmpty() && !mIgnoredSessions.contains(notification.resource())) { + return false; + } + + // Now we got here, which means that this notification either has empty + // resource, i.e. it is destined for a client applications, or it's + // destined for resource that we *think* (see the hack above) this + // NotificationSource belongs too. Which means we approve this notification, + // but it can still be discarded in the generic Tag notification filter + // below + } + } + + // user requested everything + if (mAllMonitored && notification.type() != Protocol::ChangeNotification::InvalidType) { + return true; + } + + switch (notification.type()) { + case Protocol::ChangeNotification::InvalidType: + akDebug() << "Received invalid change notification!"; + return false; + + case Protocol::ChangeNotification::Items: + if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ChangeNotification::Items)) { + return false; + } + // we have a resource or mimetype filter + if (!mMonitoredResources.isEmpty() || !mMonitoredMimeTypes.isEmpty()) { + if (mMonitoredResources.contains(notification.resource())) { + return true; + } + + if (isMoveDestinationResourceMonitored(notification)) { + return true; + } + + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, notification.entities()) { + if (isMimeTypeMonitored(entity.mimeType)) { + return true; + } + } + + return false; + } + + // we explicitly monitor that item or the collections it's in + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, notification.entities()) { + if (mMonitoredItems.contains(entity.id)) { + return true; + } + } + + return isCollectionMonitored(notification.parentCollection()) + || isCollectionMonitored(notification.parentDestCollection()); + + case Protocol::ChangeNotification::Collections: + if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ChangeNotification::Collections)) { + return false; + } + + // we have a resource filter + if (!mMonitoredResources.isEmpty()) { + const bool resourceMatches = mMonitoredResources.contains(notification.resource()) + || isMoveDestinationResourceMonitored(notification); + + // a bit hacky, but match the behaviour from the item case, + // if resource is the only thing we are filtering on, stop here, and if the resource filter matched, of course + if (mMonitoredMimeTypes.isEmpty() || resourceMatches) { + return resourceMatches; + } + // else continue + } + + // we explicitly monitor that colleciton, or all of them + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, notification.entities()) { + if (isCollectionMonitored(entity.id)) { + return true; + } + } + + return isCollectionMonitored(notification.parentCollection()) + || isCollectionMonitored(notification.parentDestCollection()); + + case Protocol::ChangeNotification::Tags: + if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ChangeNotification::Tags)) { + return false; + } + + if (mMonitoredTags.isEmpty()) { + return true; + } + + Q_FOREACH (const Protocol::ChangeNotification::Entity &entity, notification.entities()) { + if (mMonitoredTags.contains(entity.id)) { + return true; + } + } + + return false; + + case Protocol::ChangeNotification::Relations: + if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ChangeNotification::Relations)) { + return false; + } + return true; + + } + + return false; +} diff --git a/src/server/notificationsource.h b/src/server/notificationsource.h new file mode 100644 index 0000000..72acc98 --- /dev/null +++ b/src/server/notificationsource.h @@ -0,0 +1,156 @@ +/* + Copyright (c) 2010 Michael Jansen + + 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_NOTIFICATIONSOURCE_H +#define AKONADI_NOTIFICATIONSOURCE_H + +#include +#include +#include + +#include "entities.h" + +#include + +namespace Akonadi { +namespace Server { + +class Connection; +class NotificationManager; + +class NotificationSource : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.NotificationSource") + +public: + + /** + * Construct a NotificationSource. + * + * @param identifier The identifier of this notification source, defined by the client + * @param clientServiceName The D-Bus service name of the client, used to clean up if the client does not unsubscribe correctly. + * @param parent The parent object. + */ + NotificationSource(const QString &identifier, const QString &clientServiceName, NotificationManager *parent); + + /** + * Destroy the NotificationSource. + */ + virtual ~NotificationSource(); + + /** + * Emit the given notifications + * + * @param notifications List of notifications to emit. + */ + void emitNotification(const Protocol::ChangeNotification::List ¬ifications); + + /** + * Return the dbus path this message source uses. + */ + QDBusObjectPath dbusPath() const; + + /** + * Return the identifier for this message source + */ + QString identifier() const; + + /** + * Add another client service to watch for. Auto-unsubscription only happens if + * all watched client services have been stopped. + */ + void addClientServiceName(const QString &clientServiceName); + + bool acceptsNotification(const Protocol::ChangeNotification ¬ification); + +public Q_SLOTS: + /** + * Unsubscribe from the message source. + * + * This will delete the message source and make the used dbus path unavailable. + */ + Q_SCRIPTABLE void unsubscribe(); + + Q_SCRIPTABLE void setMonitoredCollection(Entity::Id id, bool monitored); + Q_SCRIPTABLE QVector monitoredCollections() const; + Q_SCRIPTABLE void setMonitoredItem(Entity::Id id, bool monitored); + Q_SCRIPTABLE QVector monitoredItems() const; + Q_SCRIPTABLE void setMonitoredTag(Entity::Id id, bool monitored); + Q_SCRIPTABLE QVector monitoredTags() const; + Q_SCRIPTABLE void setMonitoredResource(const QByteArray &resource, bool monitored); + Q_SCRIPTABLE QVector monitoredResources() const; + Q_SCRIPTABLE void setMonitoredMimeType(const QString &mimeType, bool monitored); + Q_SCRIPTABLE QStringList monitoredMimeTypes() const; + Q_SCRIPTABLE void setAllMonitored(bool allMonitored); + Q_SCRIPTABLE bool isAllMonitored() const; + Q_SCRIPTABLE void setSession( const QByteArray &sessionId ); + Q_SCRIPTABLE void setIgnoredSession(const QByteArray &sessionId, bool ignored); + Q_SCRIPTABLE QVector ignoredSessions() const; + Q_SCRIPTABLE void setMonitoredType(Protocol::ChangeNotification::Type type, bool monitored); + Q_SCRIPTABLE QVector monitoredTypes() const; + Q_SCRIPTABLE void setExclusive( bool exclusive ); + Q_SCRIPTABLE bool isExclusive() const; + +Q_SIGNALS: + // Internal, not exported to DBus + void notify(const Akonadi::Protocol::Command &response); + + Q_SCRIPTABLE void monitoredCollectionsChanged(); + Q_SCRIPTABLE void monitoredItemsChanged(); + Q_SCRIPTABLE void monitoredTagsChanged(); + Q_SCRIPTABLE void monitoredResourcesChanged(); + Q_SCRIPTABLE void monitoredMimeTypesChanged(); + Q_SCRIPTABLE void isAllMonitoredChanged(); + Q_SCRIPTABLE void ignoredSessionsChanged(); + Q_SCRIPTABLE void monitoredTypesChanged(); + +private Q_SLOTS: + void serviceUnregistered(const QString &serviceName); + +private: + bool isCollectionMonitored(Entity::Id id) const; + bool isMimeTypeMonitored(const QString &mimeType) const; + bool isMoveDestinationResourceMonitored(const Protocol::ChangeNotification &msg) const; + +private: + NotificationManager *mManager; + QString mIdentifier; + QString mDBusIdentifier; + QDBusServiceWatcher *mClientWatcher; + + QPointer mConnection; + + bool mAllMonitored; + bool mExclusive; + QSet mMonitoredCollections; + QSet mMonitoredItems; + QSet mMonitoredTags; + // TODO: Make this a bitflag + QSet mMonitoredTypes; + QSet mMonitoredMimeTypes; + QSet mMonitoredResources; + QSet mIgnoredSessions; + QByteArray mSession; + +}; // class NotificationSource + +} // namespace Server +} // namespace Akonadi + +#endif // #define AKONADI_NOTIFICATIONSOURCE_H diff --git a/src/server/nulltracer.h b/src/server/nulltracer.h new file mode 100644 index 0000000..cdeadd7 --- /dev/null +++ b/src/server/nulltracer.h @@ -0,0 +1,84 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AKONADI_NULLTRACER_H +#define AKONADI_NULLTRACER_H + +#include "tracerinterface.h" + +namespace Akonadi { +namespace Server { + +/** + * A tracer which forwards all tracing information to /dev/null ;) + */ +class NullTracer : public TracerInterface +{ +public: + virtual ~NullTracer() + { + } + + virtual void beginConnection(const QString &identifier, const QString &msg) + { + Q_UNUSED(identifier); + Q_UNUSED(msg); + } + + virtual void endConnection(const QString &identifier, const QString &msg) + { + Q_UNUSED(identifier); + Q_UNUSED(msg); + } + + virtual void connectionInput(const QString &identifier, const QByteArray &msg) + { + Q_UNUSED(identifier); + Q_UNUSED(msg); + } + + virtual void connectionOutput(const QString &identifier, const QByteArray &msg) + { + Q_UNUSED(identifier); + Q_UNUSED(msg); + } + + virtual void signal(const QString &signalName, const QString &msg) + { + Q_UNUSED(signalName); + Q_UNUSED(msg); + } + + virtual void warning(const QString &componentName, const QString &msg) + { + Q_UNUSED(componentName); + Q_UNUSED(msg); + } + + virtual void error(const QString &componentName, const QString &msg) + { + Q_UNUSED(componentName); + Q_UNUSED(msg); + } +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/preprocessorinstance.cpp b/src/server/preprocessorinstance.cpp new file mode 100644 index 0000000..78e5c74 --- /dev/null +++ b/src/server/preprocessorinstance.cpp @@ -0,0 +1,248 @@ +/****************************************************************************** + * + * File : preprocessorinstance.cpp + * Creation date : Sat 18 Jul 2009 02:50:39 + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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 "preprocessorinstance.h" +#include "preprocessorinterface.h" +#include "preprocessormanager.h" + +#include "entities.h" + +#include "agentcontrolinterface.h" +#include "agentmanagerinterface.h" + +#include "tracer.h" + +#include + +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +PreprocessorInstance::PreprocessorInstance(const QString &id) + : QObject() + , mBusy(false) + , mId(id) + , mInterface(0) +{ + Q_ASSERT(!id.isEmpty()); +} + +PreprocessorInstance::~PreprocessorInstance() +{ +} + +bool PreprocessorInstance::init() +{ + Q_ASSERT(!mBusy); // must be called very early + Q_ASSERT(!mInterface); + + mInterface = new OrgFreedesktopAkonadiPreprocessorInterface( + DBus::agentServiceName(mId, DBus::Preprocessor), + QStringLiteral("/Preprocessor"), + QDBusConnection::sessionBus(), + this); + + if (!mInterface || !mInterface->isValid()) { + Tracer::self()->warning( + QStringLiteral("PreprocessorInstance"), + QStringLiteral("Could not connect to pre-processor instance '%1': %2") + .arg(mId, + mInterface ? mInterface->lastError().message() : QString())); + delete mInterface; + mInterface = 0; + return false; + } + + QObject::connect(mInterface, &OrgFreedesktopAkonadiPreprocessorInterface::itemProcessed, this, &PreprocessorInstance::itemProcessed); + + return true; +} + +void PreprocessorInstance::enqueueItem(qint64 itemId) +{ + akDebug() << "PreprocessorInstance::enqueueItem(" << itemId << ")"; + + mItemQueue.push_back(itemId); + + // If the preprocessor is already busy processing another item then do nothing. + if (mBusy) { + // The "head" item is the one being processed and we have just added another one. + Q_ASSERT(mItemQueue.size() > 1); + return; + } + + // Not busy: handle the item. + processHeadItem(); +} + +void PreprocessorInstance::processHeadItem() +{ + // We shouldn't be called if there are no items in the queue + Q_ASSERT(!mItemQueue.empty()); + // We shouldn't be here with no interface + Q_ASSERT(mInterface); + + qint64 itemId = mItemQueue.front(); + + // Fetch the actual item data (as it may have changed since it was enqueued) + // The fetch will hit the cache if the item wasn't changed. + + PimItem actualItem = PimItem::retrieveById(itemId); + + while (!actualItem.isValid()) { + // hum... item is gone ? + // FIXME: Signal to the manager that the item is no longer valid! + PreprocessorManager::instance()->preProcessorFinishedHandlingItem(this, itemId); + + mItemQueue.pop_front(); + if (mItemQueue.empty()) { + // nothing more to process for this instance: jump out + mBusy = false; + return; + } + + // try the next one in the queue + itemId = mItemQueue.front(); + actualItem = PimItem::retrieveById(itemId); + } + + // Ok.. got a valid item to process: collection and mimetype is known. + + akDebug() << "PreprocessorInstance::processHeadItem(): about to begin processing item " << itemId; + + mBusy = true; + + mItemProcessingStartDateTime = QDateTime::currentDateTime(); + + // The beginProcessItem() D-Bus call is asynchronous (marked with NoReply attribute) + mInterface->beginProcessItem(itemId, actualItem.collectionId(), actualItem.mimeType().name()); + + akDebug() << "PreprocessorInstance::processHeadItem(): processing started for item " << itemId; +} + +int PreprocessorInstance::currentProcessingTime() +{ + if (!mBusy) { + return -1; // nothing being processed + } + + return mItemProcessingStartDateTime.secsTo(QDateTime::currentDateTime()); +} + +bool PreprocessorInstance::abortProcessing() +{ + Q_ASSERT_X(mBusy, "PreprocessorInstance::abortProcessing()", "You shouldn't call this method when isBusy() returns false"); + + OrgFreedesktopAkonadiAgentControlInterface iface( + DBus::agentServiceName(mId, DBus::Agent), + QStringLiteral("/"), + QDBusConnection::sessionBus(), + this); + + if (!iface.isValid()) { + Tracer::self()->warning( + QStringLiteral("PreprocessorInstance"), + QStringLiteral("Could not connect to pre-processor instance '%1': %2") + .arg(mId, iface.lastError().message())); + return false; + } + + // We don't check the return value.. as this is a "warning" + // The preprocessor manager will check again in a while and eventually + // terminate the agent at all... + iface.abort(); + + return true; +} + +bool PreprocessorInstance::invokeRestart() +{ + Q_ASSERT_X(mBusy, "PreprocessorInstance::invokeRestart()", "You shouldn't call this method when isBusy() returns false"); + + OrgFreedesktopAkonadiAgentManagerInterface iface( + DBus::serviceName(DBus::Control), + QStringLiteral("/AgentManager"), + QDBusConnection::sessionBus(), + this); + + if (!iface.isValid()) { + Tracer::self()->warning( + QStringLiteral("PreprocessorInstance"), + QStringLiteral("Could not connect to the AgentManager in order to restart pre-processor instance '%1': %2") + .arg(mId, iface.lastError().message())); + return false; + } + + iface.restartAgentInstance(mId); + + return true; +} + +void PreprocessorInstance::itemProcessed(qlonglong id) +{ + akDebug() << "PreprocessorInstance::itemProcessed(" << id << ")"; + + // We shouldn't be called if there are no items in the queue + if (mItemQueue.empty()) { + Tracer::self()->warning( + QStringLiteral("PreprocessorInstance"), + QStringLiteral("Pre-processor instance '%1' emitted itemProcessed(%2) but we actually have no item in the queue") + .arg(mId) + .arg(id)); + mBusy = false; + return; // preprocessor is buggy (FIXME: What now ?) + } + + // We should be busy now: this is more likely our fault, not the preprocessor's one. + Q_ASSERT(mBusy); + + qlonglong itemId = mItemQueue.front(); + + if (itemId != id) { + Tracer::self()->warning( + QStringLiteral("PreprocessorInstance"), + QStringLiteral("Pre-processor instance '%1' emitted itemProcessed(%2) but the head item in the queue has id %3") + .arg(mId) + .arg(id) + .arg(itemId)); + + // FIXME: And what now ? + } + + mItemQueue.pop_front(); + + PreprocessorManager::instance()->preProcessorFinishedHandlingItem(this, itemId); + + if (mItemQueue.empty()) { + // Nothing more to do + mBusy = false; + return; + } + + // Stay busy and process next item in the queue + processHeadItem(); +} diff --git a/src/server/preprocessorinstance.h b/src/server/preprocessorinstance.h new file mode 100644 index 0000000..669a335 --- /dev/null +++ b/src/server/preprocessorinstance.h @@ -0,0 +1,199 @@ +/****************************************************************************** + * + * File : preprocessorinstance.h + * Creation date : Sat 18 Jul 2009 02:50:39 + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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_PREPROCESSORINSTANCE_H +#define AKONADI_PREPROCESSORINSTANCE_H + +#include +#include + +#include + +class OrgFreedesktopAkonadiPreprocessorInterface; + +namespace Akonadi { +namespace Server { + +class AgentInstance; + +/** + * A single preprocessor (agent) instance. + * + * Most of the interface of this class is protected and is exposed only + * to PreprocessorManager (singleton). + * + * This class is NOT thread safe. The caller is responsible of protecting + * agains concurrent access. + */ +class PreprocessorInstance : public QObject +{ + friend class PreprocessorManager; + + Q_OBJECT + +protected: + + /** + * Create an instance of a PreprocessorInstance descriptor. + */ + PreprocessorInstance(const QString &id); + +public: // This is public only for qDeleteAll() called from PreprocessorManager + // ...for some reason couldn't convince gcc to have it as friend... + + /** + * Destroy this instance of the PreprocessorInstance descriptor. + */ + ~PreprocessorInstance(); + +private: + + /** + * The internal queue if item identifiers. + * The head item in the queue is the one currently being processed. + * The other ones are waiting. + */ + std::deque< qint64 > mItemQueue; + + /** + * Is this processor busy ? + * This, in fact, *should* be equivalent to "mItemQueue.count() > 0" + * as the head item in the queue is the one being processed now. + */ + bool mBusy; + + /** + * The date-time at that we have started processing the current + * item in the queue. This is used to compute the processing time + * and eventually spot a "dead" preprocessor (which takes longer + * than N minutes to process an item). + */ + QDateTime mItemProcessingStartDateTime; + + /** + * The id of this preprocessor instance. This is actually + * the AgentInstance identifier. + */ + QString mId; + + /** + * The preprocessor D-Bus interface. Owned. + */ + OrgFreedesktopAkonadiPreprocessorInterface *mInterface; + +protected: + + /** + * This is called by PreprocessorManager just after the construction + * in order to connect to the preprocessor instance via D-Bus. + * In case of failure this object should be destroyed as it can't + * operate properly. The error message is printed via Tracer. + */ + bool init(); + + /** + * Returns true if this preprocessor instance is currently processing an item. + * That is: if we have called "processItem()" on it and it hasn't emitted + * itemProcessed() yet. + */ + bool isBusy() const + { + return mBusy; + } + + /** + * Returns the time in seconds elapsed since the current item was submitted + * to the slave preprocessor instance. If no item is currently being + * processed then this function returns -1; + */ + int currentProcessingTime(); + + /** + * Returns the id of this preprocessor. This is actually + * the AgentInstance identifier but it's not a requirement. + */ + const QString &id() const + { + return mId; + } + + /** + * Returns a pointer to the internal preprocessor instance + * item queue. Don't mess with it unless you *really* know + * what you're doing. Use enqueueItem() to add an item + * to the queue. This method is provided to the PreprocessorManager + * to take over the item queue of a dying preprocessor. + * + * The returned pointer is granted to be non null. + */ + std::deque< qint64 > *itemQueue() + { + return &mItemQueue; + } + + /** + * This is called by PreprocessorManager to enqueue a PimItem + * for processing by this preprocessor instance. + */ + void enqueueItem(qint64 itemId); + + /** + * Attempts to abort the processing of the current item. + * May be called only if isBusy() returns true and an assertion + * will remind you of that. + * Returns true if the abort request was successfully sent + * (but not necessarily handled by the slave) and false + * if the request couldn't be sent for some reason. + */ + bool abortProcessing(); + + /** + * Attempts to invoke the preprocessor slave restart via + * AgentManager. This is the "last resort" action before + * starting to ignore the preprocessor (after it misbehaved). + */ + bool invokeRestart(); + +private: + + /** + * This function starts processing of the first item in mItemQueue. + * It's only used internally. + */ + void processHeadItem(); + +private Q_SLOTS: + + /** + * This is invoked to signal that the processing of the current (head) + * item has terminated and the next item should be processed. + */ + void itemProcessed(qlonglong id); + +}; // class PreprocessorInstance + +} // namespace Server +} // namespace Akonadi + +#endif //!_PREPROCESSORINSTANCE_H_ diff --git a/src/server/preprocessormanager.cpp b/src/server/preprocessormanager.cpp new file mode 100644 index 0000000..1924ba5 --- /dev/null +++ b/src/server/preprocessormanager.cpp @@ -0,0 +1,490 @@ +/****************************************************************************** + * + * File : preprocessormanager.cpp + * Creation date : Sat 18 Jul 2009 01:58:50 + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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 "preprocessormanager.h" +#include "akonadiserver_debug.h" +#include + +#include "entities.h" // Akonadi::Server::PimItem +#include "storage/datastore.h" +#include "tracer.h" +#include "collectionreferencemanager.h" + +#include "preprocessormanageradaptor.h" + +#include + +namespace Akonadi { +namespace Server { + +const int gHeartbeatTimeoutInMSecs = 30000; // 30 sec heartbeat + +// 2 minutes should be really enough to process an item. +// After this timeout elapses we assume that the preprocessor +// is "stuck" and we attempt to kick it by requesting an abort(). +const int gWarningItemProcessingTimeInSecs = 120; +// After 3 minutes, if the preprocessor is still "stuck" then +// we attempt to restart it via AgentManager.... +const int gMaximumItemProcessingTimeInSecs = 180; +// After 4 minutes, if the preprocessor is still "stuck" then +// we assume it's dead and just drop it's interface. +const int gDeadlineItemProcessingTimeInSecs = 240; + +} // namespace Server +} // namespace Akonadi + +using namespace Akonadi::Server; + +// The one and only PreprocessorManager object +PreprocessorManager *PreprocessorManager::mSelf = NULL; + +PreprocessorManager::PreprocessorManager() + : QObject() + , mEnabled(true) + , mMutex(new QMutex()) +{ + mSelf = this; // just to have it set early + // Hook in our D-Bus interface "shell". + new PreprocessorManagerAdaptor(this); + + QDBusConnection::sessionBus().registerObject( + QStringLiteral("/PreprocessorManager"), + this, + QDBusConnection::ExportAdaptors); + + mHeartbeatTimer = new QTimer(this); + + QObject::connect(mHeartbeatTimer, &QTimer::timeout, this, &PreprocessorManager::heartbeat); + + mHeartbeatTimer->start(gHeartbeatTimeoutInMSecs); +} + +PreprocessorManager::~PreprocessorManager() +{ + mHeartbeatTimer->stop(); + + // FIXME: Explicitly interrupt pre-processing here ? + // Pre-Processors should auto-protect themselves from re-processing an item: + // they are "closer to the DB" from this point of view. + + qDeleteAll(mPreprocessorChain); + qDeleteAll(mTransactionWaitQueueHash); // this should also disconnect all the signals from the data store objects... + + delete mMutex; +} + +bool PreprocessorManager::init() +{ + if (mSelf) { + return false; + } + mSelf = new PreprocessorManager(); + return true; +} + +void PreprocessorManager::done() +{ + if (!mSelf) { + return; + } + delete mSelf; + mSelf = NULL; +} + +bool PreprocessorManager::isActive() +{ + QMutexLocker locker(mMutex); + + if (!mEnabled) { + return false; + } + return mPreprocessorChain.count() > 0; +} + +PreprocessorInstance *PreprocessorManager::lockedFindInstance(const QString &id) +{ + Q_FOREACH (PreprocessorInstance *instance, mPreprocessorChain) { + if (instance->id() == id) { + return instance; + } + } + + return NULL; +} + +void PreprocessorManager::registerInstance(const QString &id) +{ + QMutexLocker locker(mMutex); + + akDebug() << "PreprocessorManager::registerInstance(" << id << ")"; + + PreprocessorInstance *instance = lockedFindInstance(id); + if (instance) { + return; // already registered + } + + // The PreprocessorInstance objects are actually always added at the end of the queue + // TODO: Maybe we need some kind of ordering here ? + // In that case we'll need to fiddle with the items that are currently enqueued for processing... + + instance = new PreprocessorInstance(id); + if (!instance->init()) { + Tracer::self()->warning( + QStringLiteral("PreprocessorManager"), + QStringLiteral("Could not initialize preprocessor instance '%1'") + .arg(id)); + delete instance; + return; + } + + akDebug() << "Registering preprocessor instance " << id; + + mPreprocessorChain.append(instance); +} + +void PreprocessorManager::unregisterInstance(const QString &id) +{ + QMutexLocker locker(mMutex); + + akDebug() << "PreprocessorManager::unregisterInstance(" << id << ")"; + + lockedUnregisterInstance(id); +} + +void PreprocessorManager::lockedUnregisterInstance(const QString &id) +{ + PreprocessorInstance *instance = lockedFindInstance(id); + if (!instance) { + return; // not our instance: don't complain (as we might be called for non-preprocessor agents too) + } + + // All of the preprocessor's waiting items must be queued to the next preprocessor (if there is one) + + std::deque< qint64 > *itemList = instance->itemQueue(); + Q_ASSERT(itemList); + + int idx = mPreprocessorChain.indexOf(instance); + Q_ASSERT(idx >= 0); // must be there! + + if (idx < (mPreprocessorChain.count() - 1)) { + // This wasn't the last preprocessor: trigger the next one. + PreprocessorInstance *nextPreprocessor = mPreprocessorChain[idx + 1]; + Q_ASSERT(nextPreprocessor); + Q_ASSERT(nextPreprocessor != instance); + + for (qint64 itemId : *itemList) { + nextPreprocessor->enqueueItem(itemId); + } + } else { + // This was the last preprocessor: end handling the items + for (qint64 itemId : *itemList) { + lockedEndHandleItem(itemId); + } + } + + mPreprocessorChain.removeOne(instance); + delete instance; +} + +void PreprocessorManager::beginHandleItem(const PimItem &item, const DataStore *dataStore) +{ + Q_ASSERT(dataStore); + Q_ASSERT(item.isValid()); + + // This is the entry point of the pre-processing chain. + QMutexLocker locker(mMutex); + + if (!mEnabled) { + // Preprocessing is disabled: immediately end handling the item. + // In fact we shouldn't even be here as the caller should + // have checked isActive() before calling this function. + // However, since setEnabled() may be called concurrently + // then this might not be the caller's fault. Just drop a warning. + + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::beginHandleItem(" << item.id() << ") called with a disabled preprocessor"; + + lockedEndHandleItem(item.id()); + return; + } + +#if 0 + // Now the hidden flag is stored as a part.. too hard to assert its existence :D + Q_ASSERT_X(item.hidden(), "PreprocessorManager::beginHandleItem()", "The item you pass to this function should be hidden!"); +#endif + + if (mPreprocessorChain.isEmpty() || CollectionReferenceManager::instance()->isReferenced(item.collectionId())) { + // No preprocessors at all or referenced collection: immediately end handling the item. + lockedEndHandleItem(item.id()); + return; + } + + if (dataStore->inTransaction()) { + akDebug() << "PreprocessorManager::beginHandleItem(" << item.id() << "): the DataStore is in transaction, pushing item to a wait queue"; + + // The calling thread data store is in a transaction: push the item into a wait queue + std::deque< qint64 > *waitQueue = mTransactionWaitQueueHash.value(dataStore, 0); + + if (!waitQueue) { + // No wait queue for this transaction yet... + waitQueue = new std::deque< qint64 >(); + + mTransactionWaitQueueHash.insert(dataStore, waitQueue); + + // This will usually end up being a queued connection. + QObject::connect(dataStore, &QObject::destroyed, this, &PreprocessorManager::dataStoreDestroyed); + QObject::connect(dataStore, &DataStore::transactionCommitted, this, &PreprocessorManager::dataStoreTransactionCommitted); + QObject::connect(dataStore, &DataStore::transactionRolledBack, this, &PreprocessorManager::dataStoreTransactionRolledBack); + } + + waitQueue->push_back(item.id()); + + // nothing more to do here + return; + } + + // The calling thread data store is NOT in a transaction: we can proceed directly. + lockedActivateFirstPreprocessor(item.id()); +} + +void PreprocessorManager::lockedActivateFirstPreprocessor(qint64 itemId) +{ + // Activate the first preprocessor. + PreprocessorInstance *preProcessor = mPreprocessorChain.first(); + Q_ASSERT(preProcessor); + + preProcessor->enqueueItem(itemId); + // The preprocessor will call our "preProcessorFinishedHandlingItem() method" + // when done with the item. + // + // The call should be asynchronous, that is it should never happen that + // preProcessorFinishedHandlingItem() is called from "inside" enqueueItem()... + // FIXME: Am I *really* sure of this ? If I'm wrong for some obscure reason then we have a deadlock. +} + +void PreprocessorManager::lockedKillWaitQueue(const DataStore *dataStore, bool disconnectSlots) +{ + std::deque< qint64 > *waitQueue = mTransactionWaitQueueHash.value(dataStore, 0); + if (!waitQueue) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::lockedKillWaitQueue(): called for dataStore which has no wait queue"; + return; + } + + mTransactionWaitQueueHash.remove(dataStore); + + delete waitQueue; + + if (!disconnectSlots) { + return; + } + + QObject::disconnect(dataStore, &QObject::destroyed, this, &PreprocessorManager::dataStoreDestroyed); + QObject::disconnect(dataStore, &DataStore::transactionCommitted, this, &PreprocessorManager::dataStoreTransactionCommitted); + QObject::disconnect(dataStore, &DataStore::transactionRolledBack, this, &PreprocessorManager::dataStoreTransactionRolledBack); + +} + +void PreprocessorManager::dataStoreDestroyed() +{ + QMutexLocker locker(mMutex); + + akDebug() << "PreprocessorManager::dataStoreDestroyed(): killing the wait queue"; + + const DataStore *dataStore = dynamic_cast< const DataStore *>(sender()); + if (!dataStore) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreDestroyed(): got the signal from a non DataStore object"; + return; + } + + lockedKillWaitQueue(dataStore, false); // no need to disconnect slots, qt will do that +} + +void PreprocessorManager::dataStoreTransactionCommitted() +{ + QMutexLocker locker(mMutex); + + akDebug() << "PreprocessorManager::dataStoreTransactionCommitted(): pushing items in wait queue to the preprocessing chain"; + + const DataStore *dataStore = dynamic_cast< const DataStore *>(sender()); + if (!dataStore) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreTransactionCommitted(): got the signal from a non DataStore object"; + return; + } + + std::deque< qint64 > *waitQueue = mTransactionWaitQueueHash.value(dataStore, 0); + if (!waitQueue) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreTransactionCommitted(): called for dataStore which has no wait queue"; + return; + } + + if (!mEnabled || mPreprocessorChain.isEmpty()) { + // Preprocessing has been disabled in the meantime or all the preprocessors died + for (qint64 id : *waitQueue) { + lockedEndHandleItem(id); + } + } else { + for (qint64 id : *waitQueue) { + lockedActivateFirstPreprocessor(id); + } + } + + lockedKillWaitQueue(dataStore, true); // disconnect slots this time +} + +void PreprocessorManager::dataStoreTransactionRolledBack() +{ + QMutexLocker locker(mMutex); + + akDebug() << "PreprocessorManager::dataStoreTransactionRolledBack(): killing the wait queue"; + + const DataStore *dataStore = dynamic_cast< const DataStore *>(sender()); + if (!dataStore) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreTransactionCommitted(): got the signal from a non DataStore object"; + return; + } + + lockedKillWaitQueue(dataStore, true); // disconnect slots this time +} + +void PreprocessorManager::preProcessorFinishedHandlingItem(PreprocessorInstance *preProcessor, qint64 itemId) +{ + QMutexLocker locker(mMutex); + + int idx = mPreprocessorChain.indexOf(preProcessor); + Q_ASSERT(idx >= 0); // must be there! + + if (idx < (mPreprocessorChain.count() - 1)) { + // This wasn't the last preprocessor: trigger the next one. + PreprocessorInstance *nextPreprocessor = mPreprocessorChain[idx + 1]; + Q_ASSERT(nextPreprocessor); + Q_ASSERT(nextPreprocessor != preProcessor); + + nextPreprocessor->enqueueItem(itemId); + } else { + // This was the last preprocessor: end handling the item. + lockedEndHandleItem(itemId); + } +} + +void PreprocessorManager::lockedEndHandleItem(qint64 itemId) +{ + // The exit point of the pre-processing chain. + + // Refetch the PimItem, the Collection and the MimeType now: preprocessing might have changed them. + PimItem item = PimItem::retrieveById(itemId); + if (!item.isValid()) { + // HUM... the preprocessor killed the item ? + // ... or retrieveById() failed ? + // Well.. if the preprocessor killed the item then this might be actually OK (spam?). + akDebug() << "Invalid PIM item id '" << itemId << "' passed to preprocessing chain termination function"; + return; + } + +#if 0 + if (!item.hidden()) { + // HUM... the item was already unhidden for some reason: we have nothing more to do here. + akDebug() << "The PIM item with id '" << itemId << "' reached the preprocessing chain termination function in unhidden state"; + return; + } +#endif + + if (!DataStore::self()->unhidePimItem(item)) { + Tracer::self()->warning( + QStringLiteral("PreprocessorManager"), + QStringLiteral("Failed to unhide the PIM item '%1': data is not lost but a server restart is required in order to unhide it") + .arg(itemId)); + } +} + +void PreprocessorManager::heartbeat() +{ + QMutexLocker locker(mMutex); + + // Loop through the processor instances and check their current processing time. + + QList< PreprocessorInstance *> firedPreprocessors; + + PreprocessorInstance *instance; + + Q_FOREACH (instance, mPreprocessorChain) { + // In this loop we check for "stuck" preprocessors. + + int elapsedTime = instance->currentProcessingTime(); + + if (elapsedTime < gWarningItemProcessingTimeInSecs) { + continue; // ok, still in time. + } + + // Ooops... the preprocessor looks to be "stuck". + // This is a rather critical condition and the question is "what we can do about it ?". + // The fact is that it doesn't really make sense to push another item for + // processing as the slave process is either dead (silently ?) or stuck anyway. + + // We then proceed as following: + // - we first kindly ask the preprocessor to abort the job (via Agent.Control interface) + // - if it doesn't obey after some time we attempt to restart it (via AgentManager) + // - if it doesn't obey, we drop the interface and assume it's dead until + // it's effectively restarted. + + if (elapsedTime < gMaximumItemProcessingTimeInSecs) { + // Kindly ask the preprocessor to abort the job. + + Tracer::self()->warning( + QStringLiteral("PreprocessorManager"), + QStringLiteral("Preprocessor '%1' seems to be stuck... trying to abort its job.") + .arg(instance->id())); + + if (instance->abortProcessing()) { + continue; + } + // If we're here then abortProcessing() failed. + } + + if (elapsedTime < gDeadlineItemProcessingTimeInSecs) { + // Attempt to restart the preprocessor via AgentManager interface + + Tracer::self()->warning( + QStringLiteral("PreprocessorManager"), + QStringLiteral("Preprocessor '%1' is stuck... trying to restart it") + .arg(instance->id())); + + if (instance->invokeRestart()) { + continue; + } + // If we're here then invokeRestart() failed. + } + + Tracer::self()->warning( + QStringLiteral("PreprocessorManager"), + QStringLiteral("Preprocessor '%1' is broken... ignoring it from now on") + .arg(instance->id())); + + // You're fired! Go Away! + firedPreprocessors.append(instance); + } + + // Kill the fired preprocessors, if any. + Q_FOREACH (instance, firedPreprocessors) { + lockedUnregisterInstance(instance->id()); + } +} diff --git a/src/server/preprocessormanager.h b/src/server/preprocessormanager.h new file mode 100644 index 0000000..b22cde9 --- /dev/null +++ b/src/server/preprocessormanager.h @@ -0,0 +1,313 @@ +/****************************************************************************** + * + * File : preprocessormanager.h + * Creation date : Sat 18 Jul 2009 01:58:50 + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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_PREPROCESSORMANAGER_H +#define AKONADI_PREPROCESSORMANAGER_H + +#include +#include +#include + +#include + +class QTimer; +class QMutex; + +#include "preprocessorinstance.h" + +namespace Akonadi { +namespace Server { + +class PimItem; +class DataStore; + +/** + * \class PreprocessorManager + * \brief The manager for preprocessor agents + * + * This class takes care of synchronizing the preprocessor agents. + * + * The preprocessors see the incoming PimItem objects before the user + * can see them (as long as the UI applications honor the hidden attribute). + * The items are marked as hidden (by the Append and AkAppend + * handlers) and then enqueued to the preprocessor chain via this class. + * Once all the preprocessors have done their work the item is unhidden again. + * + * Preprocessing isn't designed for critical tasks. There may + * be circumstances under that the Akonadi server fails to push an item + * to all the preprocessors. Most notably after a server restart all + * the items for that preprocessing was interrupted are just unhidden + * without any attempt to resume the preprocessor jobs. + * + * The enqueue requests may or may not arrive from "inside" a database + * transaction. The uncommitted transaction would "hide" the newly created items + * from the preprocessor instances (which are separate processes). + * This class, then, takes care of holding the newly arrived items + * in a wait queue until their transaction is committed (or rolled back). + */ +class PreprocessorManager : public QObject +{ + friend class PreprocessorInstance; + + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.PreprocessorManager") + +protected: + + /** + * Creates an instance of PreprocessorManager + */ + PreprocessorManager(); + + /** + * Destroys the instance of PreprocessorManager + * and frees all the relevant resources + */ + ~PreprocessorManager(); + +protected: + + /** + * The one and only instance pointer for the class PreprocessorManager + */ + static PreprocessorManager *mSelf; + + /** + * The hashtable of transaction wait queues. There is one wait + * queue for each DataStore that is currently in a transaction. + */ + QHash< const DataStore *, std::deque< qint64 > *> mTransactionWaitQueueHash; + + /** + * The preprocessor chain. + * The pointers inside the list are owned. + * + * In all the algorithms we assume that this list is actually very short + * (say 3-4 elements) and reverse lookup (pointer->index) is really fast. + */ + QList< PreprocessorInstance *> mPreprocessorChain; + + /** + * Is preprocessing enabled at all in this Akonadi server instance? + * This is true by default and can be set via setEnabled(). + * Mainly used to disable preprocessing via configuration file. + */ + bool mEnabled; + + /** + * The mutex used to protect the internals of this class (mainly + * the mPreprocessorChain member). + */ + QMutex *mMutex; + + /** + * The heartbeat timer. Used mainly to expire preprocessor jobs. + */ + QTimer *mHeartbeatTimer; + +public: + + /** + * Returns the one and only instance pointer for the class PreprocessorManager + * The returned pointer is valid only after a successful call to init(). + * + * \sa init() + * \sa done() + */ + static PreprocessorManager *instance() + { + return mSelf; + } + + /** + * Initializes this class singleton by creating its one and only instance. + * This is actually called in the AkonadiServer constructor. + * + * The instance is later available via the static instance() method. + * You must call done() when you've finished using this class services. + * Returns true upon successful initialisation and false when the initialization fails. + * + * \sa done() + */ + static bool init(); + + /** + * Deinitializes this class singleton (if it was initialized at all). + * This is actually called in the AkonadiServer::quit() method. + * + * \sa init() + */ + static void done(); + + /** + * Returns true if preprocessing is active in this Akonadi server. + * This means that we have at least one active preprocessor and + * preprocessing hasn't been explicitly disabled via configuration + * (so if isActive() returns true then also isEnabled() will return true). + * + * This function is thread-safe. + */ + bool isActive(); + + /** + * Returns true if this preprocessor hasn't been explicitly disabled + * via setEnabled( false ). This is used to disable preprocessing + * via configuration even if we have a valid chain of preprocessors. + * + * Please note that this flag doesn't tell if we actually have + * some registered preprocessors and thus we can do some meaningful job. + * You should use isActive() for this purpose. + */ + bool isEnabled() const + { + return mEnabled; + } + + /** + * Explicitly enables or disables the preprocessing in this Akonadi server. + * The PreprocessorManager starts in enabled state but can be disabled + * at a later stage: this is mainly used to disable preprocessing via + * configuration. + * + * Please note that setting this to true doesn't interrupt the currently + * running preprocessing jobs. Anything that was enqueued will be processed + * anyway. However, in Akonadi this is only invoked very early, + * when no preprocessors are alive yet. + */ + void setEnabled(bool enabled) + { + mEnabled = enabled; + } + + /** + * Trigger the preprocessor chain for the specified item. + * The item should have been added to the Akonadi database via + * the specified DataStore object. If the DataStore is in a + * transaction then this class will put the item in a wait + * queue until the transaction is committed. If the transaction + * is rolled back the whole wait queue will be discarded. + * If the DataStore is not in a transaction then the item + * will be pushed directly to the preprocessing chain. + * + * You should make sure that the preprocessor chain isActive() + * before calling this method. The items you pass to this method, + * also, should have the hidden attribute set. + * + * This function is thread-safe. + */ + void beginHandleItem(const PimItem &item, const DataStore *dataStore); + + /** + * This is called via D-Bus from AgentManager to register a preprocessor instance. + * + * This function is thread-safe. + */ + void registerInstance(const QString &id); + + /** + * This is called via D-Bus from AgentManager to unregister a preprocessor instance. + * + * This function is thread-safe. + */ + void unregisterInstance(const QString &id); + +protected: + + /** + * This is called by PreprocessorInstance to signal that a certain preprocessor has finished + * handling an item. + * + * This function is thread-safe. + */ + void preProcessorFinishedHandlingItem(PreprocessorInstance *preProcessor, qint64 itemId); + +private: + + /** + * Finds the preprocessor instance by its identifier. + * + * This must be called with mMutex locked. + */ + PreprocessorInstance *lockedFindInstance(const QString &id); + + /** + * Pushes the specified item to the first preprocessor. + * The caller *MUST* make sure that there is at least one preprocessor in the chain. + */ + void lockedActivateFirstPreprocessor(qint64 itemId); + + /** + * This is called internally to terminate the pre-processing + * chain for the specified Item. All the preprocessors have + * been triggered for it. + * + * This must be called with mMutex locked. + */ + void lockedEndHandleItem(qint64 itemId); + + /** + * This is the unprotected core of the unregisterInstance() function above. + */ + void lockedUnregisterInstance(const QString &id); + + /** + * Kill the wait queue for the specific DataStore object. + */ + void lockedKillWaitQueue(const DataStore *dataStore, bool disconnectSlots); + +private Q_SLOTS: + + /** + * Connected to the mHeartbeatTimer. Triggered every minute or something like that :D + * Mainly used to expire preprocessor jobs. + */ + void heartbeat(); + + /** + * This is used to handle database transactions and wait queues. + * The call to this slot usually comes from a queued signal/slot connection + * (i.e. from the *Append handler thread). + */ + void dataStoreDestroyed(); + + /** + * This is used to handle database transactions and wait queues. + * The call to this slot usually comes from a queued signal/slot connection + * (i.e. from the *Append handler thread). + */ + void dataStoreTransactionCommitted(); + + /** + * This is used to handle database transactions and wait queues. + * The call to this slot usually comes from a queued signal/slot connection + * (i.e. from the *Append handler thread). + */ + void dataStoreTransactionRolledBack(); + +}; // class PreprocessorManager + +} // namespace Server +} // namespace Akonadi + +#endif //!_PREPROCESSORMANAGER_H_ diff --git a/src/server/resourcemanager.cpp b/src/server/resourcemanager.cpp new file mode 100644 index 0000000..9c2c30e --- /dev/null +++ b/src/server/resourcemanager.cpp @@ -0,0 +1,93 @@ +/* + 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 "resourcemanager.h" +#include "tracer.h" +#include "storage/datastore.h" +#include "storage/transaction.h" +#include "resourcemanageradaptor.h" + +#include + +#include + +using namespace Akonadi::Server; + +ResourceManager *ResourceManager::mSelf = 0; + +ResourceManager::ResourceManager(QObject *parent) + : QObject(parent) +{ + new ResourceManagerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/ResourceManager"), this); +} + +void ResourceManager::addResourceInstance(const QString &name, const QStringList &capabilities) +{ + Transaction transaction(DataStore::self()); + Resource resource = Resource::retrieveByName(name); + if (resource.isValid()) { + Tracer::self()->error("ResourceManager", QStringLiteral("Resource '%1' already exists.").arg(name)); + return; // resource already exists + } + + // create the resource + resource.setName(name); + resource.setIsVirtual(capabilities.contains(QStringLiteral(AKONADI_AGENT_CAPABILITY_VIRTUAL))); + if (!resource.insert()) { + Tracer::self()->error("ResourceManager", QStringLiteral("Could not create resource '%1'.").arg(name)); + } + transaction.commit(); +} + +void ResourceManager::removeResourceInstance(const QString &name) +{ + DataStore *db = DataStore::self(); + + // remove items and collections + Resource resource = Resource::retrieveByName(name); + if (resource.isValid()) { + const QVector collections = resource.collections(); + Q_FOREACH (/*sic!*/ Collection collection, collections) { + db->cleanupCollection(collection); + } + + // remove resource + resource.remove(); + } +} + +QStringList ResourceManager::resourceInstances() const +{ + QStringList result; + const auto resources = Resource::retrieveAll(); + result.reserve(resources.size()); + Q_FOREACH (const Resource &res, resources) { + result.append(res.name()); + } + return result; +} + +ResourceManager *ResourceManager::self() +{ + if (!mSelf) { + mSelf = new ResourceManager(); + } + return mSelf; +} diff --git a/src/server/resourcemanager.h b/src/server/resourcemanager.h new file mode 100644 index 0000000..c5d75f0 --- /dev/null +++ b/src/server/resourcemanager.h @@ -0,0 +1,54 @@ +/* + 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. +*/ + +#ifndef AKONADI_RESOURCEMANAGER_H +#define AKONADI_RESOURCEMANAGER_H + +#include + +namespace Akonadi { +namespace Server { + +/** + Listens to agent instance added/removed signals and creates/removes + the corresponding data in the database. +*/ +class ResourceManager : public QObject +{ + Q_OBJECT + +public: + static ResourceManager *self(); + +private: + ResourceManager(QObject *parent = 0); + +public Q_SLOTS: + void addResourceInstance(const QString &name, const QStringList &capabilities); + void removeResourceInstance(const QString &name); + QStringList resourceInstances() const; + +private: + static ResourceManager *mSelf; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/search/abstractsearchengine.h b/src/server/search/abstractsearchengine.h new file mode 100644 index 0000000..eb72a51 --- /dev/null +++ b/src/server/search/abstractsearchengine.h @@ -0,0 +1,60 @@ +/* + Copyright (c) 2008 Tobias Koenig + Copyright (c) 2010 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_ABSTRACTSEARCHENGINE_H +#define AKONADI_ABSTRACTSEARCHENGINE_H + +#include + +namespace Akonadi { +namespace Server { + +class Collection; + +/** + * Abstract interface for search engines. + * Executed in the main thread. Must not block. + */ +class AbstractSearchEngine +{ +public: + virtual ~AbstractSearchEngine() + { + } + + /** + * Adds the given @p collection to the search. + * + * @returns true if the collection was added successfully, false otherwise. + */ + virtual void addSearch(const Collection &collection) = 0; + + /** + * Removes the collection with the given @p id from the search. + * + * @returns true if the collection was removed successfully, false otherwise. + */ + virtual void removeSearch(qint64 id) = 0; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/search/abstractsearchplugin.h b/src/server/search/abstractsearchplugin.h new file mode 100644 index 0000000..eb480e7 --- /dev/null +++ b/src/server/search/abstractsearchplugin.h @@ -0,0 +1,68 @@ +/* + Copyright (c) 2013 Daniel Vrátil + + 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_ABSTRACTSEARCHPLUGIN +#define AKONADI_ABSTRACTSEARCHPLUGIN + +#include +#include +#include + +namespace Akonadi { + +/** + * @class AbstractSearchPlugin + * + * 3rd party applications can install a search plugin for Akonadi server to + * provide access to their search capability. + * + * When the server performs a search, it will send the query to all available + * search plugins and merge the results. + * + * @since 1.12 + */ +class AbstractSearchPlugin +{ + +public: + /** + * Destructor. + */ + virtual ~AbstractSearchPlugin() + { + }; + + /** + * Reimplement this method to provide the actual search capability. + * + * The implementation can block. + * + * @param query Search query to execute. + * @return List of Akonadi Item IDs referring to items that are matching + * the query. + */ + virtual QSet search(const QString &query, const QList &collections, const QStringList &mimeTypes) = 0; + +}; + +} + +Q_DECLARE_INTERFACE(Akonadi::AbstractSearchPlugin, "org.freedesktop.Akonadi.AbstractSearchPlugin") + +#endif diff --git a/src/server/search/agentsearchengine.cpp b/src/server/search/agentsearchengine.cpp new file mode 100644 index 0000000..c07e2a1 --- /dev/null +++ b/src/server/search/agentsearchengine.cpp @@ -0,0 +1,63 @@ +/* + Copyright (c) 2010 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 "agentsearchengine.h" +#include "entities.h" + +#include + +#include + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +void AgentSearchEngine::addSearch(const Collection &collection) +{ + QDBusInterface agentMgr(DBus::serviceName(DBus::Control), + QStringLiteral(AKONADI_DBUS_AGENTMANAGER_PATH), + QStringLiteral("org.freedesktop.Akonadi.AgentManagerInternal")); + if (agentMgr.isValid()) { + QList args; + args << collection.queryString() + << QLatin1String("") + << collection.id(); + agentMgr.callWithArgumentList(QDBus::NoBlock, QStringLiteral("addSearch"), args); + return; + } + + akError() << "Failed to connect to agent manager: " << agentMgr.lastError().message(); +} + +void AgentSearchEngine::removeSearch(qint64 id) +{ + QDBusInterface agentMgr(DBus::serviceName(DBus::Control), + QStringLiteral(AKONADI_DBUS_AGENTMANAGER_PATH), + QStringLiteral("org.freedesktop.Akonadi.AgentManagerInternal")); + if (agentMgr.isValid()) { + QList args; + args << id; + agentMgr.callWithArgumentList(QDBus::NoBlock, QStringLiteral("removeSearch"), args); + return; + } + + akError() << "Failed to connect to agent manager: " << agentMgr.lastError().message(); +} diff --git a/src/server/search/agentsearchengine.h b/src/server/search/agentsearchengine.h new file mode 100644 index 0000000..d1023e1 --- /dev/null +++ b/src/server/search/agentsearchengine.h @@ -0,0 +1,39 @@ +/* + Copyright (c) 2010 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 AGENTSEARCHENGINE_H +#define AGENTSEARCHENGINE_H + +#include "abstractsearchengine.h" + +namespace Akonadi { +namespace Server { + +/** Search engine for distributing searches to agents. */ +class AgentSearchEngine : public AbstractSearchEngine +{ +public: + virtual void addSearch(const Collection &collection); + virtual void removeSearch(qint64 id); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/search/agentsearchinstance.cpp b/src/server/search/agentsearchinstance.cpp new file mode 100644 index 0000000..3e6cf07 --- /dev/null +++ b/src/server/search/agentsearchinstance.cpp @@ -0,0 +1,86 @@ +/* + Copyright (c) 2013 Daniel Vrátil + + 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 "agentsearchinstance.h" +#include "agentsearchinterface.h" +#include "searchtaskmanager.h" +#include "dbusconnectionpool.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +AgentSearchInstance::AgentSearchInstance(const QString &id) + : mId(id) + , mInterface(0) + , mServiceWatcher(0) +{ +} + +AgentSearchInstance::~AgentSearchInstance() +{ + delete mInterface; +} + +bool AgentSearchInstance::init() +{ + Q_ASSERT(!mInterface); + + mInterface = new OrgFreedesktopAkonadiAgentSearchInterface( + DBus::agentServiceName(mId, DBus::Agent), + QStringLiteral("/Search"), + DBusConnectionPool::threadConnection()); + + if (!mInterface || !mInterface->isValid()) { + delete mInterface; + mInterface = 0; + return false; + } + + mServiceWatcher = new QDBusServiceWatcher(DBus::agentServiceName(mId, DBus::Agent), + DBusConnectionPool::threadConnection(), + QDBusServiceWatcher::WatchForOwnerChange, + this); + connect(mServiceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, + this, &AgentSearchInstance::serviceOwnerChanged); + + return true; +} + +void AgentSearchInstance::serviceOwnerChanged(const QString &service, const QString &oldName, const QString &newName) +{ + Q_UNUSED(service); + Q_UNUSED(oldName); + + if (newName.isEmpty()) { + SearchTaskManager::instance()->unregisterInstance(mId); + } +} + +void AgentSearchInstance::search(const QByteArray &searchId, const QString &query, + qlonglong collectionId) +{ + mInterface->search(searchId, query, collectionId); +} + +OrgFreedesktopAkonadiAgentSearchInterface *AgentSearchInstance::interface() const +{ + return mInterface; +} diff --git a/src/server/search/agentsearchinstance.h b/src/server/search/agentsearchinstance.h new file mode 100644 index 0000000..95b2be7 --- /dev/null +++ b/src/server/search/agentsearchinstance.h @@ -0,0 +1,57 @@ +/* + Copyright (c) 2013 Daniel Vrátil + + 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_AGENTSEARCHINSTANCE_H +#define AKONADI_AGENTSEARCHINSTANCE_H + +#include +#include + +class QDBusServiceWatcher; +class OrgFreedesktopAkonadiAgentSearchInterface; + +namespace Akonadi { +namespace Server { + +class AgentSearchInstance : public QObject +{ + Q_OBJECT +public: + AgentSearchInstance(const QString &id); + virtual ~AgentSearchInstance(); + + bool init(); + + void search(const QByteArray &searchId, const QString &query, qlonglong collectionId); + + OrgFreedesktopAkonadiAgentSearchInterface *interface() const; + +private Q_SLOTS: + void serviceOwnerChanged(const QString &service, const QString &oldName, const QString &newName); + +private: + QString mId; + OrgFreedesktopAkonadiAgentSearchInterface *mInterface; + QDBusServiceWatcher *mServiceWatcher; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_AGENTSEARCHINSTANCE_H diff --git a/src/server/search/searchmanager.cpp b/src/server/search/searchmanager.cpp new file mode 100644 index 0000000..65495d1 --- /dev/null +++ b/src/server/search/searchmanager.cpp @@ -0,0 +1,456 @@ +/* + Copyright (c) 2010 Volker Krause + Copyright (c) 2013 Daniel Vrátil + + 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 "searchmanager.h" +#include "abstractsearchplugin.h" +#include "akonadiserver_debug.h" + +#include "agentsearchengine.h" +#include "notificationmanager.h" +#include "dbusconnectionpool.h" +#include "searchrequest.h" +#include "searchtaskmanager.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" +#include "storage/transaction.h" +#include "storage/selectquerybuilder.h" +#include "handler/searchhelper.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(Akonadi::Server::NotificationCollector *) + +using namespace Akonadi; +using namespace Akonadi::Server; + +SearchManager *SearchManager::sInstance = 0; + +Q_DECLARE_METATYPE(Collection) +Q_DECLARE_METATYPE(QSet) +Q_DECLARE_METATYPE(QSemaphore *) + +SearchManager::SearchManager(const QStringList &searchEngines, QObject *parent) + : AkThread(AkThread::ManualStart, QThread::InheritPriority, parent) + , mEngineNames(searchEngines) +{ + setObjectName(QStringLiteral("SearchManager")); + + qRegisterMetaType>(); + qRegisterMetaType(); + qRegisterMetaType(); + + Q_ASSERT(sInstance == 0); + sInstance = this; + + // We load search plugins (as in QLibrary::load()) in the main thread so that + // static initialization happens in the QApplication thread + loadSearchPlugins(); + + // Register to DBus on the main thread connection - otherwise we don't appear + // on the service. + QDBusConnection conn = QDBusConnection::sessionBus(); + conn.registerObject(QStringLiteral("/SearchManager"), this, + QDBusConnection::ExportAllSlots); + + // Delay-call init() + startThread(); +} + +void SearchManager::init() +{ + AkThread::init(); + + mEngines.reserve(mEngineNames.size()); + Q_FOREACH (const QString &engineName, mEngineNames) { + if (engineName == QLatin1String("Agent")) { + mEngines.append(new AgentSearchEngine); + } else { + akError() << "Unknown search engine type: " << engineName; + } + } + + initSearchPlugins(); + + // The timer will tick 15 seconds after last change notification. If a new notification + // is delivered in the meantime, the timer is reset + mSearchUpdateTimer = new QTimer(this); + mSearchUpdateTimer->setInterval(15 * 1000); + mSearchUpdateTimer->setSingleShot(true); + connect(mSearchUpdateTimer, &QTimer::timeout, + this, &SearchManager::searchUpdateTimeout); +} + +void SearchManager::quit() +{ + QDBusConnection conn = DBusConnectionPool::threadConnection(); + conn.unregisterObject(QStringLiteral("/SearchManager"), QDBusConnection::UnregisterTree); + conn.disconnectFromBus(conn.name()); + + // Make sure all childrens are deleted within context of this thread + qDeleteAll(children()); + + qDeleteAll(mEngines); + qDeleteAll(mPlugins); + Q_FOREACH (QPluginLoader *loader, mPluginLoaders) { + loader->unload(); + delete loader; + } + + AkThread::quit(); +} + + +SearchManager::~SearchManager() +{ + quitThread(); + + sInstance = 0; +} + +SearchManager *SearchManager::instance() +{ + Q_ASSERT(sInstance); + return sInstance; +} + +void SearchManager::registerInstance(const QString &id) +{ + SearchTaskManager::instance()->registerInstance(id); +} + +void SearchManager::unregisterInstance(const QString &id) +{ + SearchTaskManager::instance()->unregisterInstance(id); +} + +QVector SearchManager::searchPlugins() const +{ + return mPlugins; +} + +void SearchManager::loadSearchPlugins() +{ + QStringList loadedPlugins; + const QString pluginOverride = QString::fromLatin1(qgetenv("AKONADI_OVERRIDE_SEARCHPLUGIN")); + if (!pluginOverride.isEmpty()) { + akDebug() << "Overriding the search plugins with: " << pluginOverride; + } + + const QStringList dirs = XdgBaseDirs::findPluginDirs(); + Q_FOREACH (const QString &pluginDir, dirs) { + QDir dir(pluginDir + QLatin1String("/akonadi")); + const QStringList fileNames = dir.entryList(QDir::Files); + akDebug() << "SEARCH MANAGER: searching in " << pluginDir + QLatin1String("/akonadi") << ":" << fileNames; + Q_FOREACH (const QString &fileName, fileNames) { + const QString filePath = pluginDir % QLatin1String("/akonadi/") % fileName; + std::unique_ptr loader(new QPluginLoader(filePath)); + const QVariantMap metadata = loader->metaData().value(QStringLiteral("MetaData")).toVariant().toMap(); + if (metadata.value(QStringLiteral("X-Akonadi-PluginType")).toString() != QLatin1String("SearchPlugin")) { + akDebug() << "===>" << fileName << metadata.value(QStringLiteral("X-Akonadi-PluginType")).toString(); + continue; + } + + const QString libraryName = metadata.value(QStringLiteral("X-Akonadi-Library")).toString(); + if (loadedPlugins.contains(libraryName)) { + akDebug() << "Already loaded one version of this plugin, skipping: " << libraryName; + continue; + } + + // When search plugin override is active, ignore all plugins except for the override + if (!pluginOverride.isEmpty()) { + if (libraryName != pluginOverride) { + akDebug() << libraryName << "skipped because of AKONADI_OVERRIDE_SEARCHPLUGIN"; + continue; + } + + // When there's no override, only load plugins enabled by default + } else if (metadata.value(QStringLiteral("X-Akonadi-LoadByDefault"), true).toBool() == false) { + continue; + } + + if (!loader->load()) { + akError() << "Failed to load search plugin" << libraryName << ":" << loader->errorString(); + continue; + } + + mPluginLoaders << loader.release(); + loadedPlugins << libraryName; + } + } +} + +void SearchManager::initSearchPlugins() +{ + for (QPluginLoader *loader : mPluginLoaders) { + if (!loader->load()) { + akError() << "Failed to load search plugin" << loader->fileName() << ":" << loader->errorString(); + continue; + } + + AbstractSearchPlugin *plugin = qobject_cast(loader->instance()); + if (!plugin) { + akError() << loader->fileName() << "is not a valid Akonadi search plugin"; + continue; + } + + akDebug() << "SearchManager: loaded search plugin" << loader->fileName(); + mPlugins << plugin; + } +} + +void SearchManager::scheduleSearchUpdate() +{ + // Reset if the timer is active (use QueuedConnection to invoke start() from + // the thread the QTimer lives in instead of caller's thread, otherwise crashes + // and weird things can happen. + QMetaObject::invokeMethod(mSearchUpdateTimer, "start", Qt::QueuedConnection); +} + +void SearchManager::searchUpdateTimeout() +{ + // Get all search collections, that is subcollections of "Search", which always has ID 1 + const Collection::List collections = Collection::retrieveFiltered(Collection::parentIdFullColumnName(), 1); + Q_FOREACH (const Collection &collection, collections) { + updateSearchAsync(collection); + } +} + +void SearchManager::updateSearchAsync(const Collection &collection) +{ + QMetaObject::invokeMethod(this, "updateSearchImpl", + Qt::QueuedConnection, + Q_ARG(Collection, collection), + Q_ARG(QSemaphore *, 0)); +} + +void SearchManager::updateSearch(const Collection &collection) +{ + QMutex mutex; + + mLock.lock(); + if (mUpdatingCollections.contains(collection.id())) { + mLock.unlock(); + return; + } + mUpdatingCollections.insert(collection.id()); + mLock.unlock(); + + QSemaphore sem(1); + sem.acquire(); + + QMetaObject::invokeMethod(this, "updateSearchImpl", + Qt::QueuedConnection, + Q_ARG(Collection, collection), + Q_ARG(QSemaphore*, &sem)); + + // Now wait for updateSearchImpl to wake us. + if (!sem.tryAcquire()) { + sem.acquire(); + } + sem.release(); + + mLock.lock(); + mUpdatingCollections.remove(collection.id()); + mLock.unlock(); +} + +#define wakeUpCaller(cond) \ + if (cond) { \ + cond->release(); \ + } + +void SearchManager::updateSearchImpl(const Collection &collection, QSemaphore *cond) +{ + if (collection.queryString().size() >= 32768) { + qCWarning(AKONADISERVER_LOG) << "The query is at least 32768 chars long, which is the maximum size supported by the akonadi db schema. The query is therefore most likely truncated and will not be executed."; + wakeUpCaller(cond); + return; + } + if (collection.queryString().isEmpty()) { + wakeUpCaller(cond); + return; + } + + const QStringList queryAttributes = collection.queryAttributes().split(QLatin1Char(' ')); + const bool remoteSearch = queryAttributes.contains(QStringLiteral(AKONADI_PARAM_REMOTE)); + bool recursive = queryAttributes.contains(QStringLiteral(AKONADI_PARAM_RECURSIVE)); + + QStringList queryMimeTypes; + const QVector mimeTypes = collection.mimeTypes(); + queryMimeTypes.reserve(mimeTypes.count()); + + Q_FOREACH (const MimeType &mt, mimeTypes) { + queryMimeTypes << mt.name(); + } + + QVector queryAncestors; + if (collection.queryCollections().isEmpty()) { + queryAncestors << 0; + recursive = true; + } else { + const QStringList collectionIds = collection.queryCollections().split(QLatin1Char(' ')); + queryAncestors.reserve(collectionIds.count()); + Q_FOREACH (const QString &colId, collectionIds) { + queryAncestors << colId.toLongLong(); + } + } + + // Always query the given collections + QVector queryCollections = queryAncestors; + + if (recursive) { + // Resolve subcollections if necessary + queryCollections += SearchHelper::matchSubcollectionsByMimeType(queryAncestors, queryMimeTypes); + } + + //This happens if we try to search a virtual collection in recursive mode (because virtual collections are excluded from listCollectionsRecursive) + if (queryCollections.isEmpty()) { + akDebug() << "No collections to search, you're probably trying to search a virtual collection."; + wakeUpCaller(cond); + return; + } + + // Query all plugins for search results + SearchRequest request("searchUpdate-" + QByteArray::number(QDateTime::currentDateTime().toTime_t())); + request.setCollections(queryCollections); + request.setMimeTypes(queryMimeTypes); + request.setQuery(collection.queryString()); + request.setRemoteSearch(remoteSearch); + request.setStoreResults(true); + request.setProperty("SearchCollection", QVariant::fromValue(collection)); + connect(&request, &SearchRequest::resultsAvailable, + this, &SearchManager::searchUpdateResultsAvailable); + request.exec(); // blocks until all searches are done + + const QSet results = request.results(); + + // Get all items in the collection + QueryBuilder qb(CollectionPimItemRelation::tableName()); + qb.addColumn(CollectionPimItemRelation::rightColumn()); + qb.addValueCondition(CollectionPimItemRelation::leftColumn(), Query::Equals, collection.id()); + if (!qb.exec()) { + wakeUpCaller(cond); + return; + } + + DataStore::self()->beginTransaction(); + + // Unlink all items that were not in search results from the collection + QVariantList toRemove; + while (qb.query().next()) { + const qint64 id = qb.query().value(0).toLongLong(); + if (!results.contains(id)) { + toRemove << id; + Collection::removePimItem(collection.id(), id); + } + } + + if (!DataStore::self()->commitTransaction()) { + wakeUpCaller(cond); + return; + } + + if (!toRemove.isEmpty()) { + SelectQueryBuilder qb; + qb.addValueCondition(PimItem::idFullColumnName(), Query::In, toRemove); + if (!qb.exec()) { + wakeUpCaller(cond); + return; + } + + const QVector removedItems = qb.result(); + DataStore::self()->notificationCollector()->itemsUnlinked(removedItems, collection); + } + + akDebug() << "Search update finished"; + akDebug() << "All results:" << results.count(); + akDebug() << "Removed results:" << toRemove.count(); + + wakeUpCaller(cond); +} + +void SearchManager::searchUpdateResultsAvailable(const QSet &results) +{ + const Collection collection = sender()->property("SearchCollection").value(); + akDebug() << "searchUpdateResultsAvailable" << collection.id() << results.count() << "results"; + + QSet newMatches = results; + QSet existingMatches; + { + QueryBuilder qb(CollectionPimItemRelation::tableName()); + qb.addColumn(CollectionPimItemRelation::rightColumn()); + qb.addValueCondition(CollectionPimItemRelation::leftColumn(), Query::Equals, collection.id()); + if (!qb.exec()) { + return; + } + + while (qb.query().next()) { + const qint64 id = qb.query().value(0).toLongLong(); + if (newMatches.contains(id)) { + existingMatches << id; + } + } + } + + akDebug() << "Got" << newMatches.count() << "results, out of which" << existingMatches.count() << "are already in the collection"; + + newMatches = newMatches - existingMatches; + + const bool existingTransaction = DataStore::self()->inTransaction(); + if (!existingTransaction) { + DataStore::self()->beginTransaction(); + } + + QVariantList newMatchesVariant; + newMatchesVariant.reserve(newMatches.count()); + Q_FOREACH (qint64 id, newMatches) { + newMatchesVariant << id; + Collection::addPimItem(collection.id(), id); + } + + akDebug() << "Added" << newMatches.count(); + + if (!existingTransaction && !DataStore::self()->commitTransaction()) { + akDebug() << "Failed to commit transaction"; + return; + } + + if (!newMatchesVariant.isEmpty()) { + SelectQueryBuilder qb; + qb.addValueCondition(PimItem::idFullColumnName(), Query::In, newMatchesVariant); + if (!qb.exec()) { + return ; + } + const QVector newItems = qb.result(); + DataStore::self()->notificationCollector()->itemsLinked(newItems, collection); + // Force collector to dispatch the notification now + DataStore::self()->notificationCollector()->dispatchNotifications(); + } +} diff --git a/src/server/search/searchmanager.h b/src/server/search/searchmanager.h new file mode 100644 index 0000000..de30bbf --- /dev/null +++ b/src/server/search/searchmanager.h @@ -0,0 +1,135 @@ +/* + Copyright (c) 2010 Volker Krause + Copyright (c) 2013 Daniel Vrátil + + 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 SEARCHMANAGER_H +#define SEARCHMANAGER_H + +#include "akthread.h" + +#include +#include +#include +#include + +class QSemaphore; +class QTimer; +class QPluginLoader; + +namespace Akonadi { + +class AbstractSearchPlugin; + +namespace Server { + +class NotificationCollector; +class AbstractSearchEngine; +class Collection; + +/** + * SearchManager creates and deletes persistent searches for all currently + * active search engines. + */ +class SearchManager : public AkThread +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.SearchManager") + +public: + /** Create a new search manager with the given @p searchEngines. */ + explicit SearchManager(const QStringList &searchEngines, QObject *parent = 0); + + ~SearchManager(); + + /** + * Returns a global instance of the search manager. + */ + static SearchManager *instance(); + + /** + * Updates the search query asynchronously. Returns immediately + */ + virtual void updateSearchAsync(const Collection &collection); + + /** + * Updates the search query synchronously. + */ + virtual void updateSearch(const Collection &collection); + + /** + * Returns currently available search plugins. + */ + virtual QVector searchPlugins() const; + +public Q_SLOTS: + virtual void scheduleSearchUpdate(); + + /** + * This is called via D-Bus from AgentManager to register an agent with + * search interface. + */ + virtual void registerInstance(const QString &id); + + /** + * This is called via D-Bus from AgentManager to unregister an agent with + * search interface. + */ + virtual void unregisterInstance(const QString &id); + +private Q_SLOTS: + void searchUpdateTimeout(); + void searchUpdateResultsAvailable(const QSet &results); + + /** + * Actual implementation of search updates. + * + * Since caller invokes this method from a different thread, they use + * QMetaObject::invokeMethod(). To still make it possible for callers to behave + * synchronously, we can pass in a QWaitCondition that the code will wake up + * once the search update is completed. + */ + void updateSearchImpl(const Collection &collection, QSemaphore *cond); + +private: + void init() Q_DECL_OVERRIDE; + void quit() Q_DECL_OVERRIDE; + + // Called from main thread + void loadSearchPlugins(); + // Called from manager thread + void initSearchPlugins(); + + static SearchManager *sInstance; + + QStringList mEngineNames; + QVector mPluginLoaders; + QVector mEngines; + QVector mPlugins; + + QTimer *mSearchUpdateTimer; + + QMutex mLock; + QSet mUpdatingCollections; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/search/searchrequest.cpp b/src/server/search/searchrequest.cpp new file mode 100644 index 0000000..b481aa1 --- /dev/null +++ b/src/server/search/searchrequest.cpp @@ -0,0 +1,162 @@ +/* + Copyright (c) 2013, 2014 Daniel Vrátil + + 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 "searchrequest.h" + +#include + +#include "searchtaskmanager.h" +#include "abstractsearchplugin.h" +#include "searchmanager.h" +#include "connection.h" + +#include +#include + +using namespace Akonadi::Server; + +SearchRequest::SearchRequest(const QByteArray &connectionId) + : mConnectionId(connectionId) + , mRemoteSearch(true) + , mStoreResults(false) +{ +} + +SearchRequest::~SearchRequest() +{ +} + +QByteArray SearchRequest::connectionId() const +{ + return mConnectionId; +} + +void SearchRequest::setQuery(const QString &query) +{ + mQuery = query; +} + +QString SearchRequest::query() const +{ + return mQuery; +} + +void SearchRequest::setCollections(const QVector &collectionsIds) +{ + mCollections = collectionsIds; +} + +QVector SearchRequest::collections() const +{ + return mCollections; +} + +void SearchRequest::setMimeTypes(const QStringList &mimeTypes) +{ + mMimeTypes = mimeTypes; +} + +QStringList SearchRequest::mimeTypes() const +{ + return mMimeTypes; +} + +void SearchRequest::setRemoteSearch(bool remote) +{ + mRemoteSearch = remote; +} + +bool SearchRequest::remoteSearch() const +{ + return mRemoteSearch; +} + +void SearchRequest::setStoreResults(bool storeResults) +{ + mStoreResults = storeResults; +} + +QSet SearchRequest::results() const +{ + return mResults; +} + +void SearchRequest::emitResults(const QSet &results) +{ + Q_EMIT resultsAvailable(results); + if (mStoreResults) { + mResults.unite(results); + } +} + +void SearchRequest::searchPlugins() +{ + const QVector plugins = SearchManager::instance()->searchPlugins(); + Q_FOREACH (AbstractSearchPlugin *plugin, plugins) { + const QSet result = plugin->search(mQuery, mCollections.toList(), mMimeTypes); + emitResults(result); + } +} + +void SearchRequest::exec() +{ + akDebug() << "Executing search" << mConnectionId; + + //TODO should we move this to the AgentSearchManager as well? If we keep it here the agents can be searched in parallel + //since the plugin search is executed in this thread directly. + searchPlugins(); + + // If remote search is disabled, just finish here after searching the plugins + if (!mRemoteSearch) { + akDebug() << "Search done" << mConnectionId << "(without remote search)"; + return; + } + + SearchTask task; + task.id = mConnectionId; + task.query = mQuery; + task.mimeTypes = mMimeTypes; + task.collections = mCollections; + task.complete = false; + + SearchTaskManager::instance()->addTask(&task); + + task.sharedLock.lock(); + Q_FOREVER { + if (task.complete) { + akDebug() << "All queries processed!"; + break; + } else { + task.notifier.wait(&task.sharedLock); + + akDebug() << task.pendingResults.count() << "search results available in search" << task.id; + if (!task.pendingResults.isEmpty()) { + emitResults(task.pendingResults); + } + task.pendingResults.clear(); + } + } + + if (!task.pendingResults.isEmpty()) { + emitResults(task.pendingResults); + } + task.sharedLock.unlock(); + + akDebug() << "Search done" << mConnectionId; +} diff --git a/src/server/search/searchrequest.h b/src/server/search/searchrequest.h new file mode 100644 index 0000000..aff1cc8 --- /dev/null +++ b/src/server/search/searchrequest.h @@ -0,0 +1,83 @@ +/* + Copyright (c) 2013, 2014 Daniel Vrátil + + 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_SEARCHREQUEST_H +#define AKONADI_SEARCHREQUEST_H + +#include +#include +#include +#include + +namespace Akonadi { +namespace Server { + +class Connection; + +class SearchRequest : public QObject +{ + Q_OBJECT + +public: + SearchRequest(const QByteArray &connectionId); + ~SearchRequest(); + + void setQuery(const QString &query); + QString query() const; + void setCollections(const QVector &collections); + QVector collections() const; + void setMimeTypes(const QStringList &mimeTypes); + QStringList mimeTypes() const; + void setRemoteSearch(bool remote); + bool remoteSearch() const; + + /** + * Whether results should be stored after they are emitted via resultsAvailable(), + * so that they can be extracted via results() after the search is over. This + * is disabled by default. + */ + void setStoreResults(bool storeResults); + + QByteArray connectionId() const; + + void exec(); + + QSet results() const; + +Q_SIGNALS: + void resultsAvailable(const QSet &results); + +private: + void searchPlugins(); + void emitResults(const QSet &results); + + QByteArray mConnectionId; + QString mQuery; + QVector mCollections; + QStringList mMimeTypes; + bool mRemoteSearch; + bool mStoreResults; + QSet mResults; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SEARCHREQUEST_H diff --git a/src/server/search/searchtaskmanager.cpp b/src/server/search/searchtaskmanager.cpp new file mode 100644 index 0000000..66fbefb --- /dev/null +++ b/src/server/search/searchtaskmanager.cpp @@ -0,0 +1,327 @@ +/* + Copyright (c) 2013, 2014 Daniel Vrátil + + 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 "searchtaskmanager.h" +#include "agentsearchinstance.h" +#include "connection.h" +#include "storage/selectquerybuilder.h" +#include "dbusconnectionpool.h" +#include "entities.h" + +#include + +#include + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +SearchTaskManager *SearchTaskManager::sInstance = 0; + +SearchTaskManager::SearchTaskManager() + : AkThread() + , mShouldStop(false) +{ + setObjectName(QStringLiteral("SearchTaskManager")); + sInstance = this; + + QTimer::singleShot(0, this, &SearchTaskManager::searchLoop); +} + +SearchTaskManager::~SearchTaskManager() +{ + QMutexLocker locker(&mLock); + mShouldStop = true; + mWait.wakeAll(); + locker.unlock(); + + quitThread(); + + mInstancesLock.lock(); + qDeleteAll(mInstances); + mInstancesLock.unlock(); +} + +SearchTaskManager *SearchTaskManager::instance() +{ + Q_ASSERT(sInstance); + return sInstance; +} + +void SearchTaskManager::registerInstance(const QString &id) +{ + QMutexLocker locker(&mInstancesLock); + + akDebug() << "SearchManager::registerInstance(" << id << ")"; + + AgentSearchInstance *instance = mInstances.value(id); + if (instance) { + return; // already registered + } + + instance = new AgentSearchInstance(id); + if (!instance->init()) { + akDebug() << "Failed to initialize Search agent"; + delete instance; + return; + } + + akDebug() << "Registering search instance " << id; + mInstances.insert(id, instance); +} + +void SearchTaskManager::unregisterInstance(const QString &id) +{ + QMutexLocker locker(&mInstancesLock); + + QMap::Iterator it = mInstances.find(id); + if (it != mInstances.end()) { + akDebug() << "Unregistering search instance" << id; + it.value()->deleteLater(); + mInstances.erase(it); + } +} + +void SearchTaskManager::addTask(SearchTask *task) +{ + QueryBuilder qb(Collection::tableName()); + qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), + Collection::resourceIdFullColumnName(), + Resource::idFullColumnName()); + qb.addColumn(Collection::idFullColumnName()); + qb.addColumn(Resource::nameFullColumnName()); + + Q_ASSERT(!task->collections.isEmpty()); + QVariantList list; + list.reserve(task->collections.size()); + Q_FOREACH (qint64 collection, task->collections) { + list << collection; + } + qb.addValueCondition(Collection::idFullColumnName(), Query::In, list); + + if (!qb.exec()) { + throw SearchException(qb.query().lastError().text()); + } + + QSqlQuery query = qb.query(); + if (!query.next()) { + return; + } + + mInstancesLock.lock(); + + org::freedesktop::Akonadi::AgentManager agentManager(DBus::serviceName(DBus::Control), QStringLiteral("/AgentManager"), + DBusConnectionPool::threadConnection()); + do { + const QString resourceId = query.value(1).toString(); + if (!mInstances.contains(resourceId)) { + akDebug() << "Resource" << resourceId << "does not implement Search interface, skipping"; + } else if (!agentManager.agentInstanceOnline(resourceId)) { + akDebug() << "Agent" << resourceId << "is offline, skipping"; + } else if (agentManager.agentInstanceStatus(resourceId) > 2) { // 2 == Broken, 3 == Not Configured + akDebug() << "Agent" << resourceId << "is broken or not configured"; + } else { + const qint64 collectionId = query.value(0).toLongLong(); + akDebug() << "Enqueued search query (" << resourceId << ", " << collectionId << ")"; + task->queries << qMakePair(resourceId, collectionId); + } + } while (query.next()); + mInstancesLock.unlock(); + + QMutexLocker locker(&mLock); + mTasklist.append(task); + mWait.wakeAll(); +} + +void SearchTaskManager::pushResults(const QByteArray &searchId, const QSet &ids, + Connection *connection) +{ + Q_UNUSED(searchId); + + akDebug() << ids.count() << "results for search" << searchId << "pushed from" << connection->context()->resource().name(); + + QMutexLocker locker(&mLock); + ResourceTask *task = mRunningTasks.take(connection->context()->resource().name()); + if (!task) { + akDebug() << "No running task for" << connection->context()->resource().name() << " - maybe it has timed out?"; + return; + } + + if (task->parentTask->id != searchId) { + akDebug() << "Received results for different search - maybe the original task has timed out?"; + akDebug() << "Search is" << searchId << ", but task is" << task->parentTask->id; + return; + } + + task->results = ids; + mPendingResults.append(task); + + mWait.wakeAll(); +} + +bool SearchTaskManager::allResourceTasksCompleted(SearchTask *agentSearchTask) const +{ + // Check for queries pending to be dispatched + if (!agentSearchTask->queries.isEmpty()) { + return false; + } + + // Check for running queries + QMap::const_iterator it = mRunningTasks.begin(); + for (; it != mRunningTasks.end(); ++it) { + if (it.value()->parentTask == agentSearchTask) { + return false; + } + } + + return true; +} + +SearchTaskManager::TasksMap::Iterator SearchTaskManager::cancelRunningTask(TasksMap::Iterator &iter) +{ + ResourceTask *task = iter.value(); + SearchTask *parentTask = task->parentTask; + QMutexLocker locker(&parentTask->sharedLock); + //erase the task before allResourceTasksCompleted + SearchTaskManager::TasksMap::Iterator it = mRunningTasks.erase(iter); + // We're not clearing the results since we don't want to clear successful results from other resources + parentTask->complete = allResourceTasksCompleted(parentTask); + parentTask->notifier.wakeAll(); + delete task; + + return it; +} + +void SearchTaskManager::searchLoop() +{ + qint64 timeout = ULONG_MAX; + + QMutexLocker locker(&mLock); + + Q_FOREVER { + akDebug() << "Search loop is waiting, will wake again in" << timeout << "ms"; + mWait.wait(&mLock, timeout); + + if (mShouldStop) { + Q_FOREACH (SearchTask *task, mTasklist) { + QMutexLocker locker(&task->sharedLock); + task->queries.clear(); + task->notifier.wakeAll(); + } + + QMap::Iterator it = mRunningTasks.begin(); + for (; it != mRunningTasks.end();) { + if (mTasklist.contains(it.value()->parentTask)) { + delete it.value(); + it = mRunningTasks.erase(it); + continue; + } + it = cancelRunningTask(it); + } + + break; + } + + // First notify about available results + while (!mPendingResults.isEmpty()) { + ResourceTask *finishedTask = mPendingResults.first(); + mPendingResults.remove(0); + akDebug() << "Pending results from" << finishedTask->resourceId << "for collection" << finishedTask->collectionId << "for search" << finishedTask->parentTask->id << "available!"; + SearchTask *parentTask = finishedTask->parentTask; + QMutexLocker locker(&parentTask->sharedLock); + // We need to append, this agent search task is shared + parentTask->pendingResults += finishedTask->results; + parentTask->complete = allResourceTasksCompleted(parentTask); + parentTask->notifier.wakeAll(); + delete finishedTask; + } + + // No check whether there are any tasks running longer than 1 minute and kill them + QMap::Iterator it = mRunningTasks.begin(); + const qint64 now = QDateTime::currentMSecsSinceEpoch(); + for (; it != mRunningTasks.end();) { + ResourceTask *task = it.value(); + if (now - task->timestamp > 60 * 1000) { + // Remove the task - and signal to parent task that it has "finished" without results + akDebug() << "Resource task" << task->resourceId << "for search" << task->parentTask->id << "timed out!"; + it = cancelRunningTask(it); + } else { + ++it; + } + } + + if (!mTasklist.isEmpty()) { + SearchTask *task = mTasklist.first(); + akDebug() << "Search task" << task->id << "available!"; + if (task->queries.isEmpty()) { + akDebug() << "nothing to do for task"; + QMutexLocker locker(&task->sharedLock); + //After this the AgentSearchTask will be destroyed + task->complete = true; + task->notifier.wakeAll(); + mTasklist.remove(0); + continue; + } + + QVector >::iterator it = task->queries.begin(); + for (; it != task->queries.end();) { + if (!mRunningTasks.contains(it->first)) { + akDebug() << "\t Sending query for collection" << it->second << "to resource" << it->first; + ResourceTask *rTask = new ResourceTask; + rTask->resourceId = it->first; + rTask->collectionId = it->second; + rTask->parentTask = task; + rTask->timestamp = QDateTime::currentMSecsSinceEpoch(); + mRunningTasks.insert(it->first, rTask); + + mInstancesLock.lock(); + AgentSearchInstance *instance = mInstances.value(it->first); + if (!instance) { + mInstancesLock.unlock(); + // Resource disappeared in the meanwhile + continue; + } + + instance->search(task->id, task->query, it->second); + mInstancesLock.unlock(); + + task->sharedLock.lock(); + it = task->queries.erase(it); + task->sharedLock.unlock(); + } else { + ++it; + } + } + // Yay! We managed to dispatch all requests! + if (task->queries.isEmpty()) { + akDebug() << "All queries from task" << task->id << "dispatched!"; + mTasklist.remove(0); + } + + timeout = 60 * 1000; // check whether all tasks have finished within a minute + } else { + if (mRunningTasks.isEmpty()) { + timeout = ULONG_MAX; + } + } + } +} diff --git a/src/server/search/searchtaskmanager.h b/src/server/search/searchtaskmanager.h new file mode 100644 index 0000000..8da864d --- /dev/null +++ b/src/server/search/searchtaskmanager.h @@ -0,0 +1,120 @@ +/* + Copyright (c) 2013, 2014 Daniel Vrátil + + 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_SEARCHTASKMANAGER_H +#define AKONADI_SEARCHTASKMANAGER_H + +#include "akthread.h" + +#include +#include +#include +#include +#include +#include +#include "exception.h" +#include "agentmanagerinterface.h" + +namespace Akonadi { +namespace Server { + +class AkonadiServer; +class Connection; +class SearchRequest; +class AgentSearchInstance; + +class SearchResultsRetriever; + +class SearchTask +{ +public: + QByteArray id; + QString query; + QStringList mimeTypes; + QVector collections; + bool complete; + + QMutex sharedLock; + QWaitCondition notifier; + + QVector > queries; + QSet pendingResults; +}; + +class SearchTaskManager : public AkThread +{ + Q_OBJECT + +public: + static SearchTaskManager *instance(); + + ~SearchTaskManager(); + + void registerInstance(const QString &id); + void unregisterInstance(const QString &id); + + void addTask(SearchTask *task); + + void pushResults(const QByteArray &searchId, const QSet &ids, Connection *connection); + +private Q_SLOTS: + void searchLoop(); + +private: + class ResourceTask + { + public: + QString resourceId; + qint64 collectionId; + SearchTask *parentTask; + QSet results; + + qint64 timestamp; + }; + + typedef QMap TasksMap; + + static SearchTaskManager *sInstance; + + explicit SearchTaskManager(); + bool mShouldStop; + + TasksMap::Iterator cancelRunningTask(TasksMap::Iterator &iter); + bool allResourceTasksCompleted(SearchTask *agentSearchTask) const; + + QMap mInstances; + QMutex mInstancesLock; + + QWaitCondition mWait; + QMutex mLock; + + QVector mTasklist; + + QMap mRunningTasks; + QVector mPendingResults; + + friend class AkonadiServer; +}; + +AKONADI_EXCEPTION_MAKE_INSTANCE(SearchException); + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SEARCHTASKMANAGER_H diff --git a/src/server/storage/CMakeLists.txt b/src/server/storage/CMakeLists.txt new file mode 100644 index 0000000..041e98c --- /dev/null +++ b/src/server/storage/CMakeLists.txt @@ -0,0 +1,8 @@ +find_program(XMLLINT_EXECUTABLE xmllint) + +if(NOT XMLLINT_EXECUTABLE) + message(STATUS "xmllint not found, skipping akonadidb.xml schema validation") +else() + add_test(akonadidb-xmllint ${XMLLINT_EXECUTABLE} --noout --schema ${CMAKE_CURRENT_SOURCE_DIR}/akonadidb.xsd ${CMAKE_CURRENT_SOURCE_DIR}/akonadidb.xml) + add_test(akonadidbupdate-xmllint ${XMLLINT_EXECUTABLE} --noout --schema ${CMAKE_CURRENT_SOURCE_DIR}/dbupdate.xsd ${CMAKE_CURRENT_SOURCE_DIR}/dbupdate.xml) +endif() diff --git a/src/server/storage/akonadi-mysql-client.sh b/src/server/storage/akonadi-mysql-client.sh new file mode 100755 index 0000000..ee2b81a --- /dev/null +++ b/src/server/storage/akonadi-mysql-client.sh @@ -0,0 +1,15 @@ +#! /bin/sh +# connect to mysqld started by akonadi +# useful for developing + +if [ -z "$1" ]; then + akonadisocket="$HOME/.local/share/akonadi/socket-`hostname`/mysql.socket" +else + if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + echo "Usage: $0 [instance identifier]" + exit 1; + fi + akonadisocket="$HOME/.local/share/akonadi/instance/$1/socket-`hostname`/mysql.socket" +fi + +mysql --socket=$akonadisocket akonadi diff --git a/src/server/storage/akonadi-mysql-server.sh b/src/server/storage/akonadi-mysql-server.sh new file mode 100755 index 0000000..adb4da1 --- /dev/null +++ b/src/server/storage/akonadi-mysql-server.sh @@ -0,0 +1,16 @@ +#! /bin/sh +# start mysqld as started by akonadi +# useful for developing + +akonadihome=$HOME/.local/share/akonadi +globalconfig=$KDEDIR/share/akonadi/mysql-global.conf +localconfig=$HOME/.config/akonadi/mysql-local.conf +if [ -f $globalconfig ]; then + cat $globalconfig $localconfig > $akonadihome/mysql.conf +fi + +/usr/sbin/mysqld \ + --defaults-file=$akonadihome/mysql.conf \ + --datadir=$akonadihome/db_data/ \ + "--socket=$akonadihome/socket-`hostname`/mysql.socket" + diff --git a/src/server/storage/akonadidb.qrc b/src/server/storage/akonadidb.qrc new file mode 100644 index 0000000..b1ab061 --- /dev/null +++ b/src/server/storage/akonadidb.qrc @@ -0,0 +1,5 @@ + + + dbupdate.xml + + diff --git a/src/server/storage/akonadidb.xml b/src/server/storage/akonadidb.xml new file mode 100644 index 0000000..7d7026a --- /dev/null +++ b/src/server/storage/akonadidb.xml @@ -0,0 +1,236 @@ + + + + + + + + + Contains the schema version of the database. + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + This meta data is stored inside akonadi to provide fast access. + + +
+ + + + + + + + + + + create/modified time + + + read access time + + + Indicates that this item has unsaved changes. + + + + + + + +
+ + + This meta data is stored inside akonadi to provide fast access. + + +
+ + + Table containing item part types. + + + Part name, without namespace. + + + Part namespace. + + +
+ + + + + + + + + + + +
+ + + + + + + +
+ + + + + +
+ + + + + + +
+ + + + + + + +
+ + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + + Specifies allowed MimeType for a Collection + + + + Used to associate items with search folders. + +
diff --git a/src/server/storage/akonadidb.xsd b/src/server/storage/akonadidb.xsd new file mode 100644 index 0000000..f447a83 --- /dev/null +++ b/src/server/storage/akonadidb.xsd @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/server/storage/collectionqueryhelper.cpp b/src/server/storage/collectionqueryhelper.cpp new file mode 100644 index 0000000..b1f8dfc --- /dev/null +++ b/src/server/storage/collectionqueryhelper.cpp @@ -0,0 +1,166 @@ +/* + 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 "collectionqueryhelper.h" + +#include "connection.h" +#include "entities.h" +#include "storage/querybuilder.h" +#include "storage/selectquerybuilder.h" +#include "handler.h" +#include "queryhelper.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +void CollectionQueryHelper::remoteIdToQuery(const QStringList &rids, Connection *connection, QueryBuilder &qb) +{ + if (rids.size() == 1) { + qb.addValueCondition(Collection::remoteIdFullColumnName(), Query::Equals, rids.first()); + } else { + qb.addValueCondition(Collection::remoteIdFullColumnName(), Query::In, rids); + } + + if (connection->context()->resource().isValid()) { + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, connection->context()->resource().id()); + } +} + +void CollectionQueryHelper::scopeToQuery(const Scope &scope, Connection *connection, QueryBuilder &qb) +{ + if (scope.scope() == Scope::Uid) { + QueryHelper::setToQuery(scope.uidSet(), Collection::idFullColumnName(), qb); + } else if (scope.scope() == Scope::Rid) { + if (connection->context()->collectionId() <= 0 && !connection->context()->resource().isValid()) { + throw HandlerException("Operations based on remote identifiers require a resource or collection context"); + } + CollectionQueryHelper::remoteIdToQuery(scope.ridSet(), connection, qb); + } else if (scope.scope() == Scope::HierarchicalRid) { + if (!connection->context()->resource().isValid()) { + throw HandlerException("Operations based on hierarchical remote identifiers require a resource or collection context"); + } + const Collection c = CollectionQueryHelper::resolveHierarchicalRID(scope.hridChain(), connection->context()->resource().id()); + qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, c.id()); + } else { + throw HandlerException("WTF?"); + } +} + +bool CollectionQueryHelper::hasAllowedName(const Collection &collection, const QString &name, Collection::Id parent) +{ + Q_UNUSED(collection); + SelectQueryBuilder qb; + if (parent > 0) { + qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, parent); + } else { + qb.addValueCondition(Collection::parentIdColumn(), Query::Is, QVariant()); + } + qb.addValueCondition(Collection::nameColumn(), Query::Equals, name); + if (!qb.exec()) { + return false; + } + const QVector result = qb.result(); + if (result.size() > 0) { + if (result.first().id() == collection.id()) { + return true; + } + return false; + } + return true; +} + +bool CollectionQueryHelper::canBeMovedTo(const Collection &collection, const Collection &_parent) +{ + if (_parent.isValid()) { + Collection parent = _parent; + Q_FOREVER { + if (parent.id() == collection.id()) { + return false; // target is child of source + } + if (parent.parentId() == 0) { + break; + } + parent = parent.parent(); + } + } + return hasAllowedName(collection, collection.name(), _parent.id()); +} + +Collection CollectionQueryHelper::resolveHierarchicalRID(const QVector &ridChain, Resource::Id resId) +{ + if (ridChain.size() < 2) { + throw HandlerException("Empty or incomplete hierarchical RID chain"); + } + if (!ridChain.last().isEmpty()) { + throw HandlerException("Hierarchical RID chain is not root-terminated"); + } + Collection::Id parentId = 0; + Collection result; + for (int i = ridChain.size() - 2; i >= 0; --i) { + SelectQueryBuilder qb; + if (parentId > 0) { + qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, parentId); + } else { + qb.addValueCondition(Collection::parentIdColumn(), Query::Is, QVariant()); + } + qb.addValueCondition(Collection::remoteIdColumn(), Query::Equals, ridChain.at(i).remoteId); + qb.addValueCondition(Collection::resourceIdColumn(), Query::Equals, resId); + if (!qb.exec()) { + throw HandlerException("Unable to execute query"); + } + Collection::List results = qb.result(); + if (results.size() == 0) { + throw HandlerException("Hierarchical RID does not specify an existing collection"); + } else if (results.size() > 1) { + throw HandlerException("Hierarchical RID does not specify a unique collection"); + } + result = results.first(); + parentId = result.id(); + } + return result; +} + +Collection CollectionQueryHelper::singleCollectionFromScope(const Scope &scope, Connection *connection) +{ + // root + if (scope.scope() == Scope::Uid && scope.uidSet().intervals().count() == 1) { + const ImapInterval i = scope.uidSet().intervals().at(0); + if (!i.size()) { // ### why do we need this hack for 0, shouldn't that be size() == 1? + Collection root; + root.setId(0); + return root; + } + } + SelectQueryBuilder qb; + scopeToQuery(scope, connection, qb); + if (!qb.exec()) { + throw HandlerException("Unable to execute query"); + } + const Collection::List cols = qb.result(); + if (cols.isEmpty()) { + throw HandlerException("No collection found"); + } + if (cols.size() > 1) { + throw HandlerException("Collection cannot be uniquely identified"); + } + return cols.first(); +} diff --git a/src/server/storage/collectionqueryhelper.h b/src/server/storage/collectionqueryhelper.h new file mode 100644 index 0000000..2032169 --- /dev/null +++ b/src/server/storage/collectionqueryhelper.h @@ -0,0 +1,78 @@ +/* + 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. +*/ + +#ifndef AKONADI_COLLECTIONQUERYHELPER_H +#define AKONADI_COLLECTIONQUERYHELPER_H + +#include "entities.h" + +#include + +namespace Akonadi { + +class ImapSet; + +namespace Server { + +class Connection; +class QueryBuilder; + +/** + Helper methods to generate WHERE clauses for collection queries based on a Scope object. +*/ +namespace CollectionQueryHelper { + +/** + Add conditions to @p qb for the given remote identifier @p rid. + The rid context is taken from @p connection. +*/ +void remoteIdToQuery(const QStringList &rids, Connection *connection, QueryBuilder &qb); + +/** + Add conditions to @p qb for the given collection operation scope @p scope. + The rid context is taken from @p connection, if none is specified an exception is thrown. +*/ +void scopeToQuery(const Scope &scope, Connection *connection, QueryBuilder &qb); + +/** + Checks if a collection could exist in the given parent folder with the given name. +*/ +bool hasAllowedName(const Collection &collection, const QString &name, Collection::Id parent); + +/** + Checks if a collection could be moved from its current parent into the given one. +*/ +bool canBeMovedTo(const Collection &collection, const Collection &parent); + +/** + Retrieve the collection referred to by the given hierarchical RID chain. +*/ +Collection resolveHierarchicalRID(const QVector &hridChain, Resource::Id resId); + +/** + Returns an existing collection specified by the given scope. If that does not + specify exactly one valid collection, an exception is thrown. +*/ +Collection singleCollectionFromScope(const Scope &scope, Connection *connection); +} + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/collectionstatistics.cpp b/src/server/storage/collectionstatistics.cpp new file mode 100644 index 0000000..780d48d --- /dev/null +++ b/src/server/storage/collectionstatistics.cpp @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "collectionstatistics.h" +#include "querybuilder.h" +#include "countquerybuilder.h" +#include "akdebug.h" +#include "entities.h" + +#include + +#include + +using namespace Akonadi::Server; + +CollectionStatistics *CollectionStatistics::sInstance = 0; + +CollectionStatistics *CollectionStatistics::self() +{ + if (sInstance == 0) { + sInstance = new CollectionStatistics(); + } + return sInstance; +} + +void CollectionStatistics::invalidateCollection(const Collection &col) +{ + QMutexLocker lock(&mCacheLock); + mCache.remove(col.id()); +} + +const CollectionStatistics::Statistics CollectionStatistics::statistics(const Collection &col) +{ + QMutexLocker lock(&mCacheLock); + auto it = mCache.find(col.id()); + if (it == mCache.end()) { + it = mCache.insert(col.id(), getCollectionStatistics(col)); + } + return it.value(); +} + +CollectionStatistics::Statistics CollectionStatistics::getCollectionStatistics(const Collection &col) +{ + static const QString SeenFlagsTableName = QStringLiteral("SeenFlags"); + static const QString IgnoredFlagsTableName = QStringLiteral("IgnoredFlags"); + +#define FLAGS_COLUMN(table, column) \ + QStringLiteral("%1.%2").arg(table##TableName, PimItemFlagRelation::column()) + + // COUNT(DISTINCT PimItemTable.id) + CountQueryBuilder qb(PimItem::tableName(), PimItem::idFullColumnName(), CountQueryBuilder::Distinct); + // SUM(PimItemTable.size) + qb.addAggregation(PimItem::sizeFullColumnName(), QStringLiteral("sum")); + + // SUM(CASE WHEN SeenFlags.flag_id IS NOT NULL OR IgnoredFlags.flag_id IS NOT NULL THEN 1 ELSE 0 END) + // This allows us to get read messages count in a single query with the other + // statistics. It is much than doing two queries, because the database + // only has to calculate the JOINs once. + // + // Flag::retrieveByName() will hit the Entity cache, which allows us to avoid + // a second JOIN with FlagTable, which PostgreSQL seems to struggle to optimize. + Query::Condition cond(Query::Or); + cond.addValueCondition(FLAGS_COLUMN(SeenFlags, rightColumn), Query::IsNot, QVariant()); + cond.addValueCondition(FLAGS_COLUMN(IgnoredFlags, rightColumn), Query::IsNot, QVariant()); + + Query::Case caseStmt(cond, QStringLiteral("1"), QStringLiteral("0")); + qb.addAggregation(caseStmt, QStringLiteral("sum")); + + // We need to join PimItemFlagRelation table twice - once for \SEEN flag and once + // for $IGNORED flag, otherwise entries from PimItemTable get duplicated when an + // item has both flags and the SUM(CASE ...) above returns bogus values + { + Query::Condition seenCondition(Query::And); + seenCondition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, FLAGS_COLUMN(SeenFlags, leftColumn)); + seenCondition.addValueCondition(FLAGS_COLUMN(SeenFlags, rightColumn), Query::Equals, + Flag::retrieveByName(QStringLiteral(AKONADI_FLAG_SEEN)).id()); + qb.addJoin(QueryBuilder::LeftJoin, + QStringLiteral("%1 AS %2").arg(PimItemFlagRelation::tableName(), SeenFlagsTableName), + seenCondition); + } + { + Query::Condition ignoredCondition(Query::And); + ignoredCondition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, FLAGS_COLUMN(IgnoredFlags, leftColumn)); + ignoredCondition.addValueCondition(FLAGS_COLUMN(IgnoredFlags, rightColumn), Query::Equals, + Flag::retrieveByName(QStringLiteral(AKONADI_FLAG_IGNORED)).id()); + qb.addJoin(QueryBuilder::LeftJoin, + QStringLiteral("%1 AS %2").arg(PimItemFlagRelation::tableName(), IgnoredFlagsTableName), + ignoredCondition); + } + +#undef FLAGS_COLUMN + + if (col.isVirtual()) { + qb.addJoin(QueryBuilder::InnerJoin, CollectionPimItemRelation::tableName(), + CollectionPimItemRelation::rightFullColumnName(), PimItem::idFullColumnName()); + qb.addValueCondition(CollectionPimItemRelation::leftFullColumnName(), Query::Equals, col.id()); + } else { + qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, col.id()); + } + + if (!qb.exec()) { + return { -1, -1, -1 }; + } + if (!qb.query().next()) { + akError() << "Error during retrieving result of statistics query:" << qb.query().lastError().text(); + return { -1, -1, -1 }; + } + + return { qb.query().value(0).toLongLong(), + qb.query().value(1).toLongLong(), + qb.query().value(2).toLongLong() }; +} diff --git a/src/server/storage/collectionstatistics.h b/src/server/storage/collectionstatistics.h new file mode 100644 index 0000000..031d897 --- /dev/null +++ b/src/server/storage/collectionstatistics.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2014 Daniel Vrátil + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef AKONADI_SERVER_COLLECTIONSTATISTICS_H +#define AKONADI_SERVER_COLLECTIONSTATISTICS_H + +class QMutex; + +#include +#include + +namespace Akonadi { +namespace Server { + +class Collection; + +/** + * Provides cache for collection statistics + * + * Collection statistics are requested very often, so to take some load from the + * database we cache the results until the statistics are invalidated (see + * NotificationCollector, which takes care for invalidating the statistics). + * + * The cache (together with optimization of the actual SQL query) seems to + * massively improve initial folder listing on system start (when IO and CPU loads + * are very high). + */ +class CollectionStatistics +{ +public: + struct Statistics + { + qint64 count; + qint64 size; + qint64 read; + }; + + static CollectionStatistics *self(); + + const Statistics statistics(const Collection &col); + void invalidateCollection(const Collection &col); + +private: + Statistics getCollectionStatistics(const Collection &col); + + QMutex mCacheLock; + QHash mCache; + + static CollectionStatistics *sInstance; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SERVER_COLLECTIONSTATISTICS_H diff --git a/src/server/storage/countquerybuilder.h b/src/server/storage/countquerybuilder.h new file mode 100644 index 0000000..22d7b0f --- /dev/null +++ b/src/server/storage/countquerybuilder.h @@ -0,0 +1,88 @@ +/* + 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_COUNTQUERYBUILDER_H +#define AKONADI_COUNTQUERYBUILDER_H + +#include "storage/querybuilder.h" + +#include + +#include "akonadiserver_debug.h" +#include + +namespace Akonadi { +namespace Server { + +/** + Helper class for creating queries to count elements in a database. +*/ +class CountQueryBuilder : public QueryBuilder +{ +public: + enum CountMode { + All, + Distinct + }; + + /** + Creates a new query builder that counts all entries in @p table. + */ + explicit inline CountQueryBuilder(const QString &table) + : QueryBuilder(table, Select) + { + addColumn(QStringLiteral("count(*)")); + } + + /** + * Creates a new query builder that counts entries in @p column of @p table. + * If @p mode is set to @c Distinct, duplicate entries in that column are ignored. + */ + inline CountQueryBuilder(const QString &table, const QString &column, CountMode mode) + : QueryBuilder(table, Select) + { + Q_ASSERT(!table.isEmpty()); + Q_ASSERT(!column.isEmpty()); + QString s = QStringLiteral("count("); + if (mode == Distinct) { + s += QLatin1String("DISTINCT "); + } + s += column; + s += QLatin1Char(')'); + addColumn(s); + } + + /** + Returns the result of this query. + @returns -1 on error. + */ + inline int result() + { + if (!query().next()) { + akDebug() << "Error during retrieving result of query:" << query().lastError().text(); + return -1; + } + return query().value(0).toInt(); + } +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/datastore.cpp b/src/server/storage/datastore.cpp new file mode 100644 index 0000000..f07ab30 --- /dev/null +++ b/src/server/storage/datastore.cpp @@ -0,0 +1,1440 @@ +/*************************************************************************** + * Copyright (C) 2006 by Andreas Gungl * + * Copyright (C) 2007 by Robert Zwerus * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "datastore.h" + +#include "dbconfig.h" +#include "dbinitializer.h" +#include "dbupdater.h" +#include "notificationmanager.h" +#include "tracer.h" +#include "transaction.h" +#include "selectquerybuilder.h" +#include "handlerhelper.h" +#include "countquerybuilder.h" +#include "parthelper.h" +#include "handler.h" +#include "collectionqueryhelper.h" +#include "akonadischema.h" +#include "parttypehelper.h" +#include "querycache.h" +#include "queryhelper.h" +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +static QMutex sTransactionMutex; +bool DataStore::s_hasForeignKeyConstraints = false; + +QThreadStorage DataStore::sInstances; + +#define TRANSACTION_MUTEX_LOCK if ( DbType::isSystemSQLite( m_database ) ) sTransactionMutex.lock() +#define TRANSACTION_MUTEX_UNLOCK if ( DbType::isSystemSQLite( m_database ) ) sTransactionMutex.unlock() + +#define setBoolPtr(ptr, val) \ +{ \ + if ((ptr)) { \ + *(ptr) = (val); \ + } \ +} + +/*************************************************************************** + * DataStore * + ***************************************************************************/ +DataStore::DataStore() + : QObject() + , m_dbOpened(false) + , m_transactionLevel(0) + , mNotificationCollector(0) + , m_keepAliveTimer(0) +{ + notificationCollector(); + + if (DbConfig::configuredDatabase()->driverName() == QLatin1String("QMYSQL")) { + // Send a dummy query to MySQL every 1 hour to keep the connection alive, + // otherwise MySQL just drops the connection and our subsequent queries fail + // without properly reporting the error + m_keepAliveTimer = new QTimer(this); + m_keepAliveTimer->setInterval(3600 * 1000); + QObject::connect(m_keepAliveTimer, &QTimer::timeout, + this, &DataStore::sendKeepAliveQuery); + m_keepAliveTimer->start(); + } +} + +DataStore::~DataStore() +{ + if (m_dbOpened) { + close(); + } +} + +void DataStore::open() +{ + m_connectionName = QUuid::createUuid().toString() + QString::number(reinterpret_cast(QThread::currentThread())); + Q_ASSERT(!QSqlDatabase::contains(m_connectionName)); + + m_database = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), m_connectionName); + DbConfig::configuredDatabase()->apply(m_database); + + if (!m_database.isValid()) { + m_dbOpened = false; + return; + } + m_dbOpened = m_database.open(); + + if (!m_dbOpened) { + debugLastDbError("Cannot open database."); + } else { + akDebug() << "Database" << m_database.databaseName() << "opened using driver" << m_database.driverName(); + } + + DbConfig::configuredDatabase()->initSession(m_database); +} + +QSqlDatabase DataStore::database() +{ + if (!m_dbOpened) { + open(); + } + return m_database; +} + +void DataStore::close() +{ + + if (m_keepAliveTimer) { + m_keepAliveTimer->stop(); + } + + if (!m_dbOpened) { + return; + } + + if (inTransaction()) { + // By setting m_transactionLevel to '1' here, we skip all nested transactions + // and rollback the outermost transaction. + m_transactionLevel = 1; + rollbackTransaction(); + } + + QueryCache::clear(); + m_database.close(); + m_database = QSqlDatabase(); + QSqlDatabase::removeDatabase(m_connectionName); + + m_dbOpened = false; +} + +bool DataStore::init() +{ + Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); + + AkonadiSchema schema; + DbInitializer::Ptr initializer = DbInitializer::createInstance(database(), &schema); + if (!initializer->run()) { + akError() << initializer->errorMsg(); + return false; + } + s_hasForeignKeyConstraints = initializer->hasForeignKeyConstraints(); + + if (QFile::exists(QStringLiteral(":dbupdate.xml"))) { + DbUpdater updater(database(), QStringLiteral(":dbupdate.xml")); + if (!updater.run()) { + return false; + } + } else { + qCWarning(AKONADISERVER_LOG) << "Warning: dbupdate.xml not found, skipping updates"; + } + + if (!initializer->updateIndexesAndConstraints()) { + akError() << initializer->errorMsg(); + return false; + } + + // enable caching for some tables + MimeType::enableCache(true); + Flag::enableCache(true); + Resource::enableCache(true); + Collection::enableCache(true); + PartType::enableCache(true); + + return true; +} + +NotificationCollector *DataStore::notificationCollector() +{ + if (mNotificationCollector == 0) { + mNotificationCollector = new NotificationCollector(this); + NotificationManager::self()->connectNotificationCollector(notificationCollector()); + } + + return mNotificationCollector; +} + +DataStore *DataStore::self() +{ + if (!sInstances.hasLocalData()) { + sInstances.setLocalData(new DataStore()); + } + return sInstances.localData(); +} + +bool DataStore::hasDataStore() +{ + return sInstances.hasLocalData(); +} + +/* --- ItemFlags ----------------------------------------------------- */ + +bool DataStore::setItemsFlags(const PimItem::List &items, const QVector &flags, + bool *flagsChanged, const Collection &col, bool silent) +{ + QSet removedFlags; + QSet addedFlags; + QVariantList insIds; + QVariantList insFlags; + Query::Condition delConds(Query::Or); + + setBoolPtr(flagsChanged, false); + + Q_FOREACH (const PimItem &item, items) { + const Flag::List itemFlags = item.flags(); + Q_FOREACH (const Flag &flag, itemFlags) { + if (!flags.contains(flag)) { + removedFlags << flag.name().toLatin1(); + Query::Condition cond; + cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::Equals, item.id()); + cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::Equals, flag.id()); + delConds.addCondition(cond); + } + } + + Q_FOREACH (const Flag &flag, flags) { + if (!itemFlags.contains(flag)) { + addedFlags << flag.name().toLatin1(); + insIds << item.id(); + insFlags << flag.id(); + } + } + } + + if (!removedFlags.empty()) { + QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete); + qb.addCondition(delConds); + if (!qb.exec()) { + return false; + } + } + + if (!addedFlags.empty()) { + QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert); + qb2.setColumnValue(PimItemFlagRelation::leftColumn(), insIds); + qb2.setColumnValue(PimItemFlagRelation::rightColumn(), insFlags); + qb2.setIdentificationColumn(QString()); + if (!qb2.exec()) { + return false; + } + } + + if (!silent && (!addedFlags.isEmpty() || !removedFlags.isEmpty())) { + mNotificationCollector->itemsFlagsChanged(items, addedFlags, removedFlags, col); + } + + setBoolPtr(flagsChanged, (addedFlags != removedFlags)); + + return true; +} + +bool DataStore::doAppendItemsFlag(const PimItem::List &items, const Flag &flag, + const QSet &existing, const Collection &col, + bool silent) +{ + QVariantList flagIds; + QVariantList appendIds; + PimItem::List appendItems; + Q_FOREACH (const PimItem &item, items) { + if (existing.contains(item.id())) { + continue; + } + + flagIds << flag.id(); + appendIds << item.id(); + appendItems << item; + } + + if (appendItems.isEmpty()) { + return true; // all items have the desired flags already + } + + QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert); + qb2.setColumnValue(PimItemFlagRelation::leftColumn(), appendIds); + qb2.setColumnValue(PimItemFlagRelation::rightColumn(), flagIds); + qb2.setIdentificationColumn(QString()); + if (!qb2.exec()) { + akDebug() << "Failed to execute query:" << qb2.query().lastError(); + return false; + } + + if (!silent) { + mNotificationCollector->itemsFlagsChanged(appendItems, QSet() << flag.name().toLatin1(), + QSet(), col); + } + + return true; +} + +bool DataStore::appendItemsFlags(const PimItem::List &items, const QVector &flags, + bool *flagsChanged, bool checkIfExists, + const Collection &col, bool silent) +{ + QSet added; + + QVariantList itemsIds; + itemsIds.reserve(items.count()); + Q_FOREACH (const PimItem &item, items) { + itemsIds.append(item.id()); + } + + setBoolPtr(flagsChanged, false); + + Q_FOREACH (const Flag &flag, flags) { + QSet existing; + if (checkIfExists) { + QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Select); + Query::Condition cond; + cond.addValueCondition(PimItemFlagRelation::rightColumn(), Query::Equals, flag.id()); + cond.addValueCondition(PimItemFlagRelation::leftColumn(), Query::In, itemsIds); + qb.addColumn(PimItemFlagRelation::leftColumn()); + qb.addCondition(cond); + + if (!qb.exec()) { + akDebug() << "Failed to execute query:" << qb.query().lastError(); + return false; + } + + QSqlQuery query = qb.query(); + if (query.driver()->hasFeature(QSqlDriver::QuerySize)) { + //The query size feature is not suppoerted by the sqllite driver + if (query.size() == items.count()) { + continue; + } + setBoolPtr(flagsChanged, true); + } + + while (query.next()) { + existing << query.value(0).value(); + } + if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) { + if (existing.size() != items.count()) { + setBoolPtr(flagsChanged, true); + } + } + } + + if (!doAppendItemsFlag(items, flag, existing, col, silent)) { + return false; + } + } + + return true; +} + +bool DataStore::removeItemsFlags(const PimItem::List &items, const QVector &flags, + bool *flagsChanged, const Collection &col, bool silent) +{ + QSet removedFlags; + QVariantList itemsIds; + QVariantList flagsIds; + + setBoolPtr(flagsChanged, false); + itemsIds.reserve(items.count()); + + Q_FOREACH (const PimItem &item, items) { + itemsIds << item.id(); + for (int i = 0; i < flags.count(); ++i) { + const QByteArray flagName = flags[i].name().toLatin1(); + if (!removedFlags.contains(flagName)) { + flagsIds << flags[i].id(); + removedFlags << flagName; + } + } + } + + // Delete all given flags from all given items in one go + QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete); + Query::Condition cond(Query::And); + cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::In, flagsIds); + cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::In, itemsIds); + qb.addCondition(cond); + if (!qb.exec()) { + return false; + } + + if (qb.query().numRowsAffected() != 0) { + setBoolPtr(flagsChanged, true); + if (!silent) { + mNotificationCollector->itemsFlagsChanged(items, QSet(), removedFlags, col); + } + } + + return true; +} + +/* --- ItemTags ----------------------------------------------------- */ + +bool DataStore::setItemsTags(const PimItem::List &items, const Tag::List &tags, + bool *tagsChanged, bool silent) +{ + QSet removedTags; + QSet addedTags; + QVariantList insIds; + QVariantList insTags; + Query::Condition delConds(Query::Or); + + setBoolPtr(tagsChanged, false); + + Q_FOREACH (const PimItem &item, items) { + const Tag::List itemTags = item.tags(); + Q_FOREACH (const Tag &tag, itemTags) { + if (!tags.contains(tag)) { + // Remove tags from items that had it set + removedTags << tag.id(); + Query::Condition cond; + cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::Equals, item.id()); + cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, tag.id()); + delConds.addCondition(cond); + } + } + + Q_FOREACH (const Tag &tag, tags) { + if (!itemTags.contains(tag)) { + // Add tags to items that did not have the tag + addedTags << tag.id(); + insIds << item.id(); + insTags << tag.id(); + } + } + } + + if (!removedTags.empty()) { + QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete); + qb.addCondition(delConds); + if (!qb.exec()) { + return false; + } + } + + if (!addedTags.empty()) { + QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert); + qb2.setColumnValue(PimItemTagRelation::leftColumn(), insIds); + qb2.setColumnValue(PimItemTagRelation::rightColumn(), insTags); + qb2.setIdentificationColumn(QString()); + if (!qb2.exec()) { + return false; + } + } + + if (!silent && (!addedTags.empty() || !removedTags.empty())) { + mNotificationCollector->itemsTagsChanged(items, addedTags, removedTags); + } + + setBoolPtr(tagsChanged, (addedTags != removedTags)); + + return true; +} + +bool DataStore::doAppendItemsTag(const PimItem::List &items, const Tag &tag, + const QSet &existing, const Collection &col, + bool silent) +{ + QVariantList tagIds; + QVariantList appendIds; + PimItem::List appendItems; + Q_FOREACH (const PimItem &item, items) { + if (existing.contains(item.id())) { + continue; + } + + tagIds << tag.id(); + appendIds << item.id(); + appendItems << item; + } + + if (appendItems.isEmpty()) { + return true; // all items have the desired tags already + } + + QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert); + qb2.setColumnValue(PimItemTagRelation::leftColumn(), appendIds); + qb2.setColumnValue(PimItemTagRelation::rightColumn(), tagIds); + qb2.setIdentificationColumn(QString()); + if (!qb2.exec()) { + akDebug() << "Failed to execute query:" << qb2.query().lastError(); + return false; + } + + if (!silent) { + mNotificationCollector->itemsTagsChanged(appendItems, QSet() << tag.id(), + QSet(), col); + } + + return true; +} + +bool DataStore::appendItemsTags(const PimItem::List &items, const Tag::List &tags, + bool *tagsChanged, bool checkIfExists, + const Collection &col, bool silent) +{ + QSet added; + + QVariantList itemsIds; + itemsIds.reserve(items.count()); + Q_FOREACH (const PimItem &item, items) { + itemsIds.append(item.id()); + } + + setBoolPtr(tagsChanged, false); + + Q_FOREACH (const Tag &tag, tags) { + QSet existing; + if (checkIfExists) { + QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Select); + Query::Condition cond; + cond.addValueCondition(PimItemTagRelation::rightColumn(), Query::Equals, tag.id()); + cond.addValueCondition(PimItemTagRelation::leftColumn(), Query::In, itemsIds); + qb.addColumn(PimItemTagRelation::leftColumn()); + qb.addCondition(cond); + + if (!qb.exec()) { + akDebug() << "Failed to execute query:" << qb.query().lastError(); + return false; + } + + QSqlQuery query = qb.query(); + if (query.size() == items.count()) { + continue; + } + + setBoolPtr(tagsChanged, true); + + while (query.next()) { + existing << query.value(0).value(); + } + } + + if (!doAppendItemsTag(items, tag, existing, col, silent)) { + return false; + } + } + + return true; +} + +bool DataStore::removeItemsTags(const PimItem::List &items, const Tag::List &tags, + bool *tagsChanged, bool silent) +{ + QSet removedTags; + QVariantList itemsIds; + QVariantList tagsIds; + + setBoolPtr(tagsChanged, false); + itemsIds.reserve(items.count()); + + Q_FOREACH (const PimItem &item, items) { + itemsIds << item.id(); + for (int i = 0; i < tags.count(); ++i) { + const qint64 tagId = tags[i].id(); + if (!removedTags.contains(tagId)) { + tagsIds << tagId; + removedTags << tagId; + } + } + } + + // Delete all given tags from all given items in one go + QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete); + Query::Condition cond(Query::And); + cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, tagsIds); + cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::In, itemsIds); + qb.addCondition(cond); + if (!qb.exec()) { + return false; + } + + if (qb.query().numRowsAffected() != 0) { + setBoolPtr(tagsChanged, true); + if (!silent) { + mNotificationCollector->itemsTagsChanged(items, QSet(), removedTags); + } + } + + return true; +} + +bool DataStore::removeTags(const Tag::List &tags, bool silent) +{ + // Currently the "silent" argument is only for API symmetry + Q_UNUSED(silent); + + QVariantList removedTagsIds; + QSet removedTags; + removedTagsIds.reserve(tags.count()); + removedTags.reserve(tags.count()); + Q_FOREACH (const Tag &tag, tags) { + removedTagsIds << tag.id(); + removedTags << tag.id(); + } + + // Get all PIM items that we will untag + SelectQueryBuilder itemsQuery; + itemsQuery.addJoin(QueryBuilder::LeftJoin, PimItemTagRelation::tableName(), PimItemTagRelation::leftFullColumnName(), PimItem::idFullColumnName()); + itemsQuery.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, removedTagsIds); + + if (!itemsQuery.exec()) { + qCDebug(AKONADISERVER_LOG) << "Failed to execute query: " << itemsQuery.query().lastError(); + return false; + } + const PimItem::List items = itemsQuery.result(); + + if (!items.isEmpty()) { + DataStore::self()->notificationCollector()->itemsTagsChanged(items, QSet(), removedTags); + } + + Q_FOREACH (const Tag &tag, tags) { + // Emit special tagRemoved notification for each resource that owns the tag + QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Select); + qb.addColumn(TagRemoteIdResourceRelation::remoteIdFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), + TagRemoteIdResourceRelation::resourceIdFullColumnName(), Resource::idFullColumnName()); + qb.addColumn(Resource::nameFullColumnName()); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdFullColumnName(), Query::Equals, tag.id()); + if (!qb.exec()) { + qCDebug(AKONADISERVER_LOG) << "Failed to execute query: " << qb.query().lastError(); + return false; + } + + // Emit specialized notifications for each resource + QSqlQuery query = qb.query(); + while (query.next()) { + const QString rid = query.value(0).toString(); + const QByteArray resource = query.value(1).toByteArray(); + + DataStore::self()->notificationCollector()->tagRemoved(tag, resource, rid); + } + + // And one for clients - without RID + DataStore::self()->notificationCollector()->tagRemoved(tag, QByteArray(), QString()); + } + + // Just remove the tags, table constraints will take care of the rest + QueryBuilder qb(Tag::tableName(), QueryBuilder::Delete); + qb.addValueCondition(Tag::idColumn(), Query::In, removedTagsIds); + if (!qb.exec()) { + qCDebug(AKONADISERVER_LOG) << "Failed to execute query: " << itemsQuery.query().lastError(); + return false; + } + + return true; +} + + +/* --- ItemParts ----------------------------------------------------- */ + +bool DataStore::removeItemParts(const PimItem &item, const QSet &parts) +{ + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); + qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id()); + qb.addCondition(PartTypeHelper::conditionFromFqNames(parts)); + + qb.exec(); + Part::List existingParts = qb.result(); + Q_FOREACH (Part part, existingParts) { + if (!PartHelper::remove(&part)) { + return false; + } + } + + mNotificationCollector->itemChanged(item, parts); + return true; +} + +bool DataStore::invalidateItemCache(const PimItem &item) +{ + // find all payload item parts + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); + qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id()); + qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); + qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("PLD")); + qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false); + + if (!qb.exec()) { + return false; + } + + const Part::List parts = qb.result(); + // clear data field + Q_FOREACH (Part part, parts) { + if (!PartHelper::truncate(part)) { + return false; + } + } + + return true; +} + +/* --- Collection ------------------------------------------------------ */ +bool DataStore::appendCollection(Collection &collection) +{ + // no need to check for already existing collection with the same name, + // a unique index on parent + name prevents that in the database + if (!collection.insert()) { + return false; + } + + mNotificationCollector->collectionAdded(collection); + return true; +} + +bool DataStore::cleanupCollection(Collection &collection) +{ + if (!s_hasForeignKeyConstraints) { + return cleanupCollection_slow(collection); + } + + // db will do most of the work for us, we just deal with notifications and external payload parts here + Q_ASSERT(s_hasForeignKeyConstraints); + + // collect item deletion notifications + const PimItem::List items = collection.items(); + const QByteArray resource = collection.resource().name().toLatin1(); + + // generate the notification before actually removing the data + // TODO: we should try to get rid of this, requires client side changes to resources and Monitor though + mNotificationCollector->itemsRemoved(items, collection, resource); + + // remove all external payload parts + QueryBuilder qb(Part::tableName(), QueryBuilder::Select); + qb.addColumn(Part::dataFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, collection.id()); + qb.addValueCondition(Part::externalFullColumnName(), Query::Equals, true); + qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); + if (!qb.exec()) { + return false; + } + + try { + while (qb.query().next()) { + ExternalPartStorage::self()->removePartFile( + ExternalPartStorage::resolveAbsolutePath(qb.query().value(0).toByteArray())); + } + } catch (const PartHelperException &e) { + akDebug() << e.what(); + return false; + } + + // delete the collection itself, referential actions will do the rest + mNotificationCollector->collectionRemoved(collection); + return collection.remove(); +} + +bool DataStore::cleanupCollection_slow(Collection &collection) +{ + Q_ASSERT(!s_hasForeignKeyConstraints); + + // delete the content + const PimItem::List items = collection.items(); + const QByteArray resource = collection.resource().name().toLatin1(); + mNotificationCollector->itemsRemoved(items, collection, resource); + + Q_FOREACH (const PimItem &item, items) { + if (!item.clearFlags()) { // TODO: move out of loop and use only a single query + return false; + } + if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) { // TODO: reduce to single query + return false; + } + + if (!PimItem::remove(PimItem::idColumn(), item.id())) { // TODO: move into single query + return false; + } + + if (!Entity::clearRelation(item.id(), Entity::Right)) { // TODO: move into single query + return false; + } + } + + // delete collection mimetypes + collection.clearMimeTypes(); + Collection::clearPimItems(collection.id()); + + // delete attributes + Q_FOREACH (CollectionAttribute attr, collection.attributes()) { + if (!attr.remove()) { + return false; + } + } + + // delete the collection itself + mNotificationCollector->collectionRemoved(collection); + return collection.remove(); +} + +static bool recursiveSetResourceId(const Collection &collection, qint64 resourceId) +{ + Transaction transaction(DataStore::self()); + + QueryBuilder qb(Collection::tableName(), QueryBuilder::Update); + qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, collection.id()); + qb.setColumnValue(Collection::resourceIdColumn(), resourceId); + qb.setColumnValue(Collection::remoteIdColumn(), QVariant()); + qb.setColumnValue(Collection::remoteRevisionColumn(), QVariant()); + if (!qb.exec()) { + return false; + } + + // this is a cross-resource move, so also reset any resource-specific data (RID, RREV, etc) + // as well as mark the items dirty to prevent cache purging before they have been written back + qb = QueryBuilder(PimItem::tableName(), QueryBuilder::Update); + qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, collection.id()); + qb.setColumnValue(PimItem::remoteIdColumn(), QVariant()); + qb.setColumnValue(PimItem::remoteRevisionColumn(), QVariant()); + const QDateTime now = QDateTime::currentDateTime(); + qb.setColumnValue(PimItem::datetimeColumn(), now); + qb.setColumnValue(PimItem::atimeColumn(), now); + qb.setColumnValue(PimItem::dirtyColumn(), true); + if (!qb.exec()) { + return false; + } + + transaction.commit(); + + Q_FOREACH (const Collection &col, collection.children()) { + if (!recursiveSetResourceId(col, resourceId)) { + return false; + } + } + return true; +} + +bool DataStore::moveCollection(Collection &collection, const Collection &newParent) +{ + if (collection.parentId() == newParent.id()) { + return true; + } + + if (!m_dbOpened || !newParent.isValid()) { + return false; + } + + const QByteArray oldResource = collection.resource().name().toLatin1(); + + int resourceId = collection.resourceId(); + const Collection source = collection.parent(); + if (newParent.id() > 0) { // not root + resourceId = newParent.resourceId(); + } + if (!CollectionQueryHelper::canBeMovedTo(collection, newParent)) { + return false; + } + + collection.setParentId(newParent.id()); + if (collection.resourceId() != resourceId) { + collection.setResourceId(resourceId); + collection.setRemoteId(QString()); + collection.setRemoteRevision(QString()); + if (!recursiveSetResourceId(collection, resourceId)) { + return false; + } + } + + if (!collection.update()) { + return false; + } + + mNotificationCollector->collectionMoved(collection, source, oldResource, newParent.resource().name().toLatin1()); + return true; +} + +bool DataStore::appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes) +{ + if (mimeTypes.isEmpty()) { + return true; + } + SelectQueryBuilder qb; + qb.addValueCondition(MimeType::nameColumn(), Query::In, mimeTypes); + if (!qb.exec()) { + return false; + } + QStringList missingMimeTypes = mimeTypes; + + Q_FOREACH (const MimeType &mt, qb.result()) { + // unique index on n:m relation prevents duplicates, ie. this will fail + // if this mimetype is already set + if (!Collection::addMimeType(collectionId, mt.id())) { + return false; + } + missingMimeTypes.removeAll(mt.name()); + } + + // the MIME type doesn't exist, so we have to add it to the db + Q_FOREACH (const QString &mtName, missingMimeTypes) { + qint64 mimeTypeId; + if (!appendMimeType(mtName, &mimeTypeId)) { + return false; + } + if (!Collection::addMimeType(collectionId, mimeTypeId)) { + return false; + } + } + + return true; +} + +void DataStore::activeCachePolicy(Collection &col) +{ + if (!col.cachePolicyInherit()) { + return; + } + + Collection parent = col; + while (parent.parentId() != 0) { + parent = parent.parent(); + if (!parent.cachePolicyInherit()) { + col.setCachePolicyCheckInterval(parent.cachePolicyCheckInterval()); + col.setCachePolicyCacheTimeout(parent.cachePolicyCacheTimeout()); + col.setCachePolicySyncOnDemand(parent.cachePolicySyncOnDemand()); + col.setCachePolicyLocalParts(parent.cachePolicyLocalParts()); + return; + } + } + + // ### system default + col.setCachePolicyCheckInterval(-1); + col.setCachePolicyCacheTimeout(-1); + col.setCachePolicySyncOnDemand(false); + col.setCachePolicyLocalParts(QStringLiteral("ALL")); +} + +QVector DataStore::virtualCollections(const PimItem &item) +{ + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), + Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName()); + qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, item.id()); + + if (!qb.exec()) { + akDebug() << "Error during selection of records from table CollectionPimItemRelation" + << qb.query().lastError().text(); + return QVector(); + } + + return qb.result(); +} + +QMap > DataStore::virtualCollections(const PimItem::List &items) +{ + QueryBuilder qb(CollectionPimItemRelation::tableName(), QueryBuilder::Select); + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), + Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), + PimItem::idFullColumnName(), CollectionPimItemRelation::rightFullColumnName()); + qb.addColumn(Collection::idFullColumnName()); + qb.addColumns(QStringList() << PimItem::idFullColumnName() + << PimItem::remoteIdFullColumnName() + << PimItem::remoteRevisionFullColumnName() + << PimItem::mimeTypeIdFullColumnName()); + qb.addSortColumn(Collection::idFullColumnName(), Query::Ascending); + + if (items.count() == 1) { + qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, items.first().id()); + } else { + QVariantList ids; + ids.reserve(items.count()); + Q_FOREACH (const PimItem &item, items) { + ids << item.id(); + } + qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::In, ids); + } + + if (!qb.exec()) { + akDebug() << "Error during selection of records from table CollectionPimItemRelation" + << qb.query().lastError().text(); + return QMap >(); + } + + QSqlQuery query = qb.query(); + QMap > map; + query.next(); + while (query.isValid()) { + const qlonglong collectionId = query.value(0).toLongLong(); + QList &pimItems = map[collectionId]; + do { + PimItem item; + item.setId(query.value(1).toLongLong()); + item.setRemoteId(query.value(2).toString()); + item.setRemoteRevision(query.value(3).toString()); + item.setMimeTypeId(query.value(4).toLongLong()); + pimItems << item; + } while (query.next() && query.value(0).toLongLong() == collectionId); + } + + return map; +} + +/* --- MimeType ------------------------------------------------------ */ +bool DataStore::appendMimeType(const QString &mimetype, qint64 *insertId) +{ + if (MimeType::exists(mimetype)) { + akDebug() << "Cannot insert mimetype " << mimetype + << " because it already exists."; + return false; + } + + MimeType mt(mimetype); + return mt.insert(insertId); +} + +/* --- PimItem ------------------------------------------------------- */ +bool DataStore::appendPimItem(QVector &parts, + const MimeType &mimetype, + const Collection &collection, + const QDateTime &dateTime, + const QString &remote_id, + const QString &remoteRevision, + const QString &gid, + PimItem &pimItem) +{ + pimItem.setMimeTypeId(mimetype.id()); + pimItem.setCollectionId(collection.id()); + if (dateTime.isValid()) { + pimItem.setDatetime(dateTime); + } + if (remote_id.isEmpty()) { + // from application + pimItem.setDirty(true); + } else { + // from resource + pimItem.setRemoteId(remote_id); + pimItem.setDirty(false); + } + pimItem.setRemoteRevision(remoteRevision); + pimItem.setGid(gid); + pimItem.setAtime(QDateTime::currentDateTime()); + + if (!pimItem.insert()) { + return false; + } + + // insert every part + if (!parts.isEmpty()) { + //don't use foreach, the caller depends on knowing the part has changed, see the Append handler + for (QVector::iterator it = parts.begin(); it != parts.end(); ++it) { + + (*it).setPimItemId(pimItem.id()); + if ((*it).datasize() < (*it).data().size()) { + (*it).setDatasize((*it).data().size()); + } + +// akDebug() << "Insert from DataStore::appendPimItem"; + if (!PartHelper::insert(&(*it))) { + return false; + } + } + } + +// akDebug() << "appendPimItem: " << pimItem; + + mNotificationCollector->itemAdded(pimItem, collection); + return true; +} + +bool DataStore::unhidePimItem(PimItem &pimItem) +{ + if (!m_dbOpened) { + return false; + } + + akDebug() << "DataStore::unhidePimItem(" << pimItem << ")"; + + // FIXME: This is inefficient. Using a bit on the PimItemTable record would probably be some orders of magnitude faster... + return removeItemParts(pimItem, { AKONADI_ATTRIBUTE_HIDDEN }); +} + +bool DataStore::unhideAllPimItems() +{ + if (!m_dbOpened) { + return false; + } + + akDebug() << "DataStore::unhideAllPimItems()"; + + try { + return PartHelper::remove(Part::partTypeIdFullColumnName(), + PartTypeHelper::fromFqName(QStringLiteral("ATR"), QStringLiteral("HIDDEN")).id()); + } catch (...) { + } // we can live with this failing + + return false; +} + +bool DataStore::cleanupPimItems(const PimItem::List &items) +{ + // generate relation removed notifications + Q_FOREACH (const PimItem &item, items) { + SelectQueryBuilder relationQuery; + relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, item.id()); + relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, item.id()); + relationQuery.setSubQueryMode(Query::Or); + + if (!relationQuery.exec()) { + throw HandlerException("Failed to obtain relations"); + } + const Relation::List relations = relationQuery.result(); + Q_FOREACH (const Relation &relation, relations) { + DataStore::self()->notificationCollector()->relationRemoved(relation); + } + } + + // generate the notification before actually removing the data + mNotificationCollector->itemsRemoved(items); + + // FIXME: Create a single query to do this + Q_FOREACH (const PimItem &item, items) { + if (!item.clearFlags()) { + return false; + } + if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) { + return false; + } + if (!PimItem::remove(PimItem::idColumn(), item.id())) { + return false; + } + + if (!Entity::clearRelation(item.id(), Entity::Right)) { + return false; + } + } + + return true; +} + +bool DataStore::addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value) +{ + SelectQueryBuilder qb; + qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id()); + qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key); + if (!qb.exec()) { + return false; + } + + if (qb.result().count() > 0) { + akDebug() << "Attribute" << key << "already exists for collection" << col.id(); + return false; + } + + CollectionAttribute attr; + attr.setCollectionId(col.id()); + attr.setType(key); + attr.setValue(value); + + if (!attr.insert()) { + return false; + } + + mNotificationCollector->collectionChanged(col, QList() << key); + return true; +} + +bool DataStore::removeCollectionAttribute(const Collection &col, const QByteArray &key) +{ + SelectQueryBuilder qb; + qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id()); + qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key); + if (!qb.exec()) { + throw HandlerException("Unable to query for collection attribute"); + } + + const QVector result = qb.result(); + Q_FOREACH (CollectionAttribute attr, result) { + if (!attr.remove()) { + throw HandlerException("Unable to remove collection attribute"); + } + } + + if (!result.isEmpty()) { + mNotificationCollector->collectionChanged(col, QList() << key); + return true; + } + return false; +} + +void DataStore::debugLastDbError(const char *actionDescription) const +{ + akError() << "Database error:" << actionDescription; + akError() << " Last driver error:" << m_database.lastError().driverText(); + akError() << " Last database error:" << m_database.lastError().databaseText(); + + Tracer::self()->error("DataStore (Database Error)", + QStringLiteral("%1\nDriver said: %2\nDatabase said:%3") + .arg(QString::fromLatin1(actionDescription), + m_database.lastError().driverText(), + m_database.lastError().databaseText())); +} + +void DataStore::debugLastQueryError(const QSqlQuery &query, const char *actionDescription) const +{ + akError() << "Query error:" << actionDescription; + akError() << " Last error message:" << query.lastError().text(); + akError() << " Last driver error:" << m_database.lastError().driverText(); + akError() << " Last database error:" << m_database.lastError().databaseText(); + + Tracer::self()->error("DataStore (Database Query Error)", + QStringLiteral("%1: %2") + .arg(QString::fromLatin1(actionDescription), + query.lastError().text())); +} + +// static +QString DataStore::dateTimeFromQDateTime(const QDateTime &dateTime) +{ + QDateTime utcDateTime = dateTime; + if (utcDateTime.timeSpec() != Qt::UTC) { + utcDateTime.toUTC(); + } + return utcDateTime.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss")); +} + +// static +QDateTime DataStore::dateTimeToQDateTime(const QByteArray &dateTime) +{ + return QDateTime::fromString(QString::fromLatin1(dateTime), QStringLiteral("yyyy-MM-dd hh:mm:ss")); +} + +void DataStore::addQueryToTransaction(const QSqlQuery &query, bool isBatch) +{ + // This is used for replaying deadlocked transactions, so only record queries + // for backends that support concurrent transactions. + if (!inTransaction() || DbType::isSystemSQLite(m_database)) { + return; + } + + m_transactionQueries.append(qMakePair(query, isBatch)); +} + +QSqlQuery DataStore::retryLastTransaction(bool rollbackFirst) +{ + if (!inTransaction() || DbType::isSystemSQLite(m_database)) { + return QSqlQuery(); + } + + if (rollbackFirst) { + // In some cases the SQL database won't rollback the failed transaction, so + // we need to do it manually + m_database.driver()->rollbackTransaction(); + } + + // The database has rolled back the actual transaction, so reset the counter + // to 0 and start a new one in beginTransaction(). Then restore the level + // because this has to be completely transparent to the original caller + const int oldTransactionLevel = m_transactionLevel; + m_transactionLevel = 0; + if (!beginTransaction()) { + m_transactionLevel = oldTransactionLevel; + return QSqlQuery(); + } + m_transactionLevel = oldTransactionLevel; + + QSqlQuery ret; + typedef QPair QueryBoolPair; + QMutableVectorIterator iter(m_transactionQueries); + while (iter.hasNext()) { + iter.next(); + QSqlQuery query = iter.value().first; + const bool isBatch = iter.value().second; + + // Make sure the query is ready to be executed again + if (query.isActive()) { + query.finish(); + } + + bool res = false; + if (isBatch) { + // QSqlQuery::execBatch() does not reset lastError(), so for the sake + // of transparency (make it look to the caller like if the query was + // successful the first time), we create a copy of the original query, + // which has lastError empty. + QSqlQuery copiedQuery(m_database); + copiedQuery.prepare(query.executedQuery()); + const QMap boundValues = query.boundValues(); + int i = 0; + Q_FOREACH (const QVariant &value, boundValues) { + copiedQuery.bindValue(i, value); + ++i; + } + query = copiedQuery; + res = query.execBatch(); + } else { + res = query.exec(); + } + + if (!res) { + // Don't do another deadlock detection here, just give up. + akError() << "DATABASE ERROR:"; + akError() << " Error code:" << query.lastError().number(); + akError() << " DB error: " << query.lastError().databaseText(); + akError() << " Error text:" << query.lastError().text(); + akError() << " Query:" << query.executedQuery(); + + // Return the last query, because that's what caller expects to retrieve + // from QueryBuilder. It is in error state anyway. + return m_transactionQueries.last().first; + } + + // Update the query in the list + iter.setValue(qMakePair(query, isBatch)); + } + + return m_transactionQueries.last().first; +} + +bool DataStore::beginTransaction() +{ + if (!m_dbOpened) { + return false; + } + + if (m_transactionLevel == 0) { + TRANSACTION_MUTEX_LOCK; + if (DbType::type(m_database) == DbType::Sqlite) { + m_database.exec(QStringLiteral("BEGIN IMMEDIATE TRANSACTION")); + if (m_database.lastError().isValid()) { + debugLastDbError("DataStore::beginTransaction (SQLITE)"); + TRANSACTION_MUTEX_UNLOCK; + return false; + } + } else if (!m_database.driver()->beginTransaction()) { + debugLastDbError("DataStore::beginTransaction"); + TRANSACTION_MUTEX_UNLOCK; + return false; + } + } + + ++m_transactionLevel; + + return true; +} + +bool DataStore::rollbackTransaction() +{ + if (!m_dbOpened) { + return false; + } + + if (m_transactionLevel == 0) { + qCWarning(AKONADISERVER_LOG) << "DataStore::rollbackTransaction(): No transaction in progress!"; + return false; + } + + --m_transactionLevel; + + if (m_transactionLevel == 0) { + QSqlDriver *driver = m_database.driver(); + Q_EMIT transactionRolledBack(); + if (!driver->rollbackTransaction()) { + TRANSACTION_MUTEX_UNLOCK; + debugLastDbError("DataStore::rollbackTransaction"); + return false; + } + TRANSACTION_MUTEX_UNLOCK; + + m_transactionQueries.clear(); + } + + return true; +} + +bool DataStore::commitTransaction() +{ + if (!m_dbOpened) { + return false; + } + + if (m_transactionLevel == 0) { + qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): No transaction in progress!"; + return false; + } + + if (m_transactionLevel == 1) { + QSqlDriver *driver = m_database.driver(); + if (!driver->commitTransaction()) { + debugLastDbError("DataStore::commitTransaction"); + rollbackTransaction(); + return false; + } else { + TRANSACTION_MUTEX_UNLOCK; + Q_EMIT transactionCommitted(); + } + + m_transactionQueries.clear(); + } + + m_transactionLevel--; + return true; +} + +bool DataStore::inTransaction() const +{ + return m_transactionLevel > 0; +} + +void DataStore::sendKeepAliveQuery() +{ + if (m_database.isOpen()) { + QSqlQuery query(m_database); + query.exec(QStringLiteral("SELECT 1")); + } +} diff --git a/src/server/storage/datastore.h b/src/server/storage/datastore.h new file mode 100644 index 0000000..5b6c56b --- /dev/null +++ b/src/server/storage/datastore.h @@ -0,0 +1,371 @@ +/*************************************************************************** + * Copyright (C) 2006 by Andreas Gungl * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef DATASTORE_H +#define DATASTORE_H + +#include +#include +#include +#include +#include +#include +#include + +class QSqlQuery; +class QTimer; + +#include "entities.h" +#include "notificationcollector.h" + +namespace Akonadi { +namespace Server { + +class NotificationCollector; + +/** + This class handles all the database access. + +

Database configuration

+ + You can select between various database backends during runtime using the + @c $HOME/.config/akonadi/akonadiserverrc configuration file. + + Example: +@verbatim +[%General] +Driver=QMYSQL + +[QMYSQL_EMBEDDED] +Name=akonadi +Options=SERVER_DATADIR=/home/foo/.local/share/akonadi/db_data + +[QMYSQL] +Name=akonadi +Host=localhost +User=foo +Password=***** +#Options=UNIX_SOCKET=/home/foo/.local/share/akonadi/socket-bar/mysql.socket +StartServer=true +ServerPath=/usr/sbin/mysqld + +[QSQLITE] +Name=/home/foo/.local/share/akonadi/akonadi.db +@endverbatim + + Use @c General/Driver to select the QSql driver to use for database + access. The following drivers are currently supported, other might work + but are untested: + + - QMYSQL + - QMYSQL_EMBEDDED + - QSQLITE + + The options for each driver are read from the corresponding group. + The following options are supported, dependent on the driver not all of them + might have an effect: + + - Name: Database name, for sqlite that's the file name of the database. + - Host: Hostname of the database server + - User: Username for the database server + - Password: Password for the database server + - Options: Additional options, format is driver-dependent + - StartServer: Start the database locally just for Akonadi instead of using an existing one + - ServerPath: Path to the server executable +*/ +class DataStore : public QObject +{ + Q_OBJECT +public: + /** + Closes the database connection and destroys the DataStore object. + */ + virtual ~DataStore(); + + /** + Opens the database connection. + */ + virtual void open(); + + /** + Closes the database connection. + */ + virtual void close(); + + /** + Initializes the database. Should be called during startup by the main thread. + */ + virtual bool init(); + + /** + Per thread singleton. + */ + static DataStore *self(); + + /** + * Returns whether per thread DataStore has been created. + */ + static bool hasDataStore(); + + /* --- ItemFlags ----------------------------------------------------- */ + virtual bool setItemsFlags(const PimItem::List &items, const QVector &flags, + bool *flagsChanged = 0, const Collection &col = Collection(), bool silent = false); + virtual bool appendItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged = 0, + bool checkIfExists = true, const Collection &col = Collection(), bool silent = false); + virtual bool removeItemsFlags(const PimItem::List &items, const QVector &flags, bool *tagsChanged = 0, + const Collection &collection = Collection(), bool silent = false); + + /* --- ItemTags ----------------------------------------------------- */ + virtual bool setItemsTags(const PimItem::List &items, const Tag::List &tags, + bool *tagsChanged = 0, bool silent = false); + virtual bool appendItemsTags(const PimItem::List &items, const Tag::List &tags, + bool *tagsChanged = 0, bool checkIfExists = true, + const Collection &col = Collection(), bool silent = false); + virtual bool removeItemsTags(const PimItem::List &items, const Tag::List &tags, + bool *tagsChanged = 0, bool silent = false); + virtual bool removeTags( const Tag::List &tags, bool silent = false ); + + /* --- ItemParts ----------------------------------------------------- */ + virtual bool removeItemParts(const PimItem &item, const QSet &parts); + + // removes all payload parts for this item. + virtual bool invalidateItemCache(const PimItem &item); + + /* --- Collection ------------------------------------------------------ */ + virtual bool appendCollection(Collection &collection); + + /// removes the given collection and all its content + virtual bool cleanupCollection(Collection &collection); + /// same as the above but for database backends without working referential actions on foreign keys + virtual bool cleanupCollection_slow(Collection &collection); + + /// moves the collection @p collection to @p newParent. + virtual bool moveCollection(Collection &collection, const Collection &newParent); + + virtual bool appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes); + + static QString collectionDelimiter() + { + return QStringLiteral("/"); + } + + /** + Determines the active cache policy for this Collection. + The active cache policy is set in the corresponding Collection fields. + */ + virtual void activeCachePolicy(Collection &col); + + /// Returns all virtual collections the @p item is linked to + QVector virtualCollections(const PimItem &item); + + QMap< Server::Entity::Id, QList< PimItem > > virtualCollections(const Akonadi::Server::PimItem::List &items); + + /* --- MimeType ------------------------------------------------------ */ + virtual bool appendMimeType(const QString &mimetype, qint64 *insertId = 0); + + /* --- PimItem ------------------------------------------------------- */ + virtual bool appendPimItem(QVector &parts, + const MimeType &mimetype, + const Collection &collection, + const QDateTime &dateTime, + const QString &remote_id, + const QString &remoteRevision, + const QString &gid, + PimItem &pimItem); + /** + * Removes the pim item and all referenced data ( e.g. flags ) + */ + virtual bool cleanupPimItems(const PimItem::List &items); + + /** + * Unhides the specified PimItem. Emits the itemAdded() notification as + * the hidden flag is assumed to have been set by appendPimItem() before + * pushing the item to the preprocessor chain. The hidden item had his + * notifications disabled until now (so for the clients the "unhide" operation + * is actually a new item arrival). + * + * This function does NOT verify if the item was *really* hidden: this is + * responsibility of the caller. + */ + virtual bool unhidePimItem(PimItem &pimItem); + + /** + * Unhides all the items which have the "hidden" flag set. + * This function doesn't emit any notification about the items + * being unhidden so it's meant to be called only in rare circumstances. + * The most notable call to this function is at server startup + * when we attempt to restore a clean state of the database. + */ + virtual bool unhideAllPimItems(); + + /* --- Collection attributes ------------------------------------------ */ + virtual bool addCollectionAttribute(const Collection &col, const QByteArray &key, + const QByteArray &value); + /** + * Removes the given collection attribute for @p col. + * @throws HandlerException on database errors + * @returns @c true if the attribute existed, @c false otherwise + */ + virtual bool removeCollectionAttribute(const Collection &col, const QByteArray &key); + + /* --- Helper functions ---------------------------------------------- */ + + /** + Begins a transaction. No changes will be written to the database and + no notification signal will be emitted unless you call commitTransaction(). + @return @c true if successful. + */ + virtual bool beginTransaction(); + + /** + Reverts all changes within the current transaction. + */ + virtual bool rollbackTransaction(); + + /** + Commits all changes within the current transaction and emits all + collected notfication signals. If committing fails, the transaction + will be rolled back. + */ + virtual bool commitTransaction(); + + /** + Returns true if there is a transaction in progress. + */ + virtual bool inTransaction() const; + + /** + Returns the notification collector of this DataStore object. + Use this to listen to change notification signals. + */ + virtual NotificationCollector *notificationCollector(); + + /** + Returns the QSqlDatabase object. Use this for generating queries yourself. + + Will [re-]open the database, if it is closed. + */ + QSqlDatabase database(); + + /** + Sets the current session id. + */ + void setSessionId(const QByteArray &sessionId) + { + mSessionId = sessionId; + } + + /** + Returns if the database is currently open + */ + bool isOpened() const { return m_dbOpened ; } + +Q_SIGNALS: + /** + Emitted if a transaction has been successfully committed. + */ + void transactionCommitted(); + /** + Emitted if a transaction has been aborted. + */ + void transactionRolledBack(); + +protected: + /** + Creates a new DataStore object and opens it. + */ + DataStore(); + + void debugLastDbError(const char *actionDescription) const; + void debugLastQueryError(const QSqlQuery &query, const char *actionDescription) const; + +private: + bool doAppendItemsFlag(const PimItem::List &items, const Flag &flag, + const QSet &existing, const Collection &col, + bool silent); + + bool doAppendItemsTag(const PimItem::List &items, const Tag &tag, + const QSet &existing, const Collection &col, + bool silent); + + /** Converts the given date/time to the database format, i.e. + "YYYY-MM-DD HH:MM:SS". + @param dateTime the date/time in UTC + @return the date/time in database format + @see dateTimeToQDateTime + */ + static QString dateTimeFromQDateTime(const QDateTime &dateTime); + + /** Converts the given date/time from database format to QDateTime. + @param dateTime the date/time in database format + @return the date/time as QDateTime + @see dateTimeFromQDateTime + */ + static QDateTime dateTimeToQDateTime(const QByteArray &dateTime); + + /** + * Adds the @p query to current transaction, so that it can be replayed in + * case the transaction deadlocks or timeouts. + * + * When DataStore is not in transaction or SQLite is configured, this method + * does nothing. + * + * All queries will automatically be removed when transaction is committed. + * + * This method should only be used by QueryBuilder. + */ + void addQueryToTransaction(const QSqlQuery &query, bool isBatch); + + /** + * Tries to execute all queries from last transaction again. If any of the + * queries fails, the entire transaction is rolled back and fails. + * + * This method can only be used by QueryBuilder when database rolls back + * transaction due to deadlock or timeout. + * + * @return Returns an invalid query when error occurs, or the last replayed + * query on success. + */ + QSqlQuery retryLastTransaction(bool rollbackFirst); + +private Q_SLOTS: + void sendKeepAliveQuery(); + +protected: + static QThreadStorage sInstances; + + QString m_connectionName; + QSqlDatabase m_database; + bool m_dbOpened; + uint m_transactionLevel; + QVector > m_transactionQueries; + QByteArray mSessionId; + NotificationCollector *mNotificationCollector; + QTimer *m_keepAliveTimer; + static bool s_hasForeignKeyConstraints; + + // Gives QueryBuilder access to addQueryToTransaction() and retryLastTransaction() + friend class QueryBuilder; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/dbconfig.cpp b/src/server/storage/dbconfig.cpp new file mode 100644 index 0000000..9c027a9 --- /dev/null +++ b/src/server/storage/dbconfig.cpp @@ -0,0 +1,145 @@ +/* + Copyright (c) 2010 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. +*/ + +#include "dbconfig.h" + +#include "dbconfigmysql.h" +#include "dbconfigpostgresql.h" +#include "dbconfigsqlite.h" + +#include +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +//TODO: make me Q_GLOBAL_STATIC +static DbConfig *s_DbConfigInstance = Q_NULLPTR; + +DbConfig::DbConfig() +{ + const QString serverConfigFile = StandardDirs::serverConfigFile(XdgBaseDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + + mSizeThreshold = 4096; + const QVariant value = settings.value(QStringLiteral("General/SizeThreshold"), mSizeThreshold); + if (value.canConvert()) { + mSizeThreshold = value.value(); + } else { + mSizeThreshold = 0; + } + + if (mSizeThreshold < 0) { + mSizeThreshold = 0; + } +} + +DbConfig::~DbConfig() +{ +} + +bool DbConfig::isConfigured() +{ + return s_DbConfigInstance; +} + +DbConfig *DbConfig::configuredDatabase() +{ + if (!s_DbConfigInstance) { + const QString serverConfigFile = StandardDirs::serverConfigFile(XdgBaseDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + + // determine driver to use + QString driverName = settings.value(QStringLiteral("General/Driver")).toString(); + if (driverName.isEmpty()) { + driverName = QStringLiteral(AKONADI_DATABASE_BACKEND); + // when using the default, write it explicitly, in case the default changes later + settings.setValue(QStringLiteral("General/Driver"), driverName); + settings.sync(); + } + + if (driverName == QLatin1String("QMYSQL")) { + s_DbConfigInstance = new DbConfigMysql; + } else if (driverName == QLatin1String("QSQLITE")) { + s_DbConfigInstance = new DbConfigSqlite(DbConfigSqlite::Default); + } else if (driverName == QLatin1String("QSQLITE3")) { + s_DbConfigInstance = new DbConfigSqlite(DbConfigSqlite::Custom); + } else if (driverName == QLatin1String("QPSQL")) { + s_DbConfigInstance = new DbConfigPostgresql; + } else { + akError() << "Unknown database driver: " << driverName; + akError() << "Available drivers are: " << QSqlDatabase::drivers(); + return Q_NULLPTR; + } + + if (!s_DbConfigInstance->init(settings)) { + delete s_DbConfigInstance; + s_DbConfigInstance = Q_NULLPTR; + } + } + + return s_DbConfigInstance; +} + +bool DbConfig::startInternalServer() +{ + // do nothing + return true; +} + +void DbConfig::stopInternalServer() +{ + // do nothing +} + +void DbConfig::setup() +{ + // do nothing +} + +qint64 DbConfig::sizeThreshold() const +{ + return mSizeThreshold; +} + +QString DbConfig::defaultDatabaseName() +{ + if (!Instance::hasIdentifier()) { + return QStringLiteral("akonadi"); + } + // dash is not allowed in PSQL + return QLatin1Literal("akonadi_") % Instance::identifier().replace(QLatin1Char('-'), QLatin1Char('_')); +} + +void DbConfig::initSession(const QSqlDatabase &database) +{ + Q_UNUSED(database); +} + +int DbConfig::execute(const QString &cmd, const QStringList &args) const +{ + akDebug() << "Executing: " << cmd << args.join(QLatin1Char(' ')); + return QProcess::execute(cmd, args); +} diff --git a/src/server/storage/dbconfig.h b/src/server/storage/dbconfig.h new file mode 100644 index 0000000..82f1322 --- /dev/null +++ b/src/server/storage/dbconfig.h @@ -0,0 +1,130 @@ +/* + Copyright (c) 2010 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 DBCONFIG_H +#define DBCONFIG_H + +#include +#include + +namespace Akonadi { +namespace Server { + +/** + * A base class that provides an unique access layer to configuration + * and initialization of different database backends. + */ +class DbConfig +{ +public: + + virtual ~DbConfig(); + + /** + * Returns whether database have been configured. + */ + static bool isConfigured(); + + /** + * Returns the DbConfig instance for the database the user has + * configured. + */ + static DbConfig *configuredDatabase(); + + /** + * Returns the name of the used driver. + */ + virtual QString driverName() const = 0; + + /** + * Returns the database name. + */ + virtual QString databaseName() const = 0; + + /** + * This method is called whenever the Akonadi server is started + * and before the initial database connection is set up. + * + * At this point the default settings should be determined, merged + * with the given @p settings and written back. + */ + virtual bool init(QSettings &settings) = 0; + + /** + * This method applies the configured settings to the QtSql @p database + * instance. + */ + virtual void apply(QSqlDatabase &database) = 0; + + /** + * Do session setup/initialization work on @p database. + * An example would be to run some SQL commands on every new session, + * typically stuff like setting encodings, transaction isolation levels, etc. + */ + virtual void initSession(const QSqlDatabase &database); + + /** + * Returns whether an internal server needs to be used. + */ + virtual bool useInternalServer() const = 0; + + /** + * This method is called to start an external server. + */ + virtual bool startInternalServer(); + + /** + * This method is called to stop the external server. + */ + virtual void stopInternalServer(); + + /** + * Payload data bigger than this value will be stored in separate files, instead of the database. Valid + * + * @return the size threshold in bytes, defaults to 4096. + */ + virtual qint64 sizeThreshold() const; + + /** + * This method is called to setup initial database settings after a connection is established. + */ + virtual void setup(); + +protected: + DbConfig(); + + /** + * Returns the suggested default database name, if none is specified in the configuration already. + * This includes instance namespaces, so usually this is not necessary to use in combination + * with internal databases (in process or using our own server instance). + */ + static QString defaultDatabaseName(); + + /** + * Calls QProcess::execute() and also prints the command and arguments via qCDebug() + */ + int execute(const QString &cmd, const QStringList &args) const; +private: + qint64 mSizeThreshold; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/dbconfigmysql.cpp b/src/server/storage/dbconfigmysql.cpp new file mode 100644 index 0000000..01526eb --- /dev/null +++ b/src/server/storage/dbconfigmysql.cpp @@ -0,0 +1,525 @@ +/* + Copyright (c) 2010 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. +*/ + +#include "dbconfigmysql.h" +#include "utils.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +#define MYSQL_MIN_MAJOR 5 +#define MYSQL_MIN_MINOR 1 + +#define MYSQL_VERSION_CHECK(major, minor, patch) ((major << 16) | (minor << 8) | patch) + +DbConfigMysql::DbConfigMysql() + : mInternalServer(true) + , mDatabaseProcess(0) +{ +} + +QString DbConfigMysql::driverName() const +{ + return QStringLiteral("QMYSQL"); +} + +QString DbConfigMysql::databaseName() const +{ + return mDatabaseName; +} + +bool DbConfigMysql::init(QSettings &settings) +{ + // determine default settings depending on the driver + QString defaultHostName; + QString defaultOptions; + QString defaultServerPath; + QString defaultCleanShutdownCommand; + +#ifndef Q_OS_WIN + const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc"))); +#endif + + const bool defaultInternalServer = true; +#ifdef MYSQLD_EXECUTABLE + if (QFile::exists(QStringLiteral(MYSQLD_EXECUTABLE))) { + defaultServerPath = QStringLiteral(MYSQLD_EXECUTABLE); + } +#endif + const QStringList mysqldSearchPath = QStringList() + << QStringLiteral("/usr/sbin") + << QStringLiteral("/usr/local/sbin") + << QStringLiteral("/usr/local/libexec") + << QStringLiteral("/usr/libexec") + << QStringLiteral("/opt/mysql/libexec") + << QStringLiteral("/opt/local/lib/mysql5/bin") + << QStringLiteral("/opt/mysql/sbin"); + if (defaultServerPath.isEmpty()) { + defaultServerPath = XdgBaseDirs::findExecutableFile(QStringLiteral("mysqld"), mysqldSearchPath); + } + + const QString mysqladminPath = XdgBaseDirs::findExecutableFile(QStringLiteral("mysqladmin"), mysqldSearchPath); + if (!mysqladminPath.isEmpty()) { +#ifndef Q_OS_WIN + defaultCleanShutdownCommand = QStringLiteral("%1 --defaults-file=%2/mysql.conf --socket=%3/mysql.socket shutdown") + .arg(mysqladminPath, StandardDirs::saveDir("data"), socketDirectory); +#else + defaultCleanShutdownCommand = QString::fromLatin1("%1 shutdown --shared-memory").arg(mysqladminPath); +#endif + } + + mMysqlInstallDbPath = XdgBaseDirs::findExecutableFile(QStringLiteral("mysql_install_db"), mysqldSearchPath); + akDebug() << "Found mysql_install_db: " << mMysqlInstallDbPath; + + mMysqlCheckPath = XdgBaseDirs::findExecutableFile(QStringLiteral("mysqlcheck"), mysqldSearchPath); + akDebug() << "Found mysqlcheck: " << mMysqlCheckPath; + + mInternalServer = settings.value(QStringLiteral("QMYSQL/StartServer"), defaultInternalServer).toBool(); +#ifndef Q_OS_WIN + if (mInternalServer) { + defaultOptions = QStringLiteral("UNIX_SOCKET=%1/mysql.socket").arg(socketDirectory); + } +#endif + + // read settings for current driver + settings.beginGroup(driverName()); + mDatabaseName = settings.value(QStringLiteral("Name"), defaultDatabaseName()).toString(); + mHostName = settings.value(QStringLiteral("Host"), defaultHostName).toString(); + mUserName = settings.value(QStringLiteral("User")).toString(); + mPassword = settings.value(QStringLiteral("Password")).toString(); + mConnectionOptions = settings.value(QStringLiteral("Options"), defaultOptions).toString(); + mMysqldPath = settings.value(QStringLiteral("ServerPath"), defaultServerPath).toString(); + mCleanServerShutdownCommand = settings.value(QStringLiteral("CleanServerShutdownCommand"), defaultCleanShutdownCommand).toString(); + settings.endGroup(); + + // verify settings and apply permanent changes (written out below) + if (mInternalServer) { + mConnectionOptions = defaultOptions; + // intentionally not namespaced as we are the only one in this db instance when using internal mode + mDatabaseName = QStringLiteral("akonadi"); + } + if (mInternalServer && (mMysqldPath.isEmpty() || !QFile::exists(mMysqldPath))) { + mMysqldPath = defaultServerPath; + } + + akDebug() << "Using mysqld:" << mMysqldPath; + + // store back the default values + settings.beginGroup(driverName()); + settings.setValue(QStringLiteral("Name"), mDatabaseName); + settings.setValue(QStringLiteral("Host"), mHostName); + settings.setValue(QStringLiteral("Options"), mConnectionOptions); + if (!mMysqldPath.isEmpty()) { + settings.setValue(QStringLiteral("ServerPath"), mMysqldPath); + } + settings.setValue(QStringLiteral("StartServer"), mInternalServer); + settings.endGroup(); + settings.sync(); + + // apply temporary changes to the settings + if (mInternalServer) { + mHostName.clear(); + mUserName.clear(); + mPassword.clear(); + } + + return true; +} + +void DbConfigMysql::apply(QSqlDatabase &database) +{ + if (!mDatabaseName.isEmpty()) { + database.setDatabaseName(mDatabaseName); + } + if (!mHostName.isEmpty()) { + database.setHostName(mHostName); + } + if (!mUserName.isEmpty()) { + database.setUserName(mUserName); + } + if (!mPassword.isEmpty()) { + database.setPassword(mPassword); + } + + database.setConnectOptions(mConnectionOptions); + + // can we check that during init() already? + Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId)); +} + +bool DbConfigMysql::useInternalServer() const +{ + return mInternalServer; +} + +bool DbConfigMysql::startInternalServer() +{ + bool success = true; + + const QString akDir = StandardDirs::saveDir("data"); + const QString dataDir = StandardDirs::saveDir("data", QStringLiteral("db_data")); +#ifndef Q_OS_WIN + const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc"))); +#endif + + // generate config file + const QString globalConfig = XdgBaseDirs::findResourceFile("config", QStringLiteral("akonadi/mysql-global.conf")); + const QString localConfig = XdgBaseDirs::findResourceFile("config", QStringLiteral("akonadi/mysql-local.conf")); + const QString actualConfig = StandardDirs::saveDir("data") + QLatin1String("/mysql.conf"); + if (globalConfig.isEmpty()) { + akError() << "Did not find MySQL server default configuration (mysql-global.conf)"; + return false; + } + +#ifdef Q_OS_LINUX + // It is recommended to disable CoW feature when running on Btrfs to improve + // database performance. Disabling CoW only has effect on empty directory (since + // it affects only new files), so we check whether MySQL has not yet been initialized. + QDir dir(dataDir + QDir::separator() + QLatin1String("mysql")); + if (!dir.exists()) { + if (Utils::getDirectoryFileSystem(dataDir) == QLatin1String("btrfs")) { + Utils::disableCoW(dataDir); + } + } +#endif + + if (mMysqldPath.isEmpty()) { + akError() << "mysqld not found. Please verify your installation"; + return false; + } + + // Get the version of the mysqld server that we'll be using. + // MySQL (but not MariaDB) deprecates and removes command line options in + // patch version releases, so we need to adjust the command line options accordingly + // when running the helper utilities or starting the server + const unsigned int localVersion = parseCommandLineToolsVersion(); + if (localVersion == 0x000000) { + akError() << "Failed to detect mysqld version!"; + } + // TODO: Parse "MariaDB" or "MySQL" from the version string instead of relying + // on the version numbers + const bool isMariaDB = localVersion >= MYSQL_VERSION_CHECK(10, 0, 0); + akDebug().nospace() << "mysqld reports version " << (localVersion >> 16) << "." << ((localVersion >> 8) & 0x0000FF) << "." << (localVersion & 0x0000FF) + << " (" << (isMariaDB ? "MariaDB" : "Oracle MySQL") << ")"; + + + bool confUpdate = false; + QFile actualFile(actualConfig); + // update conf only if either global (or local) is newer than actual + if ((QFileInfo(globalConfig).lastModified() > QFileInfo(actualFile).lastModified()) || + (QFileInfo(localConfig).lastModified() > QFileInfo(actualFile).lastModified())) { + QFile globalFile(globalConfig); + QFile localFile(localConfig); + if (globalFile.open(QFile::ReadOnly) && actualFile.open(QFile::WriteOnly)) { + actualFile.write(globalFile.readAll()); + if (!localConfig.isEmpty()) { + if (localFile.open(QFile::ReadOnly)) { + actualFile.write(localFile.readAll()); + localFile.close(); + } + } + globalFile.close(); + actualFile.close(); + confUpdate = true; + } else { + akError() << "Unable to create MySQL server configuration file."; + akError() << "This means that either the default configuration file (mysql-global.conf) was not readable"; + akError() << "or the target file (mysql.conf) could not be written."; + return false; + } + } + + // MySQL doesn't like world writeable config files (which makes sense), but + // our config file somehow ends up being world-writable on some systems for no + // apparent reason nevertheless, so fix that + const QFile::Permissions allowedPerms = actualFile.permissions() + & (QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther); + if (allowedPerms != actualFile.permissions()) { + actualFile.setPermissions(allowedPerms); + } + + if (dataDir.isEmpty()) { + akError() << "Akonadi server was not able to create database data directory"; + return false; + } + + if (akDir.isEmpty()) { + akError() << "Akonadi server was not able to create database log directory"; + return false; + } + +#ifndef Q_OS_WIN + if (socketDirectory.isEmpty()) { + akError() << "Akonadi server was not able to create database misc directory"; + return false; + } + + // the socket path must not exceed 103 characters, so check for max dir length right away + if (socketDirectory.length() >= 90) { + akError() << "MySQL cannot deal with a socket path this long. Path was: " << socketDirectory; + return false; + } +#endif + + // move mysql error log file out of the way + const QFileInfo errorLog(dataDir + QDir::separator() + QLatin1String("mysql.err")); + if (errorLog.exists()) { + QFile logFile(errorLog.absoluteFilePath()); + QFile oldLogFile(dataDir + QDir::separator() + QLatin1String("mysql.err.old")); + if (logFile.open(QFile::ReadOnly) && oldLogFile.open(QFile::Append)) { + oldLogFile.write(logFile.readAll()); + oldLogFile.close(); + logFile.close(); + logFile.remove(); + } else { + akError() << "Failed to open MySQL error log."; + } + } + + // first run, some MySQL versions need a mysql_install_db run for that + const QString confFile = XdgBaseDirs::findResourceFile("config", QStringLiteral("akonadi/mysql-global.conf")); + if (QDir(dataDir).entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty() && !mMysqlInstallDbPath.isEmpty()) { + if (isMariaDB) { + initializeMariaDBDatabase(confFile, dataDir); + } else if (localVersion >= MYSQL_VERSION_CHECK(5, 7, 6)) { + initializeMySQL5_7_6Database(confFile, dataDir); + } else { + initializeMySQLDatabase(confFile, dataDir); + } + } + + // clear mysql ib_logfile's in case innodb_log_file_size option changed in last confUpdate + if (confUpdate) { + QFile(dataDir + QDir::separator() + QLatin1String("ib_logfile0")).remove(); + QFile(dataDir + QDir::separator() + QLatin1String("ib_logfile1")).remove(); + } + + // synthesize the mysqld command + QStringList arguments; + arguments << QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir); + arguments << QStringLiteral("--datadir=%1/").arg(dataDir); +#ifndef Q_OS_WIN + arguments << QStringLiteral("--socket=%1/mysql.socket").arg(socketDirectory); +#else + arguments << QString::fromLatin1("--shared-memory"); +#endif + + akDebug() << "Executing:" << mMysqldPath << arguments.join(QLatin1Char(' ')); + mDatabaseProcess = new QProcess; + mDatabaseProcess->start(mMysqldPath, arguments); + if (!mDatabaseProcess->waitForStarted()) { + akError() << "Could not start database server!"; + akError() << "executable:" << mMysqldPath; + akError() << "arguments:" << arguments; + akError() << "process error:" << mDatabaseProcess->errorString(); + return false; + } + +#ifndef Q_OS_WIN + // wait until mysqld has created the socket file (workaround for QTBUG-47475 in Qt5.5.0) + QString socketFile = QStringLiteral("%1/mysql.socket").arg(socketDirectory); + int counter = 50; // avoid an endless loop in case mysqld terminated + while ((counter-- > 0) && !QFileInfo::exists(socketFile)) { + QThread::msleep(100); + } +#endif + + const QLatin1String initCon("initConnection"); + { + QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QMYSQL"), initCon); + apply(db); + + db.setDatabaseName(QString()); // might not exist yet, then connecting to the actual db will fail + if (!db.isValid()) { + akError() << "Invalid database object during database server startup"; + return false; + } + + bool opened = false; + for (int i = 0; i < 120; ++i) { + opened = db.open(); + if (opened) { + break; + } + if (mDatabaseProcess->waitForFinished(500)) { + akError() << "Database process exited unexpectedly during initial connection!"; + akError() << "executable:" << mMysqldPath; + akError() << "arguments:" << arguments; + akError() << "stdout:" << mDatabaseProcess->readAllStandardOutput(); + akError() << "stderr:" << mDatabaseProcess->readAllStandardError(); + akError() << "exit code:" << mDatabaseProcess->exitCode(); + akError() << "process error:" << mDatabaseProcess->errorString(); + return false; + } + } + + if (opened) { + if (!mMysqlCheckPath.isEmpty()) { + execute(mMysqlCheckPath, { QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir), + QStringLiteral("--check-upgrade"), + QStringLiteral("--auto-repair"), +#ifndef Q_OS_WIN + QStringLiteral("--socket=%1/mysql.socket").arg(socketDirectory), +#endif + mDatabaseName + }); + } + + // Verify MySQL version + { + QSqlQuery query(db); + if (!query.exec(QStringLiteral("SELECT VERSION()")) || !query.first()) { + akError() << "Failed to verify database server version"; + akError() << "Query error:" << query.lastError().text(); + akError() << "Database error:" << db.lastError().text(); + return false; + } + + const QString version = query.value(0).toString(); + const QStringList versions = version.split(QLatin1Char('.'), QString::SkipEmptyParts); + if (versions.count() < 3) { + akError() << "Invalid database server version: " << version; + return false; + } + + if (versions[0].toInt() < MYSQL_MIN_MAJOR || (versions[0].toInt() == MYSQL_MIN_MAJOR && versions[1].toInt() < MYSQL_MIN_MINOR)) { + akError() << "Unsupported MySQL version:"; + akError() << "Current version:" << QStringLiteral("%1.%2").arg(versions[0], versions[1]); + akError() << "Minimum required version:" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR); + akError() << "Please update your MySQL database server"; + return false; + } else { + akDebug() << "MySQL version OK" + << "(required" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR) + << ", available" << QStringLiteral("%1.%2").arg(versions[0], versions[1]) << ")"; + } + } + + { + QSqlQuery query(db); + if (!query.exec(QStringLiteral("USE %1").arg(mDatabaseName))) { + akDebug() << "Failed to use database" << mDatabaseName; + akDebug() << "Query error:" << query.lastError().text(); + akDebug() << "Database error:" << db.lastError().text(); + akDebug() << "Trying to create database now..."; + if (!query.exec(QStringLiteral("CREATE DATABASE akonadi"))) { + akError() << "Failed to create database"; + akError() << "Query error:" << query.lastError().text(); + akError() << "Database error:" << db.lastError().text(); + success = false; + } + } + } // make sure query is destroyed before we close the db + db.close(); + } else { + akError() << "Failed to connect to database!"; + akError() << "Database error:" << db.lastError().text(); + success = false; + } + } + + QSqlDatabase::removeDatabase(initCon); + return success; +} + +void DbConfigMysql::stopInternalServer() +{ + if (!mDatabaseProcess) { + return; + } + + // first, try the nicest approach + if (!mCleanServerShutdownCommand.isEmpty()) { + QProcess::execute(mCleanServerShutdownCommand); + if (mDatabaseProcess->waitForFinished(3000)) { + return; + } + } + + mDatabaseProcess->terminate(); + const bool result = mDatabaseProcess->waitForFinished(3000); + // We've waited nicely for 3 seconds, to no avail, let's be rude. + if (!result) { + mDatabaseProcess->kill(); + } +} + +void DbConfigMysql::initSession(const QSqlDatabase &database) +{ + QSqlQuery query(database); + query.exec(QStringLiteral("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")); +} + +int DbConfigMysql::parseCommandLineToolsVersion() const +{ + QProcess mysqldProcess; + mysqldProcess.start(mMysqldPath, { QStringLiteral("--version") }); + mysqldProcess.waitForFinished(10000 /* 10 secs */); + + const QString out = QString::fromLocal8Bit(mysqldProcess.readAllStandardOutput()); + QRegularExpression regexp(QStringLiteral("Ver ([0-9]+)\\.([0-9]+)\\.([0-9]+)")); + auto match = regexp.match(out); + if (!match.hasMatch()) { + return 0; + } + + return (match.capturedRef(1).toInt() << 16) | (match.capturedRef(2).toInt() << 8) | match.capturedRef(3).toInt(); +} + +bool DbConfigMysql::initializeMariaDBDatabase(const QString &confFile, const QString &dataDir) const +{ + return 0 == execute(mMysqlInstallDbPath, + { QStringLiteral("--defaults-file=%1").arg(confFile), + QStringLiteral("--force"), + QStringLiteral("--datadir=%1/").arg(dataDir) }); +} + +/** + * As of MySQL 5.7.6 mysql_install_db is deprecated and mysqld --initailize should be used instead + * See MySQL Reference Manual section 2.10.1.1 (Initializing the Data Directory Manually Using mysqld) + */ +bool DbConfigMysql::initializeMySQL5_7_6Database(const QString &confFile, const QString &dataDir) const +{ + return 0 == execute(mMysqldPath, + { QStringLiteral("--defaults-file=%1").arg(confFile), + QStringLiteral("--initialize"), + QStringLiteral("--datadir=%1/").arg(dataDir) }); +} + +bool DbConfigMysql::initializeMySQLDatabase(const QString &confFile, const QString &dataDir) const +{ + // Don't use --force, it has been removed in MySQL 5.7.5 + return 0 == execute(mMysqlInstallDbPath, + { QStringLiteral("--defaults-file=%1").arg(confFile), + QStringLiteral("--datadir=%1/").arg(dataDir) }); +} diff --git a/src/server/storage/dbconfigmysql.h b/src/server/storage/dbconfigmysql.h new file mode 100644 index 0000000..b2d9591 --- /dev/null +++ b/src/server/storage/dbconfigmysql.h @@ -0,0 +1,101 @@ +/* + Copyright (c) 2010 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 DBCONFIGMYSQL_H +#define DBCONFIGMYSQL_H + +#include "dbconfig.h" + +class QProcess; + +namespace Akonadi { +namespace Server { + +class DbConfigMysql : public DbConfig +{ +public: + DbConfigMysql(); + + /** + * Returns the name of the used driver. + */ + virtual QString driverName() const; + + /** + * Returns the database name. + */ + virtual QString databaseName() const; + + /** + * This method is called whenever the Akonadi server is started + * and before the initial database connection is set up. + * + * At this point the default settings should be determined, merged + * with the given @p settings and written back. + */ + virtual bool init(QSettings &settings); + + /** + * This method applies the configured settings to the QtSql @p database + * instance. + */ + virtual void apply(QSqlDatabase &database); + + /** + * Returns whether an internal server needs to be used. + */ + virtual bool useInternalServer() const; + + /** + * This method is called to start an external server. + */ + virtual bool startInternalServer(); + + /** + * This method is called to stop the external server. + */ + virtual void stopInternalServer(); + + /// reimpl + virtual void initSession(const QSqlDatabase &database); + +private: + int parseCommandLineToolsVersion() const; + + bool initializeMariaDBDatabase(const QString &confFile, const QString &dataDir) const; + bool initializeMySQL5_7_6Database(const QString &confFile, const QString &dataDir) const; + bool initializeMySQLDatabase(const QString &confFile, const QString &dataDir) const; + + QString mDatabaseName; + QString mHostName; + QString mUserName; + QString mPassword; + QString mConnectionOptions; + QString mMysqldPath; + QString mCleanServerShutdownCommand; + QString mMysqlInstallDbPath; + QString mMysqlCheckPath; + bool mInternalServer; + QProcess *mDatabaseProcess; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/dbconfigpostgresql.cpp b/src/server/storage/dbconfigpostgresql.cpp new file mode 100644 index 0000000..a3ef03e --- /dev/null +++ b/src/server/storage/dbconfigpostgresql.cpp @@ -0,0 +1,385 @@ +/* + Copyright (c) 2010 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. +*/ + +#include "dbconfigpostgresql.h" +#include "utils.h" +#include "akonadiserver_debug.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#ifdef HAVE_UNISTD_H +#include +#endif + +using namespace Akonadi::Server; + +DbConfigPostgresql::DbConfigPostgresql() + : mHostPort(0) + , mInternalServer(true) +{ +} + +QString DbConfigPostgresql::driverName() const +{ + return QStringLiteral("QPSQL"); +} + +QString DbConfigPostgresql::databaseName() const +{ + return mDatabaseName; +} + +bool DbConfigPostgresql::init(QSettings &settings) +{ + // determine default settings depending on the driver + QString defaultHostName; + QString defaultOptions; + QString defaultServerPath; + QString defaultInitDbPath; + QString defaultPgData; + +#ifndef Q_WS_WIN // We assume that PostgreSQL is running as service on Windows + const bool defaultInternalServer = true; +#else + const bool defaultInternalServer = false; +#endif + + mInternalServer = settings.value(QStringLiteral("QPSQL/StartServer"), defaultInternalServer).toBool(); + if (mInternalServer) { + QStringList postgresSearchPath; + +#ifdef POSTGRES_PATH + const QString dir(QStringLiteral(POSTGRES_PATH)); + if (QDir(dir).exists()) { + postgresSearchPath << QStringLiteral(POSTGRES_PATH); + } +#endif + postgresSearchPath << QStringLiteral("/usr/sbin") + << QStringLiteral("/usr/local/sbin"); + // Locale all versions in /usr/lib/postgresql (i.e. /usr/lib/postgresql/X.Y) in reversed + // sorted order, so we search from the newest one to the oldest. + QStringList postgresVersionedSearchPaths; + QDir versionedDir(QStringLiteral("/usr/lib/postgresql")); + if (versionedDir.exists()) { + const auto versionedDirs = versionedDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name | QDir::Reversed); + Q_FOREACH (const auto &path, versionedDirs) { + // Don't break once PostgreSQL 10 is released, but something more future-proof will be needed + if (path.fileName().startsWith(QLatin1String("10."))) { + postgresVersionedSearchPaths.prepend(path.absoluteFilePath() + QStringLiteral("/bin")); + } else { + postgresVersionedSearchPaths.append(path.absoluteFilePath() + QStringLiteral("/bin")); + } + } + } + postgresSearchPath.append(postgresVersionedSearchPaths); + + defaultServerPath = XdgBaseDirs::findExecutableFile(QStringLiteral("pg_ctl"), postgresSearchPath); + defaultInitDbPath = XdgBaseDirs::findExecutableFile(QStringLiteral("initdb"), postgresSearchPath); + defaultHostName = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc"))); + defaultPgData = StandardDirs::saveDir("data", QStringLiteral("db_data")); + } + + // read settings for current driver + settings.beginGroup(driverName()); + mDatabaseName = settings.value(QStringLiteral("Name"), defaultDatabaseName()).toString(); + if (mDatabaseName.isEmpty()) { + mDatabaseName = defaultDatabaseName(); + } + mHostName = settings.value(QStringLiteral("Host"), defaultHostName).toString(); + if (mHostName.isEmpty()) { + mHostName = defaultHostName; + } + mHostPort = settings.value(QStringLiteral("Port")).toInt(); + // User, password and Options can be empty and still valid, so don't override them + mUserName = settings.value(QStringLiteral("User")).toString(); + mPassword = settings.value(QStringLiteral("Password")).toString(); + mConnectionOptions = settings.value(QStringLiteral("Options"), defaultOptions).toString(); + mServerPath = settings.value(QStringLiteral("ServerPath"), defaultServerPath).toString(); + if (mInternalServer && mServerPath.isEmpty()) { + mServerPath = defaultServerPath; + } + akDebug() << "Found pg_ctl:" << mServerPath; + mInitDbPath = settings.value(QStringLiteral("InitDbPath"), defaultInitDbPath).toString(); + if (mInternalServer && mInitDbPath.isEmpty()) { + mInitDbPath = defaultInitDbPath; + } + akDebug() << "Found initdb:" << mServerPath; + mPgData = settings.value(QStringLiteral("PgData"), defaultPgData).toString(); + if (mPgData.isEmpty()) { + mPgData = defaultPgData; + } + settings.endGroup(); + + // store back the default values + settings.beginGroup(driverName()); + settings.setValue(QStringLiteral("Name"), mDatabaseName); + settings.setValue(QStringLiteral("Host"), mHostName); + if (mHostPort) { + settings.setValue(QStringLiteral("Port"), mHostPort); + } + settings.setValue(QStringLiteral("Options"), mConnectionOptions); + settings.setValue(QStringLiteral("ServerPath"), mServerPath); + settings.setValue(QStringLiteral("InitDbPath"), mInitDbPath); + settings.setValue(QStringLiteral("StartServer"), mInternalServer); + settings.endGroup(); + settings.sync(); + + return true; +} + +void DbConfigPostgresql::apply(QSqlDatabase &database) +{ + if (!mDatabaseName.isEmpty()) { + database.setDatabaseName(mDatabaseName); + } + if (!mHostName.isEmpty()) { + database.setHostName(mHostName); + } + if (mHostPort > 0 && mHostPort < 65535) { + database.setPort(mHostPort); + } + if (!mUserName.isEmpty()) { + database.setUserName(mUserName); + } + if (!mPassword.isEmpty()) { + database.setPassword(mPassword); + } + + database.setConnectOptions(mConnectionOptions); + + // can we check that during init() already? + Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId)); +} + +bool DbConfigPostgresql::useInternalServer() const +{ + return mInternalServer; +} + +bool DbConfigPostgresql::startInternalServer() +{ + // We defined the mHostName to the socket directory, during init + const QString socketDir = mHostName; + bool success = true; + + // Make sure the path exists, otherwise pg_ctl fails + if (!QFile::exists(socketDir)) { + QDir().mkpath(socketDir); + } + +// TODO Windows support +#ifndef Q_WS_WIN + // If postmaster.pid exists, check whether the postgres process still exists too, + // because normally we shouldn't be able to get this far if Akonadi is already + // running. If postgres is not running, then the pidfile was left after a system + // crash or something similar and we can remove it (otherwise pg_ctl won't start) + QFile postmaster(QStringLiteral("%1/postmaster.pid").arg(mPgData)); + if (postmaster.exists() && postmaster.open(QIODevice::ReadOnly)) { + qCDebug(AKONADISERVER_LOG) << "Found a postmaster.pid pidfile, checking whether the server is still running..."; + QByteArray pid = postmaster.readLine(); + // Remvoe newline character + pid.truncate(pid.size() - 1); + QFile proc(QString::fromLatin1("/proc/" + pid + "/stat")); + // Check whether the process with the PID from pidfile still exists and whether + // it's actually still postgres or, whether the PID has been recycled in the + // meanwhile. + if (proc.open(QIODevice::ReadOnly)) { + const QByteArray stat = proc.readAll(); + const QList stats = stat.split(' '); + if (stats.count() > 1) { + // Make sure the PID actually belongs to postgres process + if (stats[1] == "(postgres)") { + // Yup, our PostgreSQL is actually running, so pretend we started the server + // and try to connect to it + qCWarning(AKONADISERVER_LOG) << "PostgreSQL for Akonadi is already running, trying to connect to it."; + return true; + } + } + proc.close(); + } + + qCDebug(AKONADISERVER_LOG) << "No postgres process with specified PID is running. Removing the pidfile and starting a new Postgres instance..."; + postmaster.close(); + postmaster.remove(); + } +#endif + + // postgres data directory not initialized yet, so call initdb on it + if (!QFile::exists(QStringLiteral("%1/PG_VERSION").arg(mPgData))) { +#ifdef Q_OS_LINUX + // It is recommended to disable CoW feature when running on Btrfs to improve + // database performance. This only has effect when done on empty directory, + // so we only call this before calling initdb + if (Utils::getDirectoryFileSystem(mPgData) == QLatin1String("btrfs")) { + Utils::disableCoW(mPgData); + } +#endif + + // call 'initdb --pgdata=/home/user/.local/share/akonadi/data_db' + execute(mInitDbPath, { QStringLiteral("--pgdata=%1").arg(mPgData), + QStringLiteral("--locale=en_US.UTF-8") // TODO: check locale + }); + } + + // synthesize the postgres command + QStringList arguments; + arguments << QStringLiteral("start") + << QStringLiteral("-w") + << QStringLiteral("--timeout=10") // default is 60 seconds. + << QStringLiteral("--pgdata=%1").arg(mPgData) + // These options are passed to postgres + // -k - directory for unix domain socket communication + // -h - disable listening for TCP/IP + << QStringLiteral("-o \"-k%1\" -h ''").arg(socketDir); + + akDebug() << "Executing:" << mServerPath << arguments.join(QLatin1Char(' ')); + QProcess pgCtl; + pgCtl.start(mServerPath, arguments); + if (!pgCtl.waitForStarted()) { + akError() << "Could not start database server!"; + akError() << "executable:" << mServerPath; + akError() << "arguments:" << arguments; + akError() << "process error:" << pgCtl.errorString(); + return false; + } + + const QLatin1String initCon("initConnection"); + { + QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL"), initCon); + apply(db); + + // use the default database that is always available + db.setDatabaseName(QStringLiteral("postgres")); + + if (!db.isValid()) { + akError() << "Invalid database object during database server startup"; + return false; + } + + bool opened = false; + for (int i = 0; i < 120; ++i) { + opened = db.open(); + if (opened) { + break; + } + + if (pgCtl.waitForFinished(500) && pgCtl.exitCode()) { + akError() << "Database process exited unexpectedly during initial connection!"; + akError() << "executable:" << mServerPath; + akError() << "arguments:" << arguments; + akError() << "stdout:" << pgCtl.readAllStandardOutput(); + akError() << "stderr:" << pgCtl.readAllStandardError(); + akError() << "exit code:" << pgCtl.exitCode(); + akError() << "process error:" << pgCtl.errorString(); + return false; + } + } + + if (opened) { + { + QSqlQuery query(db); + + // check if the 'akonadi' database already exists + query.exec(QStringLiteral("SELECT 1 FROM pg_catalog.pg_database WHERE datname = '%1'").arg(mDatabaseName)); + + // if not, create it + if (!query.first()) { + if (!query.exec(QStringLiteral("CREATE DATABASE %1").arg(mDatabaseName))) { + akError() << "Failed to create database"; + akError() << "Query error:" << query.lastError().text(); + akError() << "Database error:" << db.lastError().text(); + success = false; + } + } + } // make sure query is destroyed before we close the db + db.close(); + } + } + // Make sure pg_ctl has returned + pgCtl.waitForFinished(); + + QSqlDatabase::removeDatabase(initCon); + return success; +} + +void DbConfigPostgresql::stopInternalServer() +{ + if (!checkServerIsRunning()) { + akDebug() << "Database is no longer running"; + return; + } + + // first, try a FAST shutdown + execute(mServerPath, { QStringLiteral("stop"), + QStringLiteral("--pgdata=%1").arg(mPgData), + QStringLiteral("--mode=fast") }); + if (!checkServerIsRunning()) { + return; + } + + // second, try an IMMEDIATE shutdown + execute(mServerPath, { QStringLiteral("stop"), + QStringLiteral("--pgdata=%1").arg(mPgData), + QStringLiteral("--mode=immediate") }); + if (!checkServerIsRunning()) { + return; + } + + // third, pg_ctl couldn't terminate all the postgres processes, we have to + // kill the master one. We don't want to do that, but we've passed the last + // call. pg_ctl is used to send the kill signal (safe when kill is not + // supported by OS) + const QString pidFileName = QStringLiteral("%1/postmaster.pid").arg(mPgData); + QFile pidFile(pidFileName); + if (pidFile.open(QIODevice::ReadOnly)) { + QString postmasterPid = QString::fromUtf8(pidFile.readLine(0).trimmed()); + akError() << "The postmaster is still running. Killing it."; + + execute(mServerPath, { QStringLiteral("kill"), + QStringLiteral("ABRT"), + postmasterPid }); + } +} + +bool DbConfigPostgresql::checkServerIsRunning() +{ + const QString command = QStringLiteral("%1").arg(mServerPath); + QStringList arguments; + arguments << QStringLiteral("status") + << QStringLiteral("--pgdata=%1").arg(mPgData); + + QProcess pgCtl; + pgCtl.start(command, arguments, QIODevice::ReadOnly); + if (!pgCtl.waitForFinished(3000)) { + // Error? + return false; + } + + const QByteArray output = pgCtl.readAllStandardOutput(); + return output.startsWith("pg_ctl: server is running"); +} diff --git a/src/server/storage/dbconfigpostgresql.h b/src/server/storage/dbconfigpostgresql.h new file mode 100644 index 0000000..53e8e20 --- /dev/null +++ b/src/server/storage/dbconfigpostgresql.h @@ -0,0 +1,93 @@ +/* + Copyright (c) 2010 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 DBCONFIGPOSTGRESQL_H +#define DBCONFIGPOSTGRESQL_H + +#include "dbconfig.h" + +class QProcess; + +namespace Akonadi { +namespace Server { + +class DbConfigPostgresql : public DbConfig +{ +public: + DbConfigPostgresql(); + + /** + * Returns the name of the used driver. + */ + virtual QString driverName() const; + + /** + * Returns the database name. + */ + virtual QString databaseName() const; + + /** + * This method is called whenever the Akonadi server is started + * and before the initial database connection is set up. + * + * At this point the default settings should be determined, merged + * with the given @p settings and written back. + */ + virtual bool init(QSettings &settings); + + /** + * This method applies the configured settings to the QtSql @p database + * instance. + */ + virtual void apply(QSqlDatabase &database); + + /** + * Returns whether an internal server needs to be used. + */ + virtual bool useInternalServer() const; + + /** + * This method is called to start an external server. + */ + virtual bool startInternalServer(); + + /** + * This method is called to stop the external server. + */ + virtual void stopInternalServer(); + +private: + bool checkServerIsRunning(); + + QString mDatabaseName; + QString mHostName; + int mHostPort; + QString mUserName; + QString mPassword; + QString mConnectionOptions; + QString mServerPath; + QString mInitDbPath; + QString mPgData; + bool mInternalServer; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/dbconfigsqlite.cpp b/src/server/storage/dbconfigsqlite.cpp new file mode 100644 index 0000000..768e4fa --- /dev/null +++ b/src/server/storage/dbconfigsqlite.cpp @@ -0,0 +1,256 @@ +/* + Copyright (c) 2010 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. +*/ + +#include "dbconfigsqlite.h" +#include "utils.h" + +#include +#include +#include + +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +static QString dataDir() +{ + QString akonadiHomeDir = StandardDirs::saveDir("data"); + if (akonadiHomeDir.isEmpty()) { + akError() << "Unable to create directory 'akonadi' in " << XdgBaseDirs::homePath("data") + << "during database initialization"; + return QString(); + } + + akonadiHomeDir += QDir::separator(); + + return akonadiHomeDir; +} + +static QString sqliteDataFile() +{ + const QString dir = dataDir(); + if (dir.isEmpty()) { + return QString(); + } + const QString akonadiPath = dir + QLatin1String("akonadi.db"); + if (!QFile::exists(akonadiPath)) { + QFile file(akonadiPath); + if (!file.open(QIODevice::WriteOnly)) { + akError() << "Unable to create file" << akonadiPath << "during database initialization."; + return QString(); + } + file.close(); + } + + return akonadiPath; +} + +DbConfigSqlite::DbConfigSqlite(Version driverVersion) + : mDriverVersion(driverVersion) +{ +} + +QString DbConfigSqlite::driverName() const +{ + if (mDriverVersion == Default) { + return QStringLiteral("QSQLITE"); + } else { + return QStringLiteral("QSQLITE3"); + } +} + +QString DbConfigSqlite::databaseName() const +{ + return mDatabaseName; +} + +bool DbConfigSqlite::init(QSettings &settings) +{ + // determine default settings depending on the driver + const QString defaultDbName = sqliteDataFile(); + if (defaultDbName.isEmpty()) { + return false; + } + + // read settings for current driver + settings.beginGroup(driverName()); + mDatabaseName = settings.value(QStringLiteral("Name"), defaultDbName).toString(); + mHostName = settings.value(QStringLiteral("Host")).toString(); + mUserName = settings.value(QStringLiteral("User")).toString(); + mPassword = settings.value(QStringLiteral("Password")).toString(); + mConnectionOptions = settings.value(QStringLiteral("Options")).toString(); + settings.endGroup(); + + // store back the default values + settings.beginGroup(driverName()); + settings.setValue(QStringLiteral("Name"), mDatabaseName); + settings.endGroup(); + settings.sync(); + + return true; +} + +void DbConfigSqlite::apply(QSqlDatabase &database) +{ + if (!mDatabaseName.isEmpty()) { + database.setDatabaseName(mDatabaseName); + } + if (!mHostName.isEmpty()) { + database.setHostName(mHostName); + } + if (!mUserName.isEmpty()) { + database.setUserName(mUserName); + } + if (!mPassword.isEmpty()) { + database.setPassword(mPassword); + } + + if (driverName() == QLatin1String("QSQLITE3") && !mConnectionOptions.contains(QLatin1String("SQLITE_ENABLE_SHARED_CACHE"))) { + mConnectionOptions += QLatin1String(";QSQLITE_ENABLE_SHARED_CACHE"); + } + database.setConnectOptions(mConnectionOptions); + + // can we check that during init() already? + Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId)); +} + +bool DbConfigSqlite::useInternalServer() const +{ + return false; +} + +void DbConfigSqlite::setup() +{ + const QLatin1String connectionName("initConnection"); + + { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName(), connectionName); + + if (!db.isValid()) { + akDebug() << "Invalid database for " + << mDatabaseName + << " with driver " + << driverName(); + return; + } + + QFileInfo finfo(mDatabaseName); + if (!finfo.dir().exists()) { + QDir dir; + dir.mkpath(finfo.path()); + } + + #ifdef Q_OS_LINUX + QFile dbFile(mDatabaseName); + // It is recommended to disable CoW feature when running on Btrfs to improve + // database performance. It does not have any effect on non-empty files, so + // we check, whether the database has not yet been initialized. + if (dbFile.size() == 0) { + if (Utils::getDirectoryFileSystem(mDatabaseName) == QLatin1String("btrfs")) { + Utils::disableCoW(mDatabaseName); + } + } + #endif + + db.setDatabaseName(mDatabaseName); + if (!db.open()) { + akDebug() << "Could not open sqlite database " + << mDatabaseName + << " with driver " + << driverName() + << " for initialization"; + db.close(); + return; + } + + apply(db); + + QSqlQuery query(db); + if (!query.exec(QStringLiteral("SELECT sqlite_version()"))) { + akDebug() << "Could not query sqlite version"; + akDebug() << "Database: " << mDatabaseName; + akDebug() << "Query error: " << query.lastError().text(); + akDebug() << "Database error: " << db.lastError().text(); + db.close(); + return; + } + + if (!query.next()) { // should never occur + akDebug() << "Could not query sqlite version"; + akDebug() << "Database: " << mDatabaseName; + akDebug() << "Query error: " << query.lastError().text(); + akDebug() << "Database error: " << db.lastError().text(); + db.close(); + return; + } + + const QString sqliteVersion = query.value(0).toString(); + akDebug() << "sqlite version is " << sqliteVersion; + + const QStringList list = sqliteVersion.split(QLatin1Char('.')); + const int sqliteVersionMajor = list[0].toInt(); + const int sqliteVersionMinor = list[1].toInt(); + + // set synchronous mode to NORMAL; see http://www.sqlite.org/pragma.html#pragma_synchronous + if (!query.exec(QStringLiteral("PRAGMA synchronous = 1"))) { + akDebug() << "Could not set sqlite synchronous mode to NORMAL"; + akDebug() << "Database: " << mDatabaseName; + akDebug() << "Query error: " << query.lastError().text(); + akDebug() << "Database error: " << db.lastError().text(); + db.close(); + return; + } + + if (sqliteVersionMajor < 3 && sqliteVersionMinor < 7) { + // wal mode is only supported with >= sqlite 3.7.0 + db.close(); + return; + } + + // set write-ahead-log mode; see http://www.sqlite.org/wal.html + if (!query.exec(QStringLiteral("PRAGMA journal_mode=wal"))) { + akDebug() << "Could not set sqlite write-ahead-log journal mode"; + akDebug() << "Database: " << mDatabaseName; + akDebug() << "Query error: " << query.lastError().text(); + akDebug() << "Database error: " << db.lastError().text(); + db.close(); + return; + } + + if (!query.next()) { // should never occur + akDebug() << "Could not query sqlite journal mode"; + akDebug() << "Database: " << mDatabaseName; + akDebug() << "Query error: " << query.lastError().text(); + akDebug() << "Database error: " << db.lastError().text(); + db.close(); + return; + } + + const QString journalMode = query.value(0).toString(); + akDebug() << "sqlite journal mode is " << journalMode; + + db.close(); + } + + QSqlDatabase::removeDatabase(connectionName); +} diff --git a/src/server/storage/dbconfigsqlite.h b/src/server/storage/dbconfigsqlite.h new file mode 100644 index 0000000..53edf81 --- /dev/null +++ b/src/server/storage/dbconfigsqlite.h @@ -0,0 +1,87 @@ +/* + Copyright (c) 2010 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 DBCONFIGSQLITE_H +#define DBCONFIGSQLITE_H + +#include "dbconfig.h" + +class QProcess; + +namespace Akonadi { +namespace Server { + +class DbConfigSqlite : public DbConfig +{ +public: + enum Version { + Default, /** Uses the Qt sqlite driver */ + Custom /** Uses the custom qsqlite driver from akonadi/qsqlite */ + }; + +public: + DbConfigSqlite(Version driver); + + /** + * Returns the name of the used driver. + */ + virtual QString driverName() const; + + /** + * Returns the database name. + */ + virtual QString databaseName() const; + + /** + * This method is called whenever the Akonadi server is started + * and before the initial database connection is set up. + * + * At this point the default settings should be determined, merged + * with the given @p settings and written back. + */ + virtual bool init(QSettings &settings); + + /** + * This method applies the configured settings to the QtSql @p database + * instance. + */ + virtual void apply(QSqlDatabase &database); + + /** + * Returns whether an internal server needs to be used. + */ + virtual bool useInternalServer() const; + + /** + * Sets sqlite journal mode to WAL and synchronous mode to NORMAL + */ + virtual void setup(); +private: + Version mDriverVersion; + QString mDatabaseName; + QString mHostName; + QString mUserName; + QString mPassword; + QString mConnectionOptions; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/dbexception.cpp b/src/server/storage/dbexception.cpp new file mode 100644 index 0000000..c0087f0 --- /dev/null +++ b/src/server/storage/dbexception.cpp @@ -0,0 +1,37 @@ +/* + Copyright (c) 2012 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 "dbexception.h" + +#include +#include + +using namespace Akonadi::Server; + +DbException::DbException(const QSqlQuery &query, const char *what) + : Exception(what) +{ + mWhat += "\nSql error: " + query.lastError().text().toUtf8(); + mWhat += "\nQuery: " + query.lastQuery().toUtf8(); +} + +const char *DbException::type() const throw() +{ + return "Database Exception"; +} diff --git a/src/server/storage/dbexception.h b/src/server/storage/dbexception.h new file mode 100644 index 0000000..2a91519 --- /dev/null +++ b/src/server/storage/dbexception.h @@ -0,0 +1,42 @@ +/* + Copyright (c) 2012 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 DBEXCEPTION_H +#define DBEXCEPTION_H + +#include "exception.h" + +class QSqlError; +class QSqlQuery; + +namespace Akonadi { +namespace Server { + +/** Exception for reporting SQL errors. */ +class DbException : public Exception +{ +public: + explicit DbException(const QSqlQuery &query, const char *what = 0); + virtual const char *type() const throw(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // DBEXCEPTION_H diff --git a/src/server/storage/dbinitializer.cpp b/src/server/storage/dbinitializer.cpp new file mode 100644 index 0000000..bc89746 --- /dev/null +++ b/src/server/storage/dbinitializer.cpp @@ -0,0 +1,420 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * Copyright (C) 2012 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "dbinitializer.h" +#include "dbinitializer_p.h" +#include "querybuilder.h" +#include "dbexception.h" +#include "shared/akdebug.h" +#include "schema.h" +#include "entity.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace Akonadi::Server; + +DbInitializer::Ptr DbInitializer::createInstance(const QSqlDatabase &database, Schema *schema) +{ + DbInitializer::Ptr i; + switch (DbType::type(database)) { + case DbType::MySQL: + i.reset(new DbInitializerMySql(database)); + break; + case DbType::Sqlite: + i.reset(new DbInitializerSqlite(database)); + break; + case DbType::PostgreSQL: + i.reset(new DbInitializerPostgreSql(database)); + break; + case DbType::Unknown: + akFatal() << database.driverName() << "backend not supported"; + break; + } + i->mSchema = schema; + return i; +} + +DbInitializer::DbInitializer(const QSqlDatabase &database) + : mDatabase(database) + , mSchema(0) + , mTestInterface(0) + , m_noForeignKeyContraints(false) +{ + m_introspector = DbIntrospector::createInstance(mDatabase); +} + +DbInitializer::~DbInitializer() +{ +} + +bool DbInitializer::run() +{ + try { + akDebug() << "DbInitializer::run()"; + + Q_FOREACH (const TableDescription &table, mSchema->tables()) { + if (!checkTable(table)) { + return false; + } + } + + Q_FOREACH (const RelationDescription &relation, mSchema->relations()) { + if (!checkRelation(relation)) { + return false; + } + } + + akDebug() << "DbInitializer::run() done"; + return true; + } catch (const DbException &e) { + mErrorMsg = QString::fromUtf8(e.what()); + } + return false; +} + +bool DbInitializer::checkTable(const TableDescription &tableDescription) +{ + akDebug() << "checking table " << tableDescription.name; + + if (!m_introspector->hasTable(tableDescription.name)) { + // Get the CREATE TABLE statement for the specific SQL dialect + const QString createTableStatement = buildCreateTableStatement(tableDescription); + akDebug() << createTableStatement; + execQuery(createTableStatement); + } else { + // Check for every column whether it exists, and add the missing ones + Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { + if (!m_introspector->hasColumn(tableDescription.name, columnDescription.name)) { + // Don't add the column on update, DbUpdater will add it + if (columnDescription.noUpdate) { + continue; + } + // Get the ADD COLUMN statement for the specific SQL dialect + const QString statement = buildAddColumnStatement(tableDescription, columnDescription); + akDebug() << statement; + execQuery(statement); + } + } + + // NOTE: we do intentionally not delete any columns here, we defer that to the updater, + // very likely previous columns contain data that needs to be moved to a new column first. + } + + // Add initial data if table is empty + if (tableDescription.data.isEmpty()) { + return true; + } + if (m_introspector->isTableEmpty(tableDescription.name)) { + Q_FOREACH (const DataDescription &dataDescription, tableDescription.data) { + // Get the INSERT VALUES statement for the specific SQL dialect + const QString statement = buildInsertValuesStatement(tableDescription, dataDescription); + akDebug() << statement; + execQuery(statement); + } + } + + return true; +} + +void DbInitializer::checkForeignKeys(const TableDescription &tableDescription) +{ + try { + const QVector existingForeignKeys = m_introspector->foreignKeyConstraints(tableDescription.name); + Q_FOREACH (const ColumnDescription &column, tableDescription.columns) { + DbIntrospector::ForeignKey existingForeignKey; + Q_FOREACH (const DbIntrospector::ForeignKey &fk, existingForeignKeys) { + if (QString::compare(fk.column, column.name, Qt::CaseInsensitive) == 0) { + existingForeignKey = fk; + break; + } + } + + if (!column.refTable.isEmpty() && !column.refColumn.isEmpty()) { + if (!existingForeignKey.column.isEmpty()) { + // there's a constraint on this column, check if it's the correct one + if (QString::compare(existingForeignKey.refTable, column.refTable + QLatin1Literal("table"), Qt::CaseInsensitive) == 0 + && QString::compare(existingForeignKey.refColumn, column.refColumn, Qt::CaseInsensitive) == 0 + && QString::compare(existingForeignKey.onUpdate, referentialActionToString(column.onUpdate), Qt::CaseInsensitive) == 0 + && QString::compare(existingForeignKey.onDelete, referentialActionToString(column.onDelete), Qt::CaseInsensitive) == 0) { + continue; // all good + } + + const QString statement = buildRemoveForeignKeyConstraintStatement(existingForeignKey, tableDescription); + if (!statement.isEmpty()) { + akDebug() << "Found existing foreign constraint that doesn't match the schema:" << existingForeignKey.name + << existingForeignKey.column << existingForeignKey.refTable << existingForeignKey.refColumn; + m_removedForeignKeys << statement; + } + } + + const QString statement = buildAddForeignKeyConstraintStatement(tableDescription, column); + if (statement.isEmpty()) { // not supported + m_noForeignKeyContraints = true; + return; + } + + m_pendingForeignKeys << statement; + + } else if (!existingForeignKey.column.isEmpty()) { + // constraint exists but we don't want one here + const QString statement = buildRemoveForeignKeyConstraintStatement(existingForeignKey, tableDescription); + if (!statement.isEmpty()) { + akDebug() << "Found unexpected foreign key constraint:" << existingForeignKey.name << existingForeignKey.column + << existingForeignKey.refTable << existingForeignKey.refColumn; + m_removedForeignKeys << statement; + } + } + } + } catch (const DbException &e) { + akDebug() << "Fixing foreign key constraints failed:" << e.what(); + // we ignore this since foreign keys are only used for optimizations (not all backends support them anyway) + m_noForeignKeyContraints = true; + } +} + +void DbInitializer::checkIndexes(const TableDescription &tableDescription) +{ + // Add indices + Q_FOREACH (const IndexDescription &indexDescription, tableDescription.indexes) { + // sqlite3 needs unique index identifiers per db + const QString indexName = QStringLiteral("%1_%2").arg(tableDescription.name, indexDescription.name); + if (!m_introspector->hasIndex(tableDescription.name, indexName)) { + // Get the CREATE INDEX statement for the specific SQL dialect + m_pendingIndexes << buildCreateIndexStatement(tableDescription, indexDescription); + } + } +} + +bool DbInitializer::checkRelation(const RelationDescription &relationDescription) +{ + return checkTable(RelationTableDescription(relationDescription)); +} + +QString DbInitializer::errorMsg() const +{ + return mErrorMsg; +} + +bool DbInitializer::hasForeignKeyConstraints() const +{ + return !m_noForeignKeyContraints; +} + +bool DbInitializer::updateIndexesAndConstraints() +{ + Q_FOREACH (const TableDescription &table, mSchema->tables()) { + // Make sure the foreign key constraints are all there + checkForeignKeys(table); + checkIndexes(table); + } + Q_FOREACH (const RelationDescription &relation, mSchema->relations()) { + RelationTableDescription relTable(relation); + checkForeignKeys(relTable); + checkIndexes(relTable); + } + + try { + if (!m_pendingIndexes.isEmpty()) { + akDebug() << "Updating indexes"; + execPendingQueries(m_pendingIndexes); + m_pendingIndexes.clear(); + } + + if (!m_removedForeignKeys.isEmpty()) { + akDebug() << "Removing invalid foreign key constraints"; + execPendingQueries(m_removedForeignKeys); + m_removedForeignKeys.clear(); + } + + if (!m_pendingForeignKeys.isEmpty()) { + akDebug() << "Adding new foreign key constraints"; + execPendingQueries(m_pendingForeignKeys); + m_pendingForeignKeys.clear(); + } + } catch (const DbException &e) { + akDebug() << "Updating index failed: " << e.what(); + return false; + } + + akDebug() << "Indexes successfully created"; + return true; +} + +void DbInitializer::execPendingQueries(const QStringList &queries) +{ + Q_FOREACH (const QString &statement, queries) { + akDebug() << statement; + execQuery(statement); + } +} + +QString DbInitializer::sqlType(const QString &type, int size) const +{ + Q_UNUSED(size); + if (type == QLatin1String("int")) { + return QStringLiteral("INTEGER"); + } + if (type == QLatin1String("qint64")) { + return QStringLiteral("BIGINT"); + } + if (type == QLatin1String("QString")) { + return QStringLiteral("TEXT"); + } + if (type == QLatin1String("QByteArray")) { + return QStringLiteral("LONGBLOB"); + } + if (type == QLatin1String("QDateTime")) { + return QStringLiteral("TIMESTAMP"); + } + if (type == QLatin1String("bool")) { + return QStringLiteral("BOOL"); + } + if (type == QLatin1String("Tristate")) { + return QStringLiteral("TINYINT"); + } + + akDebug() << "Invalid type" << type; + Q_ASSERT(false); + return QString(); +} + +QString DbInitializer::sqlValue(const QString &type, const QString &value) const +{ + if (type == QLatin1String("QDateTime") && value == QLatin1String("QDateTime::currentDateTime()")) { + return QStringLiteral("CURRENT_TIMESTAMP"); + } + if (type == QLatin1String("Tristate")) { + if (value == QLatin1String("False")) { + return QString::number((int)Akonadi::Tristate::False); + } + if (value == QLatin1String("True")) { + return QString::number((int)Akonadi::Tristate::True); + } + return QString::number((int)Akonadi::Tristate::Undefined); + } + + return value; +} + +QString DbInitializer::buildAddColumnStatement(const TableDescription &tableDescription, const ColumnDescription &columnDescription) const +{ + return QStringLiteral("ALTER TABLE %1 ADD COLUMN %2").arg(tableDescription.name, buildColumnStatement(columnDescription, tableDescription)); +} + +QString DbInitializer::buildCreateIndexStatement(const TableDescription &tableDescription, const IndexDescription &indexDescription) const +{ + const QString indexName = QStringLiteral("%1_%2").arg(tableDescription.name, indexDescription.name); + QStringList columns; + if (indexDescription.sort.isEmpty()) { + columns = indexDescription.columns; + } else { + columns.reserve(indexDescription.columns.count()); + std::transform(indexDescription.columns.cbegin(), indexDescription.columns.cend(), + std::back_insert_iterator(columns), + [&indexDescription](const QString &column) { + return QStringLiteral("%1 %2").arg(column, indexDescription.sort); + }); + } + + return QStringLiteral("CREATE %1 INDEX %2 ON %3 (%4)") + .arg(indexDescription.isUnique ? QStringLiteral("UNIQUE") : QString(), indexName, tableDescription.name, columns.join(QStringLiteral(","))); +} + +QString DbInitializer::buildAddForeignKeyConstraintStatement(const TableDescription &table, const ColumnDescription &column) const +{ + Q_UNUSED(table); + Q_UNUSED(column); + return QString(); +} + +QString DbInitializer::buildRemoveForeignKeyConstraintStatement(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const +{ + Q_UNUSED(fk); + Q_UNUSED(table); + return QString(); +} + +QString DbInitializer::buildReferentialAction(ColumnDescription::ReferentialAction onUpdate, ColumnDescription::ReferentialAction onDelete) +{ + return QLatin1Literal("ON UPDATE ") + referentialActionToString(onUpdate) + + QLatin1Literal(" ON DELETE ") + referentialActionToString(onDelete); +} + +QString DbInitializer::referentialActionToString(ColumnDescription::ReferentialAction action) +{ + switch (action) { + case ColumnDescription::Cascade: + return QStringLiteral("CASCADE"); + case ColumnDescription::Restrict: + return QStringLiteral("RESTRICT"); + case ColumnDescription::SetNull: + return QStringLiteral("SET NULL"); + } + + Q_ASSERT(!"invalid referential action enum!"); + return QString(); +} + +QString DbInitializer::buildPrimaryKeyStatement(const TableDescription &table) +{ + QStringList cols; + Q_FOREACH (const ColumnDescription &column, table.columns) { + if (column.isPrimaryKey) { + cols.push_back(column.name); + } + } + return QLatin1Literal("PRIMARY KEY (") + cols.join(QStringLiteral(", ")) + QLatin1Char(')'); +} + +void DbInitializer::execQuery(const QString &queryString) +{ + // if ( Q_UNLIKELY( mTestInterface ) ) { Qt 4.7 has no Q_UNLIKELY yet + if (mTestInterface) { + mTestInterface->execStatement(queryString); + return; + } + + QSqlQuery query(mDatabase); + if (!query.exec(queryString)) { + throw DbException(query); + } +} + +void DbInitializer::setTestInterface(TestInterface *interface) +{ + mTestInterface = interface; +} + +void DbInitializer::setIntrospector(const DbIntrospector::Ptr &introspector) +{ + m_introspector = introspector; +} diff --git a/src/server/storage/dbinitializer.h b/src/server/storage/dbinitializer.h new file mode 100644 index 0000000..7c8e0e7 --- /dev/null +++ b/src/server/storage/dbinitializer.h @@ -0,0 +1,192 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef DBINITIALIZER_H +#define DBINITIALIZER_H + +#include "dbintrospector.h" +#include "schematypes.h" + +#include +#include +#include +#include +#include +#include + +class DbInitializerTest; + +namespace Akonadi { +namespace Server { + +class Schema; + +class TestInterface +{ +public: + virtual ~TestInterface() + { + } + + virtual void execStatement(const QString &statement) = 0; +}; + +/** + * A helper class which takes a reference to a database object and + * the file name of a template file and initializes the database + * according to the rules in the template file. + * + * TODO: Refactor this to be easily reusable for updater too + */ +class DbInitializer +{ +public: + typedef QSharedPointer Ptr; + + /** + Returns an initializer instance for a given backend. + */ + static DbInitializer::Ptr createInstance(const QSqlDatabase &database, Schema *schema = 0); + + /** + * Destroys the database initializer. + */ + virtual ~DbInitializer(); + + /** + * Starts the initialization process. + * On success true is returned, false otherwise. + * + * If something went wrong @see errorMsg() can be used to retrieve more + * information. + */ + bool run(); + + /** + * Returns the textual description of an occurred error. + */ + QString errorMsg() const; + + /** + * Returns whether the database has working and complete foreign keys. + * This information can be used for query optimizations. + * @note Result is invalid before run() has been called. + */ + bool hasForeignKeyConstraints() const; + + /** + * Checks and creates missing indexes. + * + * This method is run after DbUpdater to ensure that data in new columns + * are populated and creation of indexes and foreign keys does not fail. + */ + bool updateIndexesAndConstraints(); + + /** + * Returns a backend-specific CREATE TABLE SQL query describing given table + */ + virtual QString buildCreateTableStatement(const TableDescription &tableDescription) const = 0; + +protected: + /** + * Creates a new database initializer. + * + * @param database The reference to the database. + */ + DbInitializer(const QSqlDatabase &database); + + /** + * Overwrite in backend-specific sub-classes to return the SQL type for a given C++ type. + * @param type Name of the C++ type. + * @param size Optional size hint for the column, if -1 use the default SQL type for @p type. + */ + virtual QString sqlType(const QString &type, int size) const; + /** Overwrite in backend-specific sub-classes to return the SQL value for a given C++ value. */ + virtual QString sqlValue(const QString &type, const QString &value) const; + + virtual QString buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const = 0; + virtual QString buildAddColumnStatement(const TableDescription &tableDescription, const ColumnDescription &columnDescription) const; + virtual QString buildCreateIndexStatement(const TableDescription &tableDescription, const IndexDescription &indexDescription) const; + virtual QString buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const = 0; + + /** + * Returns an SQL statement to add a foreign key constraint to an existing column @p column. + * The default implementation returns an empty string, so any backend supporting foreign key constraints + * must reimplement this. + */ + virtual QString buildAddForeignKeyConstraintStatement(const TableDescription &table, const ColumnDescription &column) const; + + /** + * Returns an SQL statement to remove the foreign key constraint @p fk from table @p table. + * The default implementation returns an empty string, so any backend supporting foreign key constraints + * must reimplement this. + */ + virtual QString buildRemoveForeignKeyConstraintStatement(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const; + + static QString buildReferentialAction(ColumnDescription::ReferentialAction onUpdate, ColumnDescription::ReferentialAction onDelete); + /// Use for multi-column primary keys during table creation + static QString buildPrimaryKeyStatement(const TableDescription &table); + +private: + friend class ::DbInitializerTest; + + /** + * Sets the debug @p interface that shall be used on unit test run. + */ + void setTestInterface(TestInterface *interface); + + /** + * Sets a different DbIntrospector. This allows unit tests to simulate certain + * states of the database. + */ + void setIntrospector(const DbIntrospector::Ptr &introspector); + + /** Helper method for executing a query. + * If a debug interface is set for testing, that gets the queries instead. + * @throws DbException if something went wrong. + */ + void execQuery(const QString &queryString); + + bool checkTable(const TableDescription &tableDescription); + /** + * Checks foreign key constraints on table @p tableDescription and fixes them if necessary. + */ + void checkForeignKeys(const TableDescription &tableDescription); + void checkIndexes(const TableDescription &tableDescription); + bool checkRelation(const RelationDescription &relationDescription); + + static QString referentialActionToString(ColumnDescription::ReferentialAction action); + + void execPendingQueries(const QStringList &queries); + + QSqlDatabase mDatabase; + Schema *mSchema; + QString mErrorMsg; + TestInterface *mTestInterface; + DbIntrospector::Ptr m_introspector; + bool m_noForeignKeyContraints; + QStringList m_pendingIndexes; + QStringList m_pendingForeignKeys; + QStringList m_removedForeignKeys; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/dbinitializer_p.cpp b/src/server/storage/dbinitializer_p.cpp new file mode 100644 index 0000000..114557a --- /dev/null +++ b/src/server/storage/dbinitializer_p.cpp @@ -0,0 +1,311 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * Copyright (C) 2010 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "storage/dbinitializer_p.h" + +using namespace Akonadi::Server; + +//BEGIN MySQL + +DbInitializerMySql::DbInitializerMySql(const QSqlDatabase &database) + : DbInitializer(database) +{ +} + +QString DbInitializerMySql::sqlType(const QString &type, int size) const +{ + if (type == QLatin1String("QString")) { + return QLatin1Literal("VARBINARY(") + QString::number(size <= 0 ? 255 : size) + QLatin1Literal(")"); + } else { + return DbInitializer::sqlType(type, size); + } +} + +QString DbInitializerMySql::buildCreateTableStatement(const TableDescription &tableDescription) const +{ + QStringList columns; + QStringList references; + + Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { + columns.append(buildColumnStatement(columnDescription, tableDescription)); + + if (!columnDescription.refTable.isEmpty() && !columnDescription.refColumn.isEmpty()) { + references << QStringLiteral("FOREIGN KEY (%1) REFERENCES %2Table(%3) ") + .arg(columnDescription.name, columnDescription.refTable, columnDescription.refColumn) + + buildReferentialAction(columnDescription.onUpdate, columnDescription.onDelete); + } + } + + if (tableDescription.primaryKeyColumnCount() > 1) { + columns.push_back(buildPrimaryKeyStatement(tableDescription)); + } + columns << references; + + const QString tableProperties = QStringLiteral(" COLLATE=utf8_general_ci DEFAULT CHARSET=utf8"); + + return QStringLiteral("CREATE TABLE %1 (%2) %3").arg(tableDescription.name, columns.join(QStringLiteral(", ")), tableProperties); +} + +QString DbInitializerMySql::buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const +{ + QString column = columnDescription.name; + + column += QLatin1Char(' ') + sqlType(columnDescription.type, columnDescription.size); + + if (!columnDescription.allowNull) { + column += QLatin1String(" NOT NULL"); + } + + if (columnDescription.isAutoIncrement) { + column += QLatin1String(" AUTO_INCREMENT"); + } + + if (columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1) { + column += QLatin1String(" PRIMARY KEY"); + } + + if (columnDescription.isUnique) { + column += QLatin1String(" UNIQUE"); + } + + if (!columnDescription.defaultValue.isEmpty()) { + const QString defaultValue = sqlValue(columnDescription.type, columnDescription.defaultValue); + + if (!defaultValue.isEmpty()) { + column += QStringLiteral(" DEFAULT %1").arg(defaultValue); + } + } + + return column; +} + +QString DbInitializerMySql::buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const +{ + QMap data = dataDescription.data; + QMutableMapIterator it(data); + while (it.hasNext()) { + it.next(); + it.value().replace(QLatin1String("\\"), QLatin1String("\\\\")); + } + + return QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)") + .arg(tableDescription.name, + QStringList(data.keys()).join(QStringLiteral(",")), + QStringList(data.values()).join(QStringLiteral(","))); +} + +QString DbInitializerMySql::buildAddForeignKeyConstraintStatement(const TableDescription &table, const ColumnDescription &column) const +{ + return QLatin1Literal("ALTER TABLE ") + table.name + QLatin1Literal(" ADD FOREIGN KEY (") + column.name + + QLatin1Literal(") REFERENCES ") + column.refTable + QLatin1Literal("Table(") + column.refColumn + + QLatin1Literal(") ") + buildReferentialAction(column.onUpdate, column.onDelete); +} + +QString DbInitializerMySql::buildRemoveForeignKeyConstraintStatement(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const +{ + return QLatin1Literal("ALTER TABLE ") + table.name + QLatin1Literal(" DROP FOREIGN KEY ") + fk.name; +} + +//END MySQL + +//BEGIN Sqlite + +DbInitializerSqlite::DbInitializerSqlite(const QSqlDatabase &database) + : DbInitializer(database) +{ +} + +QString DbInitializerSqlite::buildCreateTableStatement(const TableDescription &tableDescription) const +{ + QStringList columns; + + Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { + columns.append(buildColumnStatement(columnDescription, tableDescription)); + } + + if (tableDescription.primaryKeyColumnCount() > 1) { + columns.push_back(buildPrimaryKeyStatement(tableDescription)); + } + + return QStringLiteral("CREATE TABLE %1 (%2)").arg(tableDescription.name, columns.join(QStringLiteral(", "))); +} + +QString DbInitializerSqlite::buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const +{ + QString column = columnDescription.name + QLatin1Char(' '); + + if (columnDescription.isAutoIncrement) { + column += QLatin1String("INTEGER"); + } else { + column += sqlType(columnDescription.type, columnDescription.size); + } + + if (columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1) { + column += QLatin1String(" PRIMARY KEY"); + } else if (columnDescription.isUnique) { + column += QLatin1String(" UNIQUE"); + } + + if (columnDescription.isAutoIncrement) { + column += QLatin1String(" AUTOINCREMENT"); + } + + if (!columnDescription.allowNull) { + column += QLatin1String(" NOT NULL"); + } + + if (!columnDescription.defaultValue.isEmpty()) { + const QString defaultValue = sqlValue(columnDescription.type, columnDescription.defaultValue); + + if (!defaultValue.isEmpty()) { + column += QStringLiteral(" DEFAULT %1").arg(defaultValue); + } + } + + return column; +} + +QString DbInitializerSqlite::buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const +{ + QMap data = dataDescription.data; + QMutableMapIterator it(data); + while (it.hasNext()) { + it.next(); + it.value().replace(QLatin1String("true"), QLatin1String("1")); + it.value().replace(QLatin1String("false"), QLatin1String("0")); + } + + return QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)") + .arg(tableDescription.name, + QStringList(data.keys()).join(QStringLiteral(",")), + QStringList(data.values()).join(QStringLiteral(","))); +} + +QString DbInitializerSqlite::sqlValue(const QString &type, const QString &value) const +{ + if (type == QLatin1String("bool")) { + if (value == QLatin1String("false")) { + return QStringLiteral("0"); + } + if (value == QLatin1String("true")) { + return QStringLiteral("1"); + } + return value; + } + + return Akonadi::Server::DbInitializer::sqlValue(type, value); +} + +//END Sqlite + +//BEGIN PostgreSQL + +DbInitializerPostgreSql::DbInitializerPostgreSql(const QSqlDatabase &database) + : DbInitializer(database) +{ +} + +QString DbInitializerPostgreSql::sqlType(const QString &type, int size) const +{ + if (type == QLatin1String("qint64")) { + return QStringLiteral("int8"); + } + if (type == QLatin1String("QByteArray")) { + return QStringLiteral("BYTEA"); + } + if (type == QLatin1String("Tristate")) { + return QStringLiteral("SMALLINT"); + } + + return DbInitializer::sqlType(type, size); +} + +QString DbInitializerPostgreSql::buildCreateTableStatement(const TableDescription &tableDescription) const +{ + QStringList columns; + columns.reserve(tableDescription.columns.size() + 1); + + Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { + columns.append(buildColumnStatement(columnDescription, tableDescription)); + } + + if (tableDescription.primaryKeyColumnCount() > 1) { + columns.push_back(buildPrimaryKeyStatement(tableDescription)); + } + + return QStringLiteral("CREATE TABLE %1 (%2)").arg(tableDescription.name, columns.join(QStringLiteral(", "))); +} + +QString DbInitializerPostgreSql::buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const +{ + QString column = columnDescription.name + QLatin1Char(' '); + + if (columnDescription.isAutoIncrement) { + column += QLatin1String("SERIAL"); + } else { + column += sqlType(columnDescription.type, columnDescription.size); + } + + if (columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1) { + column += QLatin1String(" PRIMARY KEY"); + } else if (columnDescription.isUnique) { + column += QLatin1String(" UNIQUE"); + } + + if (!columnDescription.allowNull && !(columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1)) { + column += QLatin1String(" NOT NULL"); + } + + if (!columnDescription.defaultValue.isEmpty()) { + const QString defaultValue = sqlValue(columnDescription.type, columnDescription.defaultValue); + + if (!defaultValue.isEmpty()) { + column += QStringLiteral(" DEFAULT %1").arg(defaultValue); + } + } + + return column; +} + +QString DbInitializerPostgreSql::buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const +{ + QMap data = dataDescription.data; + + return QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)") + .arg(tableDescription.name, + QStringList(data.keys()).join(QStringLiteral(",")), + QStringList(data.values()).join(QStringLiteral(","))); +} + +QString DbInitializerPostgreSql::buildAddForeignKeyConstraintStatement(const TableDescription &table, const ColumnDescription &column) const +{ + // constraints must have name in PostgreSQL + const QString constraintName = table.name + column.name + QLatin1Literal("_") + column.refTable + column.refColumn + QLatin1Literal("_fk"); + return QLatin1Literal("ALTER TABLE ") + table.name + QLatin1Literal(" ADD CONSTRAINT ") + constraintName + QLatin1Literal(" FOREIGN KEY (") + column.name + + QLatin1Literal(") REFERENCES ") + column.refTable + QLatin1Literal("Table(") + column.refColumn + + QLatin1Literal(") ") + buildReferentialAction(column.onUpdate, column.onDelete); +} + +QString DbInitializerPostgreSql::buildRemoveForeignKeyConstraintStatement(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const +{ + return QLatin1Literal("ALTER TABLE ") + table.name + QLatin1Literal(" DROP CONSTRAINT ") + fk.name; +} + +//END PostgreSQL diff --git a/src/server/storage/dbinitializer_p.h b/src/server/storage/dbinitializer_p.h new file mode 100644 index 0000000..30823fc --- /dev/null +++ b/src/server/storage/dbinitializer_p.h @@ -0,0 +1,71 @@ +/*************************************************************************** + * Copyright (C) 2006 by Tobias Koenig * + * Copyright (C) 2010 by Volker Krause * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef DBINITIALIZER_P_H +#define DBINITIALIZER_P_H + +#include "storage/dbinitializer.h" + +namespace Akonadi { +namespace Server { + +class DbInitializerMySql : public DbInitializer +{ +public: + DbInitializerMySql(const QSqlDatabase &database); +protected: + QString sqlType(const QString &type, int size) const; + + virtual QString buildCreateTableStatement(const TableDescription &tableDescription) const; + virtual QString buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const; + virtual QString buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const; + virtual QString buildAddForeignKeyConstraintStatement(const TableDescription &table, const ColumnDescription &column) const; + virtual QString buildRemoveForeignKeyConstraintStatement(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const; +}; + +class DbInitializerSqlite : public DbInitializer +{ +public: + DbInitializerSqlite(const QSqlDatabase &database); +protected: + virtual QString buildCreateTableStatement(const TableDescription &tableDescription) const; + virtual QString buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const; + virtual QString buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const; + virtual QString sqlValue(const QString &type, const QString &value) const; +}; + +class DbInitializerPostgreSql : public DbInitializer +{ +public: + DbInitializerPostgreSql(const QSqlDatabase &database); +protected: + QString sqlType(const QString &type, int size) const; + + virtual QString buildCreateTableStatement(const TableDescription &tableDescription) const; + virtual QString buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const; + virtual QString buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const; + virtual QString buildAddForeignKeyConstraintStatement(const TableDescription &table, const ColumnDescription &column) const; + virtual QString buildRemoveForeignKeyConstraintStatement(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/dbintrospector.cpp b/src/server/storage/dbintrospector.cpp new file mode 100644 index 0000000..4abb696 --- /dev/null +++ b/src/server/storage/dbintrospector.cpp @@ -0,0 +1,122 @@ +/* + Copyright (C) 2006 by Tobias Koenig + Copyright (c) 2012 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 "dbintrospector.h" +#include "dbintrospector_impl.h" +#include "dbtype.h" +#include "dbexception.h" +#include "querybuilder.h" + +#include + +#include +#include +#include +#include + +using namespace Akonadi::Server; + +DbIntrospector::Ptr DbIntrospector::createInstance(const QSqlDatabase &database) +{ + switch (DbType::type(database)) { + case DbType::MySQL: + return Ptr(new DbIntrospectorMySql(database)); + case DbType::Sqlite: + return Ptr(new DbIntrospectorSqlite(database)); + case DbType::PostgreSQL: + return Ptr(new DbIntrospectorPostgreSql(database)); + case DbType::Unknown: + break; + } + akFatal() << database.driverName() << "backend not supported"; + return Ptr(); +} + +DbIntrospector::DbIntrospector(const QSqlDatabase &database) + : m_database(database) +{ +} + +DbIntrospector::~DbIntrospector() +{ +} + +bool DbIntrospector::hasTable(const QString &tableName) +{ + return m_database.tables().contains(tableName, Qt::CaseInsensitive); +} + +bool DbIntrospector::hasIndex(const QString &tableName, const QString &indexName) +{ + QSqlQuery query(m_database); + if (!query.exec(hasIndexQuery(tableName, indexName))) { + throw DbException(query, "Failed to query index"); + } + return query.next(); +} + +bool DbIntrospector::hasColumn(const QString &tableName, const QString &columnName) +{ + QStringList columns = m_columnCache.value(tableName); + + if (columns.isEmpty()) { + const QSqlRecord table = m_database.record(tableName); + const int numTables = table.count(); + columns.reserve(numTables); + for (int i = 0; i < numTables; ++i) { + const QSqlField column = table.field(i); + columns.push_back(column.name().toLower()); + } + + m_columnCache.insert(tableName, columns); + } + + return columns.contains(columnName.toLower()); +} + +bool DbIntrospector::isTableEmpty(const QString &tableName) +{ + QueryBuilder queryBuilder(tableName, QueryBuilder::Select); + queryBuilder.addColumn(QStringLiteral("*")); + queryBuilder.setLimit(1); + if (!queryBuilder.exec()) { + throw DbException(queryBuilder.query(), "Unable to retrieve data from table."); + } + + QSqlQuery query = queryBuilder.query(); + if (query.size() == 0 || !query.first()) { // table is empty + return true; + } + return false; +} + +QVector DbIntrospector::foreignKeyConstraints(const QString &tableName) +{ + Q_UNUSED(tableName); + return QVector(); +} + +QString DbIntrospector::hasIndexQuery(const QString &tableName, const QString &indexName) +{ + Q_UNUSED(tableName); + Q_UNUSED(indexName); + akFatal() << "Implement index support for your database!"; + return QString(); +} diff --git a/src/server/storage/dbintrospector.h b/src/server/storage/dbintrospector.h new file mode 100644 index 0000000..97c00b1 --- /dev/null +++ b/src/server/storage/dbintrospector.h @@ -0,0 +1,124 @@ +/* + Copyright (C) 2006 by Tobias Koenig + Copyright (c) 2012 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 DBINTROSPECTOR_H +#define DBINTROSPECTOR_H + +#include +#include +#include +#include + +class DbIntrospectorTest; + +namespace Akonadi { +namespace Server { + +/** + * Methods for introspecting the current state of a database schema. + * I.e. this is about the structure of a database, not its content. + */ +class DbIntrospector +{ +public: + typedef QSharedPointer Ptr; + + /** A structure describing an existing foreign key. */ + class ForeignKey + { + public: + QString name; + QString column; + QString refTable; + QString refColumn; + QString onUpdate; // TODO use same enum as DbInitializer + QString onDelete; // dito + }; + + /** + * Returns an introspector instance for a given database. + */ + static DbIntrospector::Ptr createInstance(const QSqlDatabase &database); + + virtual ~DbIntrospector(); + + /** + * Returns @c true if table @p tableName exists. + * The default implementation relies on QSqlDatabase::tables(). Usually this + * does not need to be reimplemented. + */ + virtual bool hasTable(const QString &tableName); + + /** + * Returns @c true of the given table has an index with the given name. + * The default implementation performs the query returned by hasIndexQuery(). + * @see hasIndexQuery() + * @throws DbException on database errors. + */ + virtual bool hasIndex(const QString &tableName, const QString &indexName); + + /** + * Check whether table @p tableName has a column named @p columnName. + * The default implementation should work with all backends. + */ + virtual bool hasColumn(const QString &tableName, const QString &columnName); + + /** + * Check whether table @p tableName is empty, ie. does not contain any rows. + * The default implementation should work for all backends. + * @throws DbException on database errors. + */ + virtual bool isTableEmpty(const QString &tableName); + + /** + * Returns the foreign key constraints on table @p tableName. + * The default implementation returns an empty list, so any backend supporting + * referential integrity should reimplement this. + */ + virtual QVector foreignKeyConstraints(const QString &tableName); + +protected: + /** + * Creates a new database introspector, call from subclass. + * + * @param database The database to introspect. + */ + DbIntrospector(const QSqlDatabase &database); + + /** + * Returns a query string to determine if @p tableName has an index @p indexName. + * The query is expected to have one boolean result row/column. + * This is used by the default implementation of hasIndex() only, thus reimplmentation + * is not necessary if you reimplement hasIndex() + * The default implementation asserts. + */ + virtual QString hasIndexQuery(const QString &tableName, const QString &indexName); + + /** The database connection we are introspecting. */ + QSqlDatabase m_database; + +private: + friend class ::DbIntrospectorTest; + QHash m_columnCache; // avoids extra db roundtrips +}; + +} // namespace Server +} // namespace Akonadi + +#endif // DBINTROSPECTOR_H diff --git a/src/server/storage/dbintrospector_impl.cpp b/src/server/storage/dbintrospector_impl.cpp new file mode 100644 index 0000000..c6a4f55 --- /dev/null +++ b/src/server/storage/dbintrospector_impl.cpp @@ -0,0 +1,182 @@ +/* + Copyright (C) 2006 by Tobias Koenig + Copyright (c) 2012 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 "dbintrospector_impl.h" +#include "dbexception.h" +#include "querybuilder.h" + +#include "akonadiserver_debug.h" +#include + +using namespace Akonadi::Server; + +//BEGIN MySql + +DbIntrospectorMySql::DbIntrospectorMySql(const QSqlDatabase &database) + : DbIntrospector(database) +{ +} + +QString DbIntrospectorMySql::hasIndexQuery(const QString &tableName, const QString &indexName) +{ + return QStringLiteral("SHOW INDEXES FROM %1 WHERE `Key_name` = '%2'") + .arg(tableName, indexName); +} + +QVector< DbIntrospector::ForeignKey > DbIntrospectorMySql::foreignKeyConstraints(const QString &tableName) +{ + QueryBuilder qb(QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS"), QueryBuilder::Select); + qb.addJoin(QueryBuilder::InnerJoin, QStringLiteral("information_schema.KEY_COLUMN_USAGE"), + QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME"), + QStringLiteral("information_schema.KEY_COLUMN_USAGE.CONSTRAINT_NAME")); + qb.addColumn(QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME")); + qb.addColumn(QStringLiteral("information_schema.KEY_COLUMN_USAGE.COLUMN_NAME")); + qb.addColumn(QStringLiteral("information_schema.KEY_COLUMN_USAGE.REFERENCED_TABLE_NAME")); + qb.addColumn(QStringLiteral("information_schema.KEY_COLUMN_USAGE.REFERENCED_COLUMN_NAME")); + qb.addColumn(QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS.UPDATE_RULE")); + qb.addColumn(QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS.DELETE_RULE")); + + qb.addValueCondition(QStringLiteral("information_schema.KEY_COLUMN_USAGE.TABLE_SCHEMA"), Query::Equals, m_database.databaseName()); + qb.addValueCondition(QStringLiteral("information_schema.KEY_COLUMN_USAGE.TABLE_NAME"), Query::Equals, tableName); + + if (!qb.exec()) { + throw DbException(qb.query()); + } + + QVector result; + while (qb.query().next()) { + ForeignKey fk; + fk.name = qb.query().value(0).toString(); + fk.column = qb.query().value(1).toString(); + fk.refTable = qb.query().value(2).toString(); + fk.refColumn = qb.query().value(3).toString(); + fk.onUpdate = qb.query().value(4).toString(); + fk.onDelete = qb.query().value(5).toString(); + result.push_back(fk); + } + + return result; +} + +//END MySql + +//BEGIN Sqlite + +DbIntrospectorSqlite::DbIntrospectorSqlite(const QSqlDatabase &database) + : DbIntrospector(database) +{ +} + +QString DbIntrospectorSqlite::hasIndexQuery(const QString &tableName, const QString &indexName) +{ + return QStringLiteral("SELECT * FROM sqlite_master WHERE type='index' AND tbl_name='%1' AND name='%2';") + .arg(tableName, indexName); +} + +//END Sqlite + +//BEGIN PostgreSql + +DbIntrospectorPostgreSql::DbIntrospectorPostgreSql(const QSqlDatabase &database) + : DbIntrospector(database) +{ +} + +QVector DbIntrospectorPostgreSql::foreignKeyConstraints(const QString &tableName) +{ +#define TABLE_CONSTRAINTS "information_schema.table_constraints" +#define KEY_COLUMN_USAGE "information_schema.key_column_usage" +#define REFERENTIAL_CONSTRAINTS "information_schema.referential_constraints" +#define CONSTRAINT_COLUMN_USAGE "information_schema.constraint_column_usage" + + Query::Condition keyColumnUsageCondition(Query::And); + keyColumnUsageCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_catalog"), Query::Equals, + QStringLiteral(KEY_COLUMN_USAGE ".constraint_catalog")); + keyColumnUsageCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_schema"), Query::Equals, + QStringLiteral(KEY_COLUMN_USAGE ".constraint_schema")); + keyColumnUsageCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_name"), Query::Equals, + QStringLiteral(KEY_COLUMN_USAGE ".constraint_name")); + + Query::Condition referentialConstraintsCondition(Query::And); + referentialConstraintsCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_catalog"), Query::Equals, + QStringLiteral(REFERENTIAL_CONSTRAINTS ".constraint_catalog")); + referentialConstraintsCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_schema"), Query::Equals, + QStringLiteral(REFERENTIAL_CONSTRAINTS ".constraint_schema")); + referentialConstraintsCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_name"), Query::Equals, + QStringLiteral(REFERENTIAL_CONSTRAINTS ".constraint_name")); + + Query::Condition constraintColumnUsageCondition(Query::And); + constraintColumnUsageCondition.addColumnCondition(QStringLiteral(REFERENTIAL_CONSTRAINTS ".unique_constraint_catalog"), Query::Equals, + QStringLiteral(CONSTRAINT_COLUMN_USAGE ".constraint_catalog")); + constraintColumnUsageCondition.addColumnCondition(QStringLiteral(REFERENTIAL_CONSTRAINTS ".unique_constraint_schema"), Query::Equals, + QStringLiteral(CONSTRAINT_COLUMN_USAGE ".constraint_schema")); + constraintColumnUsageCondition.addColumnCondition(QStringLiteral(REFERENTIAL_CONSTRAINTS ".unique_constraint_name"), Query::Equals, + QStringLiteral(CONSTRAINT_COLUMN_USAGE ".constraint_name")); + + QueryBuilder qb(QStringLiteral(TABLE_CONSTRAINTS), QueryBuilder::Select); + qb.addColumn(QStringLiteral(TABLE_CONSTRAINTS ".constraint_name")); + qb.addColumn(QStringLiteral(KEY_COLUMN_USAGE ".column_name")); + qb.addColumn(QStringLiteral(CONSTRAINT_COLUMN_USAGE ".table_name AS referenced_table")); + qb.addColumn(QStringLiteral(CONSTRAINT_COLUMN_USAGE ".column_name AS referenced_column")); + qb.addColumn(QStringLiteral(REFERENTIAL_CONSTRAINTS ".update_rule")); + qb.addColumn(QStringLiteral(REFERENTIAL_CONSTRAINTS ".delete_rule")); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral(KEY_COLUMN_USAGE), keyColumnUsageCondition); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral(REFERENTIAL_CONSTRAINTS), referentialConstraintsCondition); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral(CONSTRAINT_COLUMN_USAGE), constraintColumnUsageCondition); + qb.addValueCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_type"), + Query::Equals, QLatin1String("FOREIGN KEY")); + qb.addValueCondition(QStringLiteral(TABLE_CONSTRAINTS ".table_name"), + Query::Equals, tableName.toLower()); + +#undef TABLE_CONSTRAINTS +#undef KEY_COLUMN_USAGE +#undef REFERENTIAL_CONSTRAINTS +#undef CONSTRAINT_COLUMN_USAGE + + if (!qb.exec()) { + throw DbException(qb.query()); + } + + QVector result; + while (qb.query().next()) { + ForeignKey fk; + fk.name = qb.query().value(0).toString(); + fk.column = qb.query().value(1).toString(); + fk.refTable = qb.query().value(2).toString(); + fk.refColumn = qb.query().value(3).toString(); + fk.onUpdate = qb.query().value(4).toString(); + fk.onDelete = qb.query().value(5).toString(); + result.push_back(fk); + } + + return result; +} + +QString DbIntrospectorPostgreSql::hasIndexQuery(const QString &tableName, const QString &indexName) +{ + QString query = QStringLiteral("SELECT indexname FROM pg_catalog.pg_indexes"); + query += QStringLiteral(" WHERE tablename ilike '%1'").arg(tableName); + query += QStringLiteral(" AND indexname ilike '%1'").arg(indexName); + query += QLatin1String(" UNION SELECT conname FROM pg_catalog.pg_constraint "); + query += QStringLiteral(" WHERE conname ilike '%1'").arg(indexName); + return query; +} + +//END PostgreSql diff --git a/src/server/storage/dbintrospector_impl.h b/src/server/storage/dbintrospector_impl.h new file mode 100644 index 0000000..df642c5 --- /dev/null +++ b/src/server/storage/dbintrospector_impl.h @@ -0,0 +1,55 @@ +/* + Copyright (C) 2006 by Tobias Koenig + Copyright (c) 2012 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 DBINTROSPECTOR_IMPL_H +#define DBINTROSPECTOR_IMPL_H + +#include "dbintrospector.h" + +namespace Akonadi { +namespace Server { + +class DbIntrospectorMySql : public DbIntrospector +{ +public: + DbIntrospectorMySql(const QSqlDatabase &database); + virtual QVector foreignKeyConstraints(const QString &tableName); + virtual QString hasIndexQuery(const QString &tableName, const QString &indexName); +}; + +class DbIntrospectorSqlite : public DbIntrospector +{ +public: + DbIntrospectorSqlite(const QSqlDatabase &database); + QString hasIndexQuery(const QString &tableName, const QString &indexName); +}; + +class DbIntrospectorPostgreSql : public DbIntrospector +{ +public: + DbIntrospectorPostgreSql(const QSqlDatabase &database); + virtual QVector foreignKeyConstraints(const QString &tableName); + QString hasIndexQuery(const QString &tableName, const QString &indexName); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // DBINTROSPECTOR_IMPL_H diff --git a/src/server/storage/dbtype.cpp b/src/server/storage/dbtype.cpp new file mode 100644 index 0000000..3bac308 --- /dev/null +++ b/src/server/storage/dbtype.cpp @@ -0,0 +1,46 @@ +/* + Copyright (c) 2012 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 "dbtype.h" + +using namespace Akonadi::Server; + +DbType::Type DbType::type(const QSqlDatabase &db) +{ + return typeForDriverName(db.driverName()); +} + +DbType::Type DbType::typeForDriverName(const QString &driverName) +{ + if (driverName.startsWith(QLatin1String("QMYSQL"))) { + return MySQL; + } + if (driverName == QLatin1String("QPSQL")) { + return PostgreSQL; + } + if (driverName.startsWith(QLatin1String("QSQLITE"))) { + return Sqlite; + } + return Unknown; +} + +bool DbType::isSystemSQLite(const QSqlDatabase &db) +{ + return db.driverName() == QLatin1String("QSQLITE"); +} diff --git a/src/server/storage/dbtype.h b/src/server/storage/dbtype.h new file mode 100644 index 0000000..573a519 --- /dev/null +++ b/src/server/storage/dbtype.h @@ -0,0 +1,52 @@ +/* + Copyright (c) 2012 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 DBTYPE_H +#define DBTYPE_H + +#include + +namespace Akonadi { +namespace Server { + +/** Helper methods for checking the database system we are dealing with. */ +namespace DbType { + +/** Supported database types. */ +enum Type { + Unknown, + Sqlite, + MySQL, + PostgreSQL +}; + +/** Returns the type of the given database object. */ +Type type(const QSqlDatabase &db); + +/** Returns the type for the given driver name. */ +Type typeForDriverName(const QString &driverName); + +/** Returns true when using QSQLITE driver shipped with Qt, FALSE otherwise */ +bool isSystemSQLite(const QSqlDatabase &db); + +} // namespace DbType +} // namespace Server +} // namespace Akonadi + +#endif // DBTYPE_H diff --git a/src/server/storage/dbupdate.xml b/src/server/storage/dbupdate.xml new file mode 100644 index 0000000..923086a --- /dev/null +++ b/src/server/storage/dbupdate.xml @@ -0,0 +1,319 @@ + + + + + + + + + ALTER TABLE LocationTable DROP COLUMN existCount; + ALTER TABLE LocationTable DROP COLUMN recentCount; + ALTER TABLE LocationTable DROP COLUMN unseenCount; + ALTER TABLE LocationTable DROP COLUMN firstUnseen; + + + + UPDATE LocationTable SET subscribed = true; + + + + ALTER TABLE LocationTable DROP COLUMN cachePolicyId; + ALTER TABLE ResourceTable DROP COLUMN cachePolicyId; + DROP TABLE CachePolicyTable; + + + + UPDATE PartTable SET name = 'PLD:ENVELOPE' WHERE name = 'ENVELOPE'; + UPDATE PartTable SET name = 'PLD:RFC822' WHERE name = 'RFC822'; + UPDATE PartTable SET name = 'PLD:HEAD' WHERE name = 'HEAD'; + UPDATE PartTable SET name = concat( 'ATR:', name ) WHERE substr( name, 1, 4 ) != 'PLD:'; + + + + + DROP TABLE CollectionTable; + ALTER TABLE LocationTable RENAME TO CollectionTable; + ALTER TABLE PimItemTable DROP COLUMN collectionId; + ALTER TABLE PimItemTable CHANGE locationId collectionId BIGINT; + DROP TABLE CollectionAttributeTable; + ALTER TABLE LocationAttributeTable CHANGE locationId collectionId BIGINT; + ALTER TABLE LocationAttributeTable RENAME TO CollectionAttributeTable; + DROP TABLE CollectionMimeTypeRelation; + ALTER TABLE LocationMimeTypeRelation CHANGE Location_Id Collection_Id BIGINT NOT NULL DEFAULT '0'; + ALTER TABLE LocationMimeTypeRelation RENAME TO CollectionMimeTypeRelation; + DROP TABLE CollectionPimItemRelation; + ALTER TABLE LocationPimItemRelation CHANGE Location_Id Collection_Id BIGINT NOT NULL DEFAULT '0'; + ALTER TABLE LocationPimItemRelation RENAME TO CollectionPimItemRelation; + + + + ALTER TABLE PartTable CHANGE datasize datasize BIGINT; + + + + UPDATE CollectionTable SET parentId = NULL WHERE parentId = 0; + ALTER TABLE CollectionTable CHANGE parentId parentId BIGINT DEFAULT NULL; + + + + UPDATE ResourceTable SET isVirtual = true WHERE name = 'akonadi_nepomuktag_resource'; + UPDATE ResourceTable SET isVirtual = true WHERE name = 'akonadi_search_resource'; + + + + UPDATE CollectionTable SET queryString = remoteId WHERE resourceId = 1 AND parentId IS NOT NULL; + UPDATE CollectionTable SET queryLanguage = 'SPARQL' WHERE resourceId = 1 AND parentId IS NOT NULL; + + + + ALTER TABLE CollectionAttributeTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionMimeTypeRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionPimItemRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE FlagTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE MimeTypeTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PartTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PimItemFlagRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PimitemTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE ResourceTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE SchemaVersionTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + + + + + ALTER TABLE ResourceTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE CollectionTable CHANGE remoteId remoteId VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE remoteRevision remoteRevision VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE name name VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE cachePolicyLocalParts cachePolicyLocalParts VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE queryString queryString VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE queryLanguage queryLanguage VARCHAR(255) BINARY; + ALTER TABLE MimeTypeTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE PimItemTable CHANGE remoteId remoteId VARCHAR(255) BINARY; + ALTER TABLE PimItemTable CHANGE remoteRevision remoteRevision VARCHAR(255) BINARY; + ALTER TABLE FlagTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE PartTable CHANGE name name VARCHAR(255) BINARY; + + + + + ALTER TABLE ResourceTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE CollectionTable CHANGE remoteId remoteId VARBINARY(255); + ALTER TABLE CollectionTable CHANGE remoteRevision remoteRevision VARBINARY(255); + ALTER TABLE CollectionTable CHANGE name name VARBINARY(255); + ALTER TABLE CollectionTable CHANGE cachePolicyLocalParts cachePolicyLocalParts VARBINARY(255); + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(255); + ALTER TABLE CollectionTable CHANGE queryLanguage queryLanguage VARBINARY(255); + ALTER TABLE MimeTypeTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE PimItemTable CHANGE remoteId remoteId VARBINARY(255); + ALTER TABLE PimItemTable CHANGE remoteRevision remoteRevision VARBINARY(255); + ALTER TABLE FlagTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE PartTable CHANGE name name VARBINARY(255); + + + UPDATE PimItemFlagRelation SET Flag_id=(SELECT id FROM FlagTable WHERE name='\\SEEN') WHERE Flag_id=(SELECT id FROM FlagTable WHERE name='\\Seen'); + DELETE FROM FlagTable WHERE name='\\Seen'; + + + + + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(1024); + + + + + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(32768); + + + + + ALTER TABLE PimItemFlagRelation CHANGE PimItem_id PimItem_id BIGINT NOT NULL + ALTER TABLE PimItemFlagRelation CHANGE Flag_id Flag_id BIGINT NOT NULL + ALTER TABLE CollectionMimeTypeRelation CHANGE Collection_id Collection_id BIGINT NOT NULL + ALTER TABLE CollectionMimeTypeRelation CHANGE MimeType_id MimeType_id BIGINT NOT NULL + ALTER TABLE CollectionPimItemRelation CHANGE Collection_id Collection_id BIGINT NOT NULL + ALTER TABLE CollectionPimItemRelation CHANGE PimItem_id PimItem_id BIGINT NOT NULL + + + + UPDATE CollectionTable SET isVirtual = true WHERE resourceId IN (SELECT id FROM ResourceTable WHERE isVirtual = true) + UPDATE CollectionTable SET isVirtual = 1 WHERE resourceId IN (SELECT id FROM ResourceTable WHERE isVirtual = 1) + + + + ALTER TABLE CollectionTable ALTER remoteId TYPE text USING convert_from(remoteId,'utf8'); + ALTER TABLE CollectionTable ALTER remoteRevision TYPE text USING convert_from(remoteRevision,'utf8'); + ALTER TABLE CollectionTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE CollectionTable ALTER cachePolicyLocalParts TYPE text USING convert_from(cachePolicyLocalParts,'utf8'); + ALTER TABLE CollectionTable ALTER queryString TYPE text USING convert_from(queryString,'utf8'); + ALTER TABLE CollectionTable ALTER queryLanguage TYPE text USING convert_from(queryLanguage,'utf8'); + ALTER TABLE FlagTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE MimeTypeTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE PartTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE PimItemTable ALTER remoteId TYPE text USING convert_from(remoteId,'utf8'); + ALTER TABLE PimItemTable ALTER remoteRevision TYPE text USING convert_from(remoteRevision,'utf8'); + ALTER TABLE ResourceTable ALTER name TYPE text USING convert_from(name,'utf8'); + + + + + + + + + UPDATE CollectionTable SET queryAttributes = 'QUERYLANGUAGE SPARQL' WHERE queryLanguage = 'SPARQL'; + ALTER TABLE CollectionTable DROP COLUMN queryLanguage; + + + + UPDATE CollectionTable SET enabled = subscribed; + ALTER TABLE CollectionTable DROP COLUMN subscribed; + + + + + + DELETE FROM PimItemFlagRelation WHERE pimItem_id IN ( + SELECT pimItem_id FROM PimItemFlagRelation + LEFT JOIN PimItemTable ON PimItemFlagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) + + DELETE FROM PimItemFlagRelation WHERE flag_id IN ( + SELECT flag_id FROM PimItemFlagRelation + LEFT JOIN FlagTable ON PimItemFlagRelation.flag_id = FlagTable.id + WHERE FlagTable.id IS NULL) + + + DELETE FROM PimItemTagRelation WHERE pimItem_id IN ( + SELECT pimItem_id FROM PimItemTagRelation + LEFT JOIN PimItemTable ON PimItemTagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) + + DELETE FROM PimItemTagRelation WHERE tag_id IN ( + SELECT tag_id FROM PimItemTagRelation + LEFT JOIN TagTable ON PimItemTagRelation.tag_id = TagTable.id + WHERE TagTable.id IS NULL) + + + DELETE FROM CollectionMimeTypeRelation WHERE collection_id IN ( + SELECT collection_id FROM CollectionMimeTypeRelation + LEFT JOIN CollectionTable ON CollectionMimeTypeRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) + + DELETE FROM CollectionMimeTypeRelation WHERE mimeType_id IN ( + SELECT mimeType_id FROM CollectionMimeTypeRelation + LEFT JOIN MimeTypeTable ON CollectionMimeTypeRelation.mimeType_id = MimeTypeTable.id + WHERE MimeTypeTable.id IS NULL) + + + DELETE FROM CollectionPimItemRelation WHERE collection_id IN ( + SELECT collection_id FROM CollectionPimItemRelation + LEFT JOIN CollectionTable ON CollectionPimItemRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) + + DELETE FROM CollectionPimItemRelation WHERE pimItem_id IN ( + SELECT pimItem_id FROM CollectionPimItemRelation + LEFT JOIN PimItemTable ON CollectionPimItemRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) + + + + + + DELETE FROM PimItemFlagRelation WHERE pimItem_id IN ( + SELECT id FROM ( + SELECT pimItem_id AS id FROM PimItemFlagRelation + LEFT JOIN PimItemTable ON PimItemFlagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) x) + + DELETE FROM PimItemFlagRelation WHERE flag_id IN ( + SELECT id FROM ( + SELECT flag_id AS id FROM PimItemFlagRelation + LEFT JOIN FlagTable ON PimItemFlagRelation.flag_id = FlagTable.id + WHERE FlagTable.id IS NULL) x) + + + DELETE FROM PimItemTagRelation WHERE pimItem_id IN ( + SELECT id FROM ( + SELECT pimItem_id AS id FROM PimItemTagRelation + LEFT JOIN PimItemTable ON PimItemTagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) x) + + DELETE FROM PimItemTagRelation WHERE tag_id IN ( + SELECT id FROM ( + SELECT tag_id AS id FROM PimItemTagRelation + LEFT JOIN TagTable ON PimItemTagRelation.tag_id = TagTable.id + WHERE TagTable.id IS NULL) x) + + + DELETE FROM CollectionMimeTypeRelation WHERE collection_id IN ( + SELECT id FROM ( + SELECT collection_id AS id FROM CollectionMimeTypeRelation + LEFT JOIN CollectionTable ON CollectionMimeTypeRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) x) + + DELETE FROM CollectionMimeTypeRelation WHERE mimeType_id IN ( + SELECT id FROM ( + SELECT mimeType_id AS id FROM CollectionMimeTypeRelation + LEFT JOIN MimeTypeTable ON CollectionMimeTypeRelation.mimeType_id = MimeTypeTable.id + WHERE MimeTypeTable.id IS NULL) x) + + + DELETE FROM CollectionPimItemRelation WHERE collection_id IN ( + SELECT id FROM ( + SELECT collection_id AS id FROM CollectionPimItemRelation + LEFT JOIN CollectionTable ON CollectionPimItemRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) x) + + DELETE FROM CollectionPimItemRelation WHERE pimItem_id IN ( + SELECT id FROM ( + SELECT pimItem_id AS id FROM CollectionPimItemRelation + LEFT JOIN PimItemTable ON CollectionPimItemRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) x) + + + diff --git a/src/server/storage/dbupdate.xsd b/src/server/storage/dbupdate.xsd new file mode 100644 index 0000000..86fdba7 --- /dev/null +++ b/src/server/storage/dbupdate.xsd @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/server/storage/dbupdater.cpp b/src/server/storage/dbupdater.cpp new file mode 100644 index 0000000..56369e5 --- /dev/null +++ b/src/server/storage/dbupdater.cpp @@ -0,0 +1,464 @@ +/* + Copyright (c) 2007 - 2012 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 "dbupdater.h" +#include "dbtype.h" +#include "entities.h" +#include "akonadischema.h" +#include "querybuilder.h" +#include "selectquerybuilder.h" +#include "datastore.h" +#include "dbconfig.h" +#include "dbintrospector.h" +#include "dbinitializer_p.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +DbUpdater::DbUpdater(const QSqlDatabase &database, const QString &filename) + : m_database(database) + , m_filename(filename) +{ +} + +bool DbUpdater::run() +{ + Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); + + // TODO error handling + SchemaVersion currentVersion = SchemaVersion::retrieveAll().first(); + + UpdateSet::Map updates; + + if (!parseUpdateSets(currentVersion.version(), updates)) { + return false; + } + + if (updates.isEmpty()) { + return true; + } + + // indicate clients this might take a while + // we can ignore unregistration in error cases, that'll kill the server anyway + if (!QDBusConnection::sessionBus().registerService(DBus::serviceName(DBus::UpgradeIndicator))) { + akFatal() << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message(); + } + + // QMap is sorted, so we should be replaying the changes in correct order + for (QMap::ConstIterator it = updates.constBegin(); it != updates.constEnd(); ++it) { + Q_ASSERT(it.key() > currentVersion.version()); + akDebug() << "DbUpdater: update to version:" << it.key() << " mandatory:" << it.value().abortOnFailure; + + bool success = false; + bool hasTransaction = false; + if (it.value().complex) { // complex update + const QString methodName = QStringLiteral("complexUpdate_%1()").arg(it.value().version); + const int index = metaObject()->indexOfMethod(methodName.toLatin1().constData()); + if (index == -1) { + success = false; + akError() << "Update to version" << it.value().version << "marked as complex, but no implementation is available"; + } else { + const QMetaMethod method = metaObject()->method(index); + method.invoke(this, Q_RETURN_ARG(bool, success)); + if (!success) { + akError() << "Update failed"; + } + } + } else { // regular update + success = m_database.transaction(); + if (success) { + hasTransaction = true; + Q_FOREACH (const QString &statement, it.value().statements) { + QSqlQuery query(m_database); + success = query.exec(statement); + if (!success) { + akError() << "DBUpdater: query error:" << query.lastError().text() << m_database.lastError().text(); + akError() << "Query was: " << statement; + akError() << "Target version was: " << it.key(); + akError() << "Mandatory: " << it.value().abortOnFailure; + } + } + } + } + + if (success) { + currentVersion.setVersion(it.key()); + success = currentVersion.update(); + } + + if (!success || (hasTransaction && !m_database.commit())) { + akError() << "Failed to commit transaction for database update"; + if (hasTransaction) { + m_database.rollback(); + } + if (it.value().abortOnFailure) { + return false; + } + } + } + + QDBusConnection::sessionBus().unregisterService(DBus::serviceName(DBus::UpgradeIndicator)); + return true; +} + +bool DbUpdater::parseUpdateSets(int currentVersion, UpdateSet::Map &updates) const +{ + QFile file(m_filename); + if (!file.open(QIODevice::ReadOnly)) { + akError() << "Unable to open update description file" << m_filename; + return false; + } + + QDomDocument document; + + QString errorMsg; + int line, column; + if (!document.setContent(&file, &errorMsg, &line, &column)) { + akError() << "Unable to parse update description file" << m_filename << ":" + << errorMsg << "at line" << line << "column" << column; + return false; + } + + const QDomElement documentElement = document.documentElement(); + if (documentElement.tagName() != QLatin1String("updates")) { + akError() << "Invalid update description file formant"; + return false; + } + + // iterate over the xml document and extract update information into an UpdateSet + QDomElement updateElement = documentElement.firstChildElement(); + while (!updateElement.isNull()) { + if (updateElement.tagName() == QLatin1String("update")) { + const int version = updateElement.attribute(QStringLiteral("version"), QStringLiteral("-1")).toInt(); + if (version <= 0) { + akError() << "Invalid version attribute in database update description"; + return false; + } + + if (updates.contains(version)) { + akError() << "Duplicate version attribute in database update description"; + return false; + } + + if (version <= currentVersion) { + akDebug() << "skipping update" << version; + } else { + UpdateSet updateSet; + updateSet.version = version; + updateSet.abortOnFailure = (updateElement.attribute(QStringLiteral("abortOnFailure")) == QLatin1String("true")); + + QDomElement childElement = updateElement.firstChildElement(); + while (!childElement.isNull()) { + if (childElement.tagName() == QLatin1String("raw-sql")) { + if (updateApplicable(childElement.attribute(QStringLiteral("backends")))) { + updateSet.statements << buildRawSqlStatement(childElement); + } + } else if (childElement.tagName() == QLatin1String("complex-update")) { + if (updateApplicable(childElement.attribute(QStringLiteral("backends")))) { + updateSet.complex = true; + } + } + //TODO: check for generic tags here in the future + + childElement = childElement.nextSiblingElement(); + } + + updates.insert(version, updateSet); + } + } + updateElement = updateElement.nextSiblingElement(); + } + + return true; +} + +bool DbUpdater::updateApplicable(const QString &backends) const +{ + const QStringList matchingBackends = backends.split(QLatin1Char(',')); + + QString currentBackend; + switch (DbType::type(m_database)) { + case DbType::MySQL: + currentBackend = QStringLiteral("mysql"); + break; + case DbType::PostgreSQL: + currentBackend = QStringLiteral("psql"); + break; + case DbType::Sqlite: + currentBackend = QStringLiteral("sqlite"); + break; + case DbType::Unknown: + return false; + } + + return matchingBackends.contains(currentBackend); +} + +QString DbUpdater::buildRawSqlStatement(const QDomElement &element) const +{ + return element.text().trimmed(); +} + +bool DbUpdater::complexUpdate_25() +{ + akDebug() << "Starting database update to version 25"; + + DbType::Type dbType = DbType::type(DataStore::self()->database()); + + QTime ttotal; + ttotal.start(); + + // Recover from possibly failed or interrupted update + { + // We don't care if this fails, it just means that there was no failed update + QSqlQuery query(DataStore::self()->database()); + query.exec(QStringLiteral("ALTER TABLE PartTable_old RENAME TO PartTable")); + } + + { + QSqlQuery query(DataStore::self()->database()); + query.exec(QStringLiteral("DROP TABLE IF EXISTS PartTable_new")); + } + + { + // Make sure the table is empty, otherwise we get duplicate key error + QSqlQuery query(DataStore::self()->database()); + if (dbType == DbType::Sqlite) { + query.exec(QStringLiteral("DELETE FROM PartTypeTable")); + } else { // MySQL, PostgreSQL + query.exec(QStringLiteral("TRUNCATE TABLE PartTypeTable")); + } + } + + { + // It appears that more users than expected have the invalid "GID" part in their + // PartTable, which breaks the migration below (see BKO#331867), so we apply this + // wanna-be fix to remove the invalid part before we start the actual migration. + QueryBuilder qb(QStringLiteral("PartTable"), QueryBuilder::Delete); + qb.addValueCondition(QStringLiteral("PartTable.name"), Query::Equals, QLatin1String("GID")); + qb.exec(); + } + + akDebug() << "Creating a PartTable_new"; + { + TableDescription description; + description.name = QStringLiteral("PartTable_new"); + + ColumnDescription idColumn; + idColumn.name = QStringLiteral("id"); + idColumn.type = QStringLiteral("qint64"); + idColumn.isAutoIncrement = true; + idColumn.isPrimaryKey = true; + description.columns << idColumn; + + ColumnDescription pimItemIdColumn; + pimItemIdColumn.name = QStringLiteral("pimItemId"); + pimItemIdColumn.type = QStringLiteral("qint64"); + pimItemIdColumn.allowNull = false; + description.columns << pimItemIdColumn; + + ColumnDescription partTypeIdColumn; + partTypeIdColumn.name = QStringLiteral("partTypeId"); + partTypeIdColumn.type = QStringLiteral("qint64"); + partTypeIdColumn.allowNull = false; + description.columns << partTypeIdColumn; + + ColumnDescription dataColumn; + dataColumn.name = QStringLiteral("data"); + dataColumn.type = QStringLiteral("QByteArray"); + description.columns << dataColumn; + + ColumnDescription dataSizeColumn; + dataSizeColumn.name = QStringLiteral("datasize"); + dataSizeColumn.type = QStringLiteral("qint64"); + dataSizeColumn.allowNull = false; + description.columns << dataSizeColumn; + + ColumnDescription versionColumn; + versionColumn.name = QStringLiteral("version"); + versionColumn.type = QStringLiteral("int"); + versionColumn.defaultValue = QStringLiteral("0"); + description.columns << versionColumn; + + ColumnDescription externalColumn; + externalColumn.name = QStringLiteral("external"); + externalColumn.type = QStringLiteral("bool"); + externalColumn.defaultValue = QStringLiteral("false"); + description.columns << externalColumn; + + DbInitializer::Ptr initializer = DbInitializer::createInstance(DataStore::self()->database()); + const QString queryString = initializer->buildCreateTableStatement(description); + + QSqlQuery query(DataStore::self()->database()); + if (!query.exec(queryString)) { + akError() << query.lastError().text(); + return false; + } + } + + akDebug() << "Migrating part types"; + { + // Get list of all part names + QueryBuilder qb(QStringLiteral("PartTable"), QueryBuilder::Select); + qb.setDistinct(true); + qb.addColumn(QStringLiteral("PartTable.name")); + + if (!qb.exec()) { + akError() << qb.query().lastError().text(); + return false; + } + + // Process them one by one + QSqlQuery query = qb.query(); + while (query.next()) { + // Split the part name to namespace and name and insert it to PartTypeTable + const QString partName = query.value(0).toString(); + const QString ns = partName.left(3); + const QString name = partName.mid(4); + + { + QueryBuilder qb(QStringLiteral("PartTypeTable"), QueryBuilder::Insert); + qb.setColumnValue(QStringLiteral("ns"), ns); + qb.setColumnValue(QStringLiteral("name"), name); + if (!qb.exec()) { + akError() << qb.query().lastError().text(); + return false; + } + } + akDebug() << "\t Moved part type" << partName << "to PartTypeTable"; + } + } + + akDebug() << "Migrating data from PartTable to PartTable_new"; + { + QSqlQuery query(DataStore::self()->database()); + QString queryString; + if (dbType == DbType::PostgreSQL) { + queryString = QStringLiteral("INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) " + "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, " + " PartTable.datasize, PartTable.version, PartTable.external " + "FROM PartTable " + "LEFT JOIN PartTypeTable ON " + " PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)"); + } else if (dbType == DbType::MySQL) { + queryString = QStringLiteral("INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) " + "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, " + "PartTable.datasize, PartTable.version, PartTable.external " + "FROM PartTable " + "LEFT JOIN PartTypeTable ON PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)"); + } else if (dbType == DbType::Sqlite) { + queryString = QStringLiteral("INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) " + "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, " + "PartTable.datasize, PartTable.version, PartTable.external " + "FROM PartTable " + "LEFT JOIN PartTypeTable ON PartTable.name = PartTypeTable.ns || ':' || PartTypeTable.name"); + } + + if (!query.exec(queryString)) { + akError() << query.lastError().text(); + return false; + } + } + + akDebug() << "Swapping PartTable_new for PartTable"; + { + // Does an atomic swap + + QSqlQuery query(DataStore::self()->database()); + + if (dbType == DbType::PostgreSQL || dbType == DbType::Sqlite) { + if (dbType == DbType::PostgreSQL) { + DataStore::self()->beginTransaction(); + } + + if (!query.exec(QStringLiteral("ALTER TABLE PartTable RENAME TO PartTable_old"))) { + akError() << query.lastError().text(); + DataStore::self()->rollbackTransaction(); + return false; + } + + // If this fails in SQLite (i.e. without transaction), we can still recover on next start) + if (!query.exec(QStringLiteral("ALTER TABLE PartTable_new RENAME TO PartTable"))) { + akError() << query.lastError().text(); + if (DataStore::self()->inTransaction()) { + DataStore::self()->rollbackTransaction(); + } + return false; + } + + if (dbType == DbType::PostgreSQL) { + DataStore::self()->commitTransaction(); + } + } else { // MySQL cannot do rename in transaction, but supports atomic renames + if (!query.exec(QStringLiteral("RENAME TABLE PartTable TO PartTable_old," + " PartTable_new TO PartTable"))) { + akError() << query.lastError().text(); + return false; + } + } + } + + akDebug() << "Removing PartTable_old"; + { + QSqlQuery query(DataStore::self()->database()); + if (!query.exec(QStringLiteral("DROP TABLE PartTable_old;"))) { + // It does not matter when this fails, we are successfully migrated + akDebug() << query.lastError().text(); + akDebug() << "Not a fatal problem, continuing..."; + } + } + + // Fine tuning for PostgreSQL + akDebug() << "Final tuning of new PartTable"; + { + QSqlQuery query(DataStore::self()->database()); + if (dbType == DbType::PostgreSQL) { + query.exec(QStringLiteral("ALTER TABLE PartTable RENAME CONSTRAINT parttable_new_pkey TO parttable_pkey")); + query.exec(QStringLiteral("ALTER SEQUENCE parttable_new_id_seq RENAME TO parttable_id_seq")); + query.exec(QStringLiteral("SELECT setval('parttable_id_seq', MAX(id) + 1) FROM PartTable")); + } else if (dbType == DbType::MySQL) { + // 0 will automatically reset AUTO_INCREMENT to SELECT MAX(id) + 1 FROM PartTable + query.exec(QStringLiteral("ALTER TABLE PartTable AUTO_INCREMENT = 0")); + } + } + + akDebug() << "Update done in" << ttotal.elapsed() << "ms"; + + // Foreign keys and constraints will be reconstructed automatically once + // all updates are done + + return true; +} diff --git a/src/server/storage/dbupdater.h b/src/server/storage/dbupdater.h new file mode 100644 index 0000000..868aaed --- /dev/null +++ b/src/server/storage/dbupdater.h @@ -0,0 +1,95 @@ +/* + 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_DBUPDATER_H +#define AKONADI_DBUPDATER_H + +#include +#include +#include +#include + +class QDomElement; +class DbUpdaterTest; + +namespace Akonadi { +namespace Server { + +/** + * @short A helper class that contains an update set. + */ +class UpdateSet +{ +public: + typedef QMap Map; + + UpdateSet() + : version(-1) + , abortOnFailure(false) + , complex(false) + { + } + + int version; + bool abortOnFailure; + QStringList statements; + bool complex; +}; + +/** + Updates the database schema. +*/ +class DbUpdater : public QObject +{ + Q_OBJECT + +public: + /** + * Creates a new database updates. + * + * @param database The reference to the database. + * @param filename The file containing the update descriptions. + */ + DbUpdater(const QSqlDatabase &database, const QString &filename); + + /** + * Starts the update process. + * On success true is returned, false otherwise. + */ + bool run(); + +private Q_SLOTS: + bool complexUpdate_25(); + +private: + friend class ::DbUpdaterTest; + + bool updateApplicable(const QString &backends) const; + QString buildRawSqlStatement(const QDomElement &element) const; + + bool parseUpdateSets(int, UpdateSet::Map &updates) const; + + QSqlDatabase m_database; + QString m_filename; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/doxygen-preprocess-entities.sh b/src/server/storage/doxygen-preprocess-entities.sh new file mode 100755 index 0000000..e163105 --- /dev/null +++ b/src/server/storage/doxygen-preprocess-entities.sh @@ -0,0 +1,17 @@ +if test -z "`which xsltproc`"; then + echo "No xlstproc found!" + exit 1; +fi + +case $1 in +create) + xsltproc --stringparam code header entities.xsl akonadidb.xml > entities.h + xsltproc --stringparam code source entities.xsl akonadidb.xml > entities.cpp + xsltproc entities-dox.xsl akonadidb.xml > Database.dox +;; +cleanup) + rm -f entities.h entities.cpp + rm -f Database.dox +;; +esac + diff --git a/src/server/storage/entities-dox.xsl b/src/server/storage/entities-dox.xsl new file mode 100644 index 0000000..686b539 --- /dev/null +++ b/src/server/storage/entities-dox.xsl @@ -0,0 +1,78 @@ + + + + + + +// autogenerated from akonadi.db and entities-dox.xsl +/** +\page akonadi_server_database Database Design + +\section akonadi_server_database_layout Database Layout + +This is an overview of the database layout of the \ref akonadi_design_storage "storage server". +The schema gets generated by the server using the helper class DbInitializer, based on the +definition found in @c server/src/storage/akonadidb.xml. + +\dot +digraph "Akonadi Database Layout" { + graph [rankdir="LR" fontsize="10"] + node [fontsize="10" shape="record" style="filled" fillcolor="lightyellow"] + edge [fontsize="10"] + + + [label="<1>|<>" URL="classAkonadi_1_1.html"]; + + + : -> :[label="n:1"]; + + + + + + : -> :[label="n:m" arrowtail=normal]; + +} +\enddot + + +\section akonadi_server_database_codegeneration Code Generation + +Code to access the database is generated from @c akonadidb.xml using an XSL stylesheet, @c entities.xsl. +The generated code encapsulates basic database operations, such as retrieving, inserting, updating and +removing records, as well as methods to retrieve related records. They also contain methods to retrieve +table and column names for creating SQL queries in a typo-safe way. + +The following classes are generated: + +- Akonadi::Server:: + + +For the helper tables used for n:m relations, the following classes are generated. They are only useful +when creating SQL queries that handle the n:m relations manually. + +- Akonadi::Server::Relation + +*/ + + + + diff --git a/src/server/storage/entities-header.xsl b/src/server/storage/entities-header.xsl new file mode 100644 index 0000000..d515fd3 --- /dev/null +++ b/src/server/storage/entities-header.xsl @@ -0,0 +1,298 @@ + + + + + + + + + +/** + Representation of a record in the table. + + <br> + + + This class is implicitly shared. +*/ +class : private Entity +{ + friend class DataStore; + + public: + /// List of records. + typedef QVector<> List; + + // make some stuff accessible from Entity: + using Entity::Id; + using Entity::id; + using Entity::setId; + using Entity::isValid; + using Entity::joinByName; + using Entity::addToRelation; + using Entity::removeFromRelation; + + // constructor + (); + explicit ( + + , + ); + + explicit ( + + , + ); + + ( const & other ); + + // destructor + ~(); + + /// assignment operator + & operator=( const & other ); + + /// comparisson operator, compares ids, not content + bool operator==( const & other ) const; + + // accessor methods + + /** + Returns the value of the column of this record. + + */ + () const; + /** + Sets the value of the column of this record. + + */ + void ; + + + /** Returns the name of the SQL table. */ + static QString tableName(); + + /** + Returns a list of all SQL column names. The names are in the correct + order for usage with extractResult(). + */ + static QStringList columnNames(); + + /** + Returns a list of all SQL column names prefixed with their tables names. + The names are in the correct order for usage with extractResult(). + */ + static QStringList fullColumnNames(); + + + static QString Column(); + static QString FullColumnName(); + + + /** + Extracts the query result. + @param query A executed query containing a list of records. + Note that the fields need to be in the correct order (same as in the constructor)! + */ + static QVector< > extractResult( QSqlQuery& query ); + + /** Count records with value @p value in column @p column. */ + static int count( const QString &column, const QVariant &value ); + + // check existence + + /** Checks if a record with id @p id exists. */ + static bool exists( qint64 id ); + + + /** Checks if a record with name @name exists. */ + static bool exists( const &name ); + + + // data retrieval + + /** Returns the record with id @p id. */ + static retrieveById( qint64 id ); + + + + /** Returns the record with name @p name. */ + static retrieveByName( const &name ); + + + + + static PartType retrieveByFQName( const QString &ns, const QString &name ); + + + /** Retrieve all records from this table. */ + static ::List retrieveAll(); + /** Retrieve all records with value @p value in column @p key. */ + static ::List retrieveFiltered( const QString &key, const QVariant &value ); + + + + + /** + Retrieve the record referred to by the + column of this record. + */ + () const; + + /** + Set the record referred to by the + column of this record. + */ + void set + ( const &value ); + + + + + /** + Retrieve a list of all records referring to this record + in their column . + */ + QVector<> () const; + + + // data retrieval for n:m relations + + QVector<> s() const; + + + /** + Inserts this record into the DataStore. + @param insertId pointer to an int, filled with the identifier of this record on success. + */ + bool insert( qint64* insertId = 0 ); + + /** + Returns @c true if this record has any pending changes. + */ + bool hasPendingChanges() const; + + /** + Stores all changes made to this record into the database. + Note that this method assumes the existence of an 'id' column to identify + the record to update. If that column does not exist, all records will be + changed. + @returns true on success, false otherwise. + */ + bool update(); + + + /** Deletes this record. */ + bool remove(); + + /** Deletes the record with the given id. */ + static bool remove( qint64 id ); + + + /** + Invalidates the cache entry for this record. + This method has no effect if caching is not enabled for this table. + */ + void invalidateCache() const; + + /** + Invalidates all cache entries for this table. + This method has no effect if caching is not enabled for this table. + */ + static void invalidateCompleteCache(); + + /** + Enable/disable caching for this table. + This method is not thread-safe, call before activating multi-threading. + */ + static void enableCache( bool enable ); + + // manipulate n:m relations + + + /** + Checks wether this record is in a n:m relation with the @p value. + */ + bool relatesTo( const & value ) const; + static bool relatesTo( qint64 leftId, qint64 rightId ); + + /** + Adds a n:m relation between this record and the @p value. + */ + bool add( const & value ) const; + static bool add( qint64 leftId, qint64 rightId ); + + /** + Removes a n:m relation between this record and the @p value. + */ + bool remove( const & value ) const; + static bool remove( qint64 leftId, qint64 rightId ); + + /** + Removes all relations between this record and any . + */ + bool clears() const; + static bool clears( qint64 id ); + + +// protected: + // delete records + static bool remove( const QString &column, const QVariant &value ); + + private: + class Private; + QSharedDataPointer<Private> d; +}; + + + + + + +#ifndef QT_NO_DEBUG_STREAM +// debug stream operator +QDebug & operator<<( QDebug& d, const Akonadi::Server::& entity ); +#endif + + + + + +Relation + + +/** + +*/ + +class +{ + public: + // SQL table information + static QString tableName(); + static QString leftColumn(); + static QString leftFullColumnName(); + static QString rightColumn(); + static QString rightFullColumnName(); +}; + + + diff --git a/src/server/storage/entities-source.xsl b/src/server/storage/entities-source.xsl new file mode 100644 index 0000000..40db521 --- /dev/null +++ b/src/server/storage/entities-source.xsl @@ -0,0 +1,711 @@ + + + + + + + +Table + + +// private class +class ::Private : public QSharedData +{ + public: + Private() : QSharedData() + + + , ( 0 ) + + + + + , () + + + , () + + + // on non-wince, QDateTime is one int + + , () + + + + , ( 0 ) + + + , ( false ) + + + , ( Tristate::Undefined ) + + + , _changed( false ) + + + {} + + + + qint64 ; + + + + + QString ; + + + QByteArray ; + + + // on non-wince, QDateTime is one int + + QDateTime ; + + + + int ; + + + bool : 1; + + + Tristate ; + + + bool _changed : 1; + + + + static void addToCache( const & entry ); + + // cache + static QAtomicInt cacheEnabled; + static QMutex cacheMutex; + + static QHash<qint64, > idCache; + + + static QHash<, > nameCache; + +}; + + +// static members +QAtomicInt ::Private::cacheEnabled(0); +QMutex ::Private::cacheMutex; + +QHash<qint64, > ::Private::idCache; + + +QHash<, > ::Private::nameCache; + + + +void ::Private::addToCache( const & entry ) +{ + Q_ASSERT( cacheEnabled ); + Q_UNUSED( entry ); + QMutexLocker lock(&cacheMutex); + + idCache.insert( entry.id(), entry ); + + + + + + nameCache.insert( entry.ns() + QLatin1Char(':') + entry.name(), entry ); + + + nameCache.insert( entry.name(), entry ); + + + +} + + +// constructor +::() : Entity(), + d( new Private ) +{ +} + +::( + + , + +) : + Entity(), + d( new Private ) +{ + + d-> = ; + d->_changed = true; + +} + + +::( + + , + +) : + Entity( id ), + d( new Private ) +{ + + d-> = ; + d->_changed = true; + +} + + +::( const & other ) + : Entity( other ), d( other.d ) +{ +} + +// destructor +::~() {} + +// assignment operator +& ::operator=( const & other ) +{ + if ( this != &other ) { + d = other.d; + setId( other.id() ); + } + return *this; +} + +// comparisson operator +bool ::operator==( const & other ) const +{ + return id() == other.id(); +} + +// accessor methods + + + Akonadi::Tristate + + ::() const +{ + return d->; +} + +void :: +{ + d-> = ; + d->_changed = true; +} + + + +// SQL table information +QString ::tableName() +{ + static const QString tableName = QStringLiteral( "" ); + return tableName; +} + +QStringList ::columnNames() +{ + static const QStringList columns = QStringList() + + << Column() + + ; + return columns; +} + +QStringList ::fullColumnNames() +{ + static const QStringList columns = QStringList() + + << FullColumnName() + + ; + return columns; +} + + +QString ::Column() +{ + static const QString column = QStringLiteral( "" ); + return column; +} + +QString ::FullColumnName() +{ + static const QString column = QStringLiteral( "." ); + return column; +} + + + +// count records +int ::count( const QString &column, const QVariant &value ) +{ + return Entity::count<>( column, value ); +} + +// check existence + +bool ::exists( qint64 id ) +{ + if ( Private::cacheEnabled ) { + QMutexLocker lock(&Private::cacheMutex); + if ( Private::idCache.contains( id ) ) { + return true; + } + } + return count( idColumn(), id ) > 0; +} + + +bool ::exists( const &name ) +{ + if ( Private::cacheEnabled ) { + QMutexLocker lock(&Private::cacheMutex); + if ( Private::nameCache.contains( name ) ) { + return true; + } + } + return count( nameColumn(), name ) > 0; +} + + + +// result extraction +QVector< > ::extractResult( QSqlQuery & query ) +{ + QVector<> rv; + if (query.driver()->hasFeature(QSqlDriver::QuerySize)) { + rv.reserve(query.size()); + } + while ( query.next() ) { + rv.append( ( + + (query.isNull()) ? + () : + + + Utils::variantToString( query.value( ) ) + + + static_cast<Tristate>(query.value( ).value<int>()) + + + query.value( ).value<>() + + + , + + ) ); + } + return rv; +} + +// data retrieval + + ::retrieveById( qint64 id ) +{ + + id + idCache + +} + + + + ::retrieveByName( const &name ) +{ + + name + nameCache + +} + + + +PartType PartType::retrieveByFQName( const QString & ns, const QString & name ) +{ + const QString fqname = ns + QLatin1Char(':') + name; + + ns + name + fqname + nameCache + +} + + +QVector<> ::retrieveAll() +{ + QSqlDatabase db = DataStore::self()->database(); + if ( !db.isOpen() ) + return QVector<>(); + + QueryBuilder qb( tableName(), QueryBuilder::Select ); + qb.addColumns( columnNames() ); + if ( !qb.exec() ) { + akDebug() << "Error during selection of all records from table" << tableName() + << qb.query().lastError().text() << qb.query().lastQuery(); + return QVector<>(); + } + return extractResult( qb.query() ); +} + +QVector<> ::retrieveFiltered( const QString &key, const QVariant &value ) +{ + QSqlDatabase db = DataStore::self()->database(); + if ( !db.isOpen() ) + return QVector<>(); + + SelectQueryBuilder<> qb; + if ( value.isNull() ) + qb.addValueCondition( key, Query::Is, QVariant() ); + else + qb.addValueCondition( key, Query::Equals, value ); + if ( !qb.exec() ) { + akDebug() << "Error during selection of records from table" << tableName() + << "filtered by" << key << "=" << value + << qb.query().lastError().text(); + return QVector<>(); + } + return qb.result(); +} + +// data retrieval for referenced tables + + + ::() const +{ + return ::retrieveById( () ); +} + +void :: + set + ( const &value ) +{ + d-> = value.id(); + d->_changed = true; +} + + +// data retrieval for inverse referenced tables + +QVector<> ::() const +{ + return ::retrieveFiltered( ::Column(), id() ); +} + + + + +Relation + + + +// data retrieval for n:m relations +QVector<> ::s() const +{ + QSqlDatabase db = DataStore::self()->database(); + if ( !db.isOpen() ) + return QVector<>(); + + QueryBuilder qb( ::tableName(), QueryBuilder::Select ); + static const QStringList columns = QStringList() + + << ::FullColumnName() + + ; + qb.addColumns(columns); + qb.addJoin( QueryBuilder::InnerJoin, ::tableName(), + ::rightFullColumnName(), + ::FullColumnName() ); + qb.addValueCondition( ::leftFullColumnName(), Query::Equals, id() ); + + if ( !qb.exec() ) { + akDebug() << "Error during selection of records from table Relation" + << qb.query().lastError().text(); + return QVector<>(); + } + + return ::extractResult( qb.query() ); +} + +// manipulate n:m relations +bool ::relatesTo( const & value ) const +{ + return Entity::relatesTo<>( id(), value.id() ); +} + +bool ::relatesTo( qint64 leftId, qint64 rightId ) +{ + return Entity::relatesTo<>( leftId, rightId ); +} + +bool ::add( const & value ) const +{ + return Entity::addToRelation<>( id(), value.id() ); +} + +bool ::add( qint64 leftId, qint64 rightId ) +{ + return Entity::addToRelation<>( leftId, rightId ); +} + +bool ::remove( const & value ) const +{ + return Entity::removeFromRelation<>( id(), value.id() ); +} + +bool ::remove( qint64 leftId, qint64 rightId ) +{ + return Entity::removeFromRelation<>( leftId, rightId ); +} + +bool ::clears() const +{ + return Entity::clearRelation<>( id() ); +} + +bool ::clears( qint64 id ) +{ + return Entity::clearRelation<>( id ); +} + + + +#ifndef QT_NO_DEBUG_STREAM +// debug stream operator +QDebug & operator<<( QDebug& d, const & entity ) +{ + d << "[: " + + << " = " << + + + static_cast<int>(entity.()) + + + entity.() + + + << ", " + + << "]"; + return d; +} +#endif + +// inserting new data +bool ::insert( qint64* insertId ) +{ + QSqlDatabase db = DataStore::self()->database(); + if ( !db.isOpen() ) + return false; + + QueryBuilder qb( tableName(), QueryBuilder::Insert ); + + + + if ( d->_changed && d-> > 0 ) + qb.setColumnValue( Column(), this->() ); + + + if ( d->_changed ) + + + qb.setColumnValue( Column(), static_cast<int>(this->()) ); + + + qb.setColumnValue( Column(), this->() ); + + + + + + if ( !qb.exec() ) { + akDebug() << "Error during insertion into table" << tableName() + << qb.query().lastError().text(); + return false; + } + + setId( qb.insertId() ); + if ( insertId ) + *insertId = id(); + return true; +} + +bool ::hasPendingChanges() const +{ + return false + + || d->_changed + ; +} + +// update existing data +bool ::update() +{ + invalidateCache(); + QSqlDatabase db = DataStore::self()->database(); + if ( !db.isOpen() ) + return false; + + QueryBuilder qb( tableName(), QueryBuilder::Update ); + + + + if ( d->_changed ) { + + if ( d-> <= 0 ) + qb.setColumnValue( Column(), QVariant() ); + else + + + + qb.setColumnValue( Column(), static_cast<int>(this->()) ); + + + qb.setColumnValue( Column(), this->() ); + + + } + + + + qb.addValueCondition( idColumn(), Query::Equals, id() ); + + + if ( !qb.exec() ) { + akDebug() << "Error during updating record with id" << id() + << " in table" << tableName() << qb.query().lastError().text(); + return false; + } + return true; +} + +// delete records +bool ::remove( const QString &column, const QVariant &value ) +{ + invalidateCompleteCache(); + return Entity::remove<>( column, value ); +} + + +bool ::remove() +{ + invalidateCache(); + return Entity::remove<>( idColumn(), id() ); +} + +bool ::remove( qint64 id ) +{ + return remove( idColumn(), id ); +} + + +// cache stuff +void ::invalidateCache() const +{ + if ( Private::cacheEnabled ) { + QMutexLocker lock(&Private::cacheMutex); + + Private::idCache.remove( id() ); + + + + + + Private::nameCache.remove( ns() + QLatin1Char(':') + name() ); + + + Private::nameCache.remove( name() ); + + + + } +} + +void ::invalidateCompleteCache() +{ + if ( Private::cacheEnabled ) { + QMutexLocker lock(&Private::cacheMutex); + + Private::idCache.clear(); + + + Private::nameCache.clear(); + + } +} + +void ::enableCache( bool enable ) +{ + Private::cacheEnabled = enable; +} + + + + + + +Relation +Relation + +// SQL table information +QString ::tableName() +{ + static const QString table = QStringLiteral( "" ); + return table; +} + +QString ::leftColumn() +{ + static const QString column = QStringLiteral( "_" ); + return column; +} + +QString ::leftFullColumnName() +{ + static const QString column = QStringLiteral( "._" ); + return column; +} + +QString ::rightColumn() +{ + static const QString column = QStringLiteral( "_" ); + return column; +} + +QString ::rightFullColumnName() +{ + static const QString column = QStringLiteral( "._" ); + return column; +} + + + diff --git a/src/server/storage/entities.xsl b/src/server/storage/entities.xsl new file mode 100644 index 0000000..ab16c3f --- /dev/null +++ b/src/server/storage/entities.xsl @@ -0,0 +1,263 @@ + + + + + + + + + +header + + +/* + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + + + +#ifndef AKONADI_ENTITIES_H +#define AKONADI_ENTITIES_H +#include "storage/entity.h" + +#include <private/tristate_p.h> + +#include <shared/akdebug.h> +#include <QtCore/QDebug> +#include <QtCore/QSharedDataPointer> +#include <QtCore/QString> +#include <QtCore/QVariant> + +template <typename T> class QVector; +class QSqlQuery; +class QStringList; + +namespace Akonadi { +namespace Server { + +// forward declaration for table classes + +class ; + + +// forward declaration for relation classes + +class Relation; + + + + + + + + + + +/** Returns a list of all table names. */ +QVector<QString> allDatabaseTables(); + +} // namespace Server +} // namespace Akonadi + + + + + + +Q_DECLARE_TYPEINFO( Akonadi::Server::, Q_MOVABLE_TYPE ); + +#endif + + + + + +#include <entities.h> +#include <storage/datastore.h> +#include <storage/selectquerybuilder.h> +#include <utils.h> + +#include <qsqldatabase.h> +#include <qsqlquery.h> +#include <qsqlerror.h> +#include <qsqldriver.h> +#include <qvariant.h> +#include <QtCore/QHash> +#include <QtCore/QMutex> + +using namespace Akonadi::Server; + +static QStringList removeEntry(QStringList list, const QString& entry) +{ + list.removeOne(entry); + return list; +} + + + + + + + + + +QVector<QString> Akonadi::Server::allDatabaseTables() +{ + static const QVector<QString> allTables = QVector<QString>() + + << QStringLiteral( "Table" ) + + + << QStringLiteral( "Relation" ) + + ; + return allTables; +} + + + + + + + + + + + + + + + + + const + & + + + + + + + +set( ) + + + + + + + , + + + + + + + + + + + + if ( Private::cacheEnabled ) { + QMutexLocker lock(&Private::cacheMutex); + QHash<, >::const_iterator it = Private::.constFind(); + if ( it != Private::.constEnd() ) { + return it.value(); + } + } + + QSqlDatabase db = DataStore::self()->database(); + if ( !db.isOpen() ) + return (); + + QueryBuilder qb( tableName(), QueryBuilder::Select ); + static const QStringList columns = removeEntry(columnNames(), Column()); + qb.addColumns( columns ); + qb.addValueCondition( Column(), Query::Equals, ); + + qb.addValueCondition( Column(), Query::Equals, ); + + if ( !qb.exec() ) { + akDebug() << "Error during selection of record with " + << << "from table" << tableName() + << qb.query().lastError().text(); + return (); + } + if ( !qb.query().next() ) { + return (); + } + + + int valueIndex = 0; + + const value = + + + ; + + + (qb.query().isNull(valueIndex)) ? + () : + + + Utils::variantToString( qb.query().value( valueIndex ) ) + + + static_cast<Tristate>(qb.query().value( valueIndex ).value<int>()) + + + Utils::variantToDateTime(qb.query().value(valueIndex)) + + + qb.query().value( valueIndex ).value<>() + + + ; ++valueIndex; + + + + + rv( + + value + , + + ); + if ( Private::cacheEnabled ) { + Private::addToCache( rv ); + } + return rv; + + + + + + + + + + + + + + + + diff --git a/src/server/storage/entity.cpp b/src/server/storage/entity.cpp new file mode 100644 index 0000000..5315599 --- /dev/null +++ b/src/server/storage/entity.cpp @@ -0,0 +1,192 @@ +/*************************************************************************** + * Copyright (C) 2006 by Andreas Gungl * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "entity.h" +#include "datastore.h" +#include "countquerybuilder.h" + +#include +#include +#include +#include +#include + +using namespace Akonadi::Server; + +Entity::Entity() + : m_id(-1) +{ +} + +Entity::Entity(qint64 id) + : m_id(id) +{ +} + +Entity::~Entity() +{ +} + +qint64 Entity::id() const +{ + return m_id; +} + +void Entity::setId(qint64 id) +{ + m_id = id; +} + +bool Entity::isValid() const +{ + return m_id != -1; +} + +QSqlDatabase Entity::database() +{ + return DataStore::self()->database(); +} + +int Entity::countImpl(const QString &tableName, const QString &column, const QVariant &value) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return -1; + } + + CountQueryBuilder builder(tableName); + builder.addValueCondition(column, Query::Equals, value); + + if (!builder.exec()) { + akDebug() << "Error during counting records in table" << tableName + << builder.query().lastError().text(); + return -1; + } + + return builder.result(); +} + +bool Entity::removeImpl(const QString &tableName, const QString &column, const QVariant &value) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder builder(tableName, QueryBuilder::Delete); + builder.addValueCondition(column, Query::Equals, value); + + if (!builder.exec()) { + akDebug() << "Error during deleting records from table" + << tableName << builder.query().lastError().text(); + return false; + } + return true; +} + +bool Entity::relatesToImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + CountQueryBuilder builder(tableName); + builder.addValueCondition(leftColumn, Query::Equals, leftId); + builder.addValueCondition(rightColumn, Query::Equals, rightId); + + if (!builder.exec()) { + akDebug() << "Error during counting records in table" << tableName + << builder.query().lastError().text(); + return false; + } + + if (builder.result() > 0) { + return true; + } + return false; +} + +bool Entity::addToRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder qb(tableName, QueryBuilder::Insert); + qb.setColumnValue(leftColumn, leftId); + qb.setColumnValue(rightColumn, rightId); + qb.setIdentificationColumn(QString()); + + if (!qb.exec()) { + akDebug() << "Error during adding a record to table" << tableName + << qb.query().lastError().text(); + return false; + } + + return true; +} + +bool Entity::removeFromRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder builder(tableName, QueryBuilder::Delete); + builder.addValueCondition(leftColumn, Query::Equals, leftId); + builder.addValueCondition(rightColumn, Query::Equals, rightId); + + if (!builder.exec()) { + akDebug() << "Error during removing a record from relation table" << tableName + << builder.query().lastError().text(); + return false; + } + + return true; +} + +bool Entity::clearRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 id, RelationSide side) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder builder(tableName, QueryBuilder::Delete); + switch (side) { + case Left: + builder.addValueCondition(leftColumn, Query::Equals, id); + break; + case Right: + builder.addValueCondition(rightColumn, Query::Equals, id); + break; + default: + qFatal("Invalid enum value"); + } + if (!builder.exec()) { + akDebug() << "Error during clearing relation table" << tableName + << "for id" << id << builder.query().lastError().text(); + return false; + } + + return true; +} diff --git a/src/server/storage/entity.h b/src/server/storage/entity.h new file mode 100644 index 0000000..7982be2 --- /dev/null +++ b/src/server/storage/entity.h @@ -0,0 +1,190 @@ +/*************************************************************************** + * Copyright (C) 2006 by Andreas Gungl * + * * + * This program 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 program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef ENTITY_H +#define ENTITY_H + +#include +#include +#include +#include +#include + +class QVariant; +class QSqlDatabase; + +namespace Akonadi { +namespace Server { + +/** + Base class for classes representing database records. It also contains + low-level data access and manipulation template methods. +*/ +class Entity +{ +public: + typedef qint64 Id; + +protected: + qint64 id() const; + void setId(qint64 id); + + bool isValid() const; + +public: + template static QString joinByName(const QVector &list, const QString &sep) + { + QStringList tmp; + Q_FOREACH (const T &t, list) { + tmp << t.name(); + } + return tmp.join(sep); + } + + /** + Returns the number of records having @p value in @p column. + @param column The name of the key column. + @param value The value used to identify the record. + */ + template inline static int count(const QString &column, const QVariant &value) + { + return Entity::countImpl(T::tableName(), column, value); + } + + /** + Deletes all records having @p value in @p column. + */ + template inline static bool remove(const QString &column, const QVariant &value) + { + return Entity::removeImpl(T::tableName(), column, value); + } + + /** + Checks whether an entry in a n:m relation table exists. + @param leftId Identifier of the left part of the relation. + @param rightId Identifier of the right part of the relation. + */ + template inline static bool relatesTo(qint64 leftId, qint64 rightId) + { + return Entity::relatesToImpl(T::tableName(), T::leftColumn(), T::rightColumn(), leftId, rightId); + } + + /** + Adds an entry to a n:m relation table (specified by the template parameter). + @param leftId Identifier of the left part of the relation. + @param rightId Identifier of the right part of the relation. + */ + template inline static bool addToRelation(qint64 leftId, qint64 rightId) + { + return Entity::addToRelationImpl(T::tableName(), T::leftColumn(), T::rightColumn(), leftId, rightId); + } + + /** + Removes an entry from a n:m relation table (specified by the template parameter). + @param leftId Identifier of the left part of the relation. + @param rightId Identifier of the right part of the relation. + */ + template inline static bool removeFromRelation(qint64 leftId, qint64 rightId) + { + return Entity::removeFromRelationImpl(T::tableName(), T::leftColumn(), T::rightColumn(), leftId, rightId); + } + + enum RelationSide { + Left, + Right + }; + + /** + Clears all entries from a n:m relation table (specified by the given template parameter). + @param id Identifier on the relation side. + @param side The side of the relation. + */ + template inline static bool clearRelation(qint64 id, RelationSide side = Left) + { + return Entity::clearRelationImpl(T::tableName(), T::leftColumn(), T::rightColumn(), id, side); + } + +protected: + Entity(); + explicit Entity(qint64 id); + ~Entity(); + +private: + static int countImpl(const QString &tableName, const QString &column, const QVariant &value); + static bool removeImpl(const QString &tableName, const QString &column, const QVariant &value); + static bool relatesToImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId); + static bool addToRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId); + static bool removeFromRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId); + static bool clearRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 id, RelationSide side); + +private: + static QSqlDatabase database(); + qint64 m_id; +}; + +namespace _detail { + +/*! + Binary predicate to sort collections of Entity subclasses by + their id. + + Example for sorting: + \code + std::sort( coll.begin(), coll.end(), _detail::ById() ); + \endcode + + Example for finding by id: + \code + // linear: + std::find_if( coll.begin(), coll.end(), bind( _detail::ById(), _1, myId ) ); + // binary: + std::lower_bound( coll.begin(), coll.end(), myId, _detail::ById() ); + \end +*/ +template