diff --git a/akonadi/.gitignore b/akonadi/.gitignore new file mode 100644 index 00000000..a5a13b2f --- /dev/null +++ b/akonadi/.gitignore @@ -0,0 +1,6 @@ +# Ignore the following files +*~ +*.[oa] +*.kdev4 +.swp.* +.*.swp diff --git a/akonadi/.kateconfig b/akonadi/.kateconfig new file mode 100644 index 00000000..00950631 --- /dev/null +++ b/akonadi/.kateconfig @@ -0,0 +1 @@ +// kate: space-indent on; indent-width 2; remove-trailing-space on; remove-trailing-space-save on; diff --git a/akonadi/.krazy b/akonadi/.krazy new file mode 100644 index 00000000..6f80d567 --- /dev/null +++ b/akonadi/.krazy @@ -0,0 +1 @@ +EXCLUDE i18ncheckarg,syscalls,qclasses,qmethods,crashy,strings,cpp diff --git a/akonadi/.reviewboardrc b/akonadi/.reviewboardrc new file mode 100644 index 00000000..eb6831ec --- /dev/null +++ b/akonadi/.reviewboardrc @@ -0,0 +1,3 @@ +REVIEWBOARD_URL = "https://git.reviewboard.kde.org" +TARGET_GROUPS = "akonadi" +REPOSITORY = "akonadi" diff --git a/akonadi/AUTHORS b/akonadi/AUTHORS new file mode 100644 index 00000000..fbe1c077 --- /dev/null +++ b/akonadi/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/akonadi/AkonadiConfig.cmake.in b/akonadi/AkonadiConfig.cmake.in new file mode 100644 index 00000000..e53016b9 --- /dev/null +++ b/akonadi/AkonadiConfig.cmake.in @@ -0,0 +1,44 @@ +# AkonadiConfig.cmake is generated by CMake from akonadi/AkonadiConfig.cmake.in. +# Any changed value in this file will be overwritten by CMake. + +@PACKAGE_INIT@ + +# set the akonadi version number +set(AKONADI_VERSION_MAJOR @AKONADI_VERSION_MAJOR@) +set(AKONADI_VERSION_MINOR @AKONADI_VERSION_MINOR@) +set(AKONADI_VERSION_PATCH @AKONADI_VERSION_PATCH@) +set(AKONADI_VERSION @AKONADI_VERSION@) +set(AKONADI_VERSION_STRING "@AKONADI_VERSION_STRING@") + +# set AKONADI_DEFINITIONS +set(AKONADI_USE_STRIGI_SEARCH FALSE) # backward compat, remove eventually +set(AKONADI_DEFINITIONS "@AKONADI_DEFINITIONS@") + +# set the directories +if(NOT AKONADI_INSTALL_DIR) + set(AKONADI_INSTALL_DIR "@CMAKE_INSTALL_PREFIX@") +endif(NOT AKONADI_INSTALL_DIR) + +set_and_check(AKONADI_BIN_DIR "@PACKAGE_BIN_INSTALL_DIR@") +set_and_check(AKONADI_CONFIG_DIR "@PACKAGE_CONFIG_INSTALL_DIR@") +set_and_check(AKONADI_DBUS_INTERFACES_DIR "@PACKAGE_DBUS_INTERFACES_INSTALL_DIR@") +set_and_check(AKONADI_DBUS_SERVICES_DIR "@PACKAGE_DBUS_SERVICES_INSTALL_DIR@") +set_and_check(AKONADI_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@") +set_and_check(AKONADI_LIB_DIR "@PACKAGE_LIB_INSTALL_DIR@") +set_and_check(AKONADI_SHARE_DIR "@PACKAGE_SHARE_INSTALL_PREFIX@") +set_and_check(AKONADI_XDG_MIME_INSTALL_DIR "@PACKAGE_XDG_MIME_INSTALL_DIR@") + +# the exports file exports +set(AKONADI_TARGET_PREFIX @AKONADI_TARGET_PREFIX@) + +# Load the exported targets. +if(NOT TARGET Akonadi__akonadiprotocolinternals) + get_filename_component(_currentDir "${CMAKE_CURRENT_LIST_FILE}" PATH) + include("${_currentDir}/AkonadiTargetsWithPrefix.cmake") +endif() + +macro(_akonadi_Set_Lib_Vars _prefix _lib) + set(AKONADI_${_prefix}_LIBRARIES ${AKONADI_TARGET_PREFIX}${_lib}) +endmacro() + +_akonadi_set_lib_vars(COMMON akonadiprotocolinternals) diff --git a/akonadi/CMakeLists.txt b/akonadi/CMakeLists.txt new file mode 100644 index 00000000..0c520096 --- /dev/null +++ b/akonadi/CMakeLists.txt @@ -0,0 +1,370 @@ +project(Akonadi) + +cmake_minimum_required(VERSION 2.8.8 FATAL_ERROR) + +# where to look first for cmake modules, before ${CMAKE_ROOT}/Modules/ is checked +set(CMAKE_MODULE_PATH "${Akonadi_SOURCE_DIR}/cmake/modules") + +# Enable CMake's Automoc +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +# Used to report the packages we found +include(FeatureSummary) + +############### Build Options ############### + +include(CTest) +include(CTestConfig.cmake) +option(AKONADI_BUILD_TESTS "Build the Akonadi unit tests." TRUE) +option(AKONADI_BUILD_QSQLITE "Build the Sqlite backend." TRUE) +option(INSTALL_QSQLITE_IN_QT_PREFIX "Install the QSQLite plugin in QT_PLUGIN_DIR" FALSE) +option(STATIC_LIBRARY "Build Akonadi as a static library." FALSE) +option(QT5_BUILD "Build Akonadi using the Qt5 framework" FALSE) +option(WITH_SOPRANO "Build with Soprano support. Needed for Nepomuk search integration." FALSE) + +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() + +if(AKONADI_BUILD_TESTS) + enable_testing() +endif() + +############### CMake Macros ############### + +include(InstallSettings) +include(CheckFunctionExists) +include(CheckIncludeFiles) +include(CMakePackageConfigHelpers) + +############### 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) + +############### The Akonadi version (used in AkonadiConfig.cmake) ############### + +set(AKONADI_VERSION_PATCH "0") + +# Raise the minor version if we're building Akonadi using Qt5 +if(QT5_BUILD) + set(AKONADI_VERSION_MAJOR "1") + set(AKONADI_VERSION_MINOR "79") + set(AKONADI_SOVERSION "2") +else () + set(AKONADI_VERSION_MAJOR "1") + set(AKONADI_VERSION_MINOR "13") + set(AKONADI_SOVERSION "1") +endif() + +set(AKONADI_VERSION "${AKONADI_VERSION_MAJOR}.${AKONADI_VERSION_MINOR}.${AKONADI_VERSION_PATCH}") +set(AKONADI_VERSION_STRING "${AKONADI_VERSION}") + +# If Git is installed and a '.git' directory is found, +# we append the Git revision to AKONADI_VERSION_STRING +if(EXISTS "${Akonadi_SOURCE_DIR}/.git") + find_package(Git) + if(GIT_FOUND) + message(STATUS "Found git: ${GIT_EXECUTABLE}") + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + WORKING_DIRECTORY ${Akonadi_SOURCE_DIR} + OUTPUT_VARIABLE akonadi_git_revision + OUTPUT_STRIP_TRAILING_WHITESPACE) + set(AKONADI_VERSION_STRING "${AKONADI_VERSION_STRING} (revision: ${akonadi_git_revision})") + endif() +endif() + + +############### 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}") +endmacro() + +#### DB BACKEND DEFAULT #### +set_default_db_backend(${DATABASE_BACKEND}) + + +############### Find what we need ############### + +#### Qt 4 and 5 #### +if(QT5_BUILD) + find_package(Qt5Core REQUIRED) + find_package(Qt5Gui REQUIRED) + find_package(Qt5Widgets REQUIRED) + find_package(Qt5Sql REQUIRED) + find_package(Qt5Network REQUIRED) + find_package(Qt5Xml REQUIRED) + find_package(Qt5DBus REQUIRED) + find_package(Qt5Test REQUIRED) + + include("cmake/modules/ECMQt4To5Porting.cmake") + include_directories(${QT_INCLUDES}) # TODO: Port away from this. + + if(CMAKE_VERSION VERSION_LESS 2.8.9) + message(FATAL_ERROR "Akonadi Qt 5 build requires at least CMake version 2.8.9") + endif() + + if (Qt5_POSITION_INDEPENDENT_CODE) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + endif() + + set(QT_QTTEST_LIBRARIES Qt5::Test) +else() + set(QT_USE_IMPORTED_TARGETS TRUE) # Qt 4 only + set(QT_MIN_VERSION 4.8.0) # Qt 4 only + + find_package(Qt4 REQUIRED) + include(${QT_USE_FILE}) +endif() + +if(STATIC_LIBRARY) + set(LIBRARY_TYPE STATIC) + set(AKONADI_STATIC_LIBS ON) + message(STATUS "Building Akonadi as a static library") +else() + set(LIBRARY_TYPE SHARED) +endif() + +#### Soprano #### +if(WITH_SOPRANO) + set(SOPRANO_MIN_VERSION 2.7.56) + find_package(Soprano) + + set_package_properties(Soprano PROPERTIES + URL "http://soprano.sourceforge.net" + DESCRIPTION "Storage of semantic data" + TYPE REQUIRED + PURPOSE "Soprano is needed for the Nepomuk search backend" + ) +endif() + +#### SMI #### +set(SMI_MIN_VERSION "0.20") +find_package(SharedMimeInfo ${SMI_MIN_VERSION}) +set_package_properties(SharedMimeInfo PROPERTIES + URL "http://freedesktop.org/wiki/Software/shared-mime-info" + DESCRIPTION "File types database and utilities" + TYPE REQUIRED +) + +#### XSLTProc #### +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() + +#### Boost #### +# In CMake >= 2.8.6, FindBoost.cmake tries to find BoostConfig.cmake which is +# not compatible with CMake's FindBoost. Disable this function. +set(Boost_NO_BOOST_CMAKE TRUE) + +find_package(Boost COMPONENTS program_options) +set_package_properties(Boost PROPERTIES + URL "http://www.boost.org" + DESCRIPTION "The Boost C++ Libraries" + TYPE REQUIRED + PURPOSE "Akonadi requires the Boost C++ libraries (program_options)" +) + +# should be handled by FindBoost.cmake -> cmake bug #8335 +if(WIN32 AND NOT Boost_USE_STATIC_LIBS) + add_definitions(-DBOOST_DYN_LINK) + add_definitions(-DBOOST_PROGRAM_OPTIONS_DYN_LINK) +endif() + + +#### Sqlite #### +# If Sqlite is the default backend, it cannot be optional. +if("${DATABASE_BACKEND}" STREQUAL "SQLITE") + set(SQLITE_TYPE "REQUIRED") +else() + set(SQLITE_TYPE "OPTIONAL") +endif() + +if(AKONADI_BUILD_QSQLITE) + 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() + +############### 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") + + # 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) + + # 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() + +add_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII) +add_definitions(-DQT_NO_KEYWORDS) +add_definitions(-DQT_USE_FAST_CONCATENATION -DQT_USE_FAST_OPERATOR_PLUS) + + +############### Configure checks ############### + +set(AKONADI_SYSTEM_LIBS) + +find_package(Backtrace) +if(Backtrace_FOUND) + include_directories(${Backtrace_INCLUDE_DIRS}) + set(AKONADI_SYSTEM_LIBS ${AKONADI_SYSTEM_LIBS} ${Backtrace_LIBRARIES}) +endif() + +check_include_files(unistd.h HAVE_UNISTD_H) + +# set the output paths +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) +if(WIN32) + set(LIBRARY_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH}) +else() + set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib) +endif() + +# Set up RPATH handling, so the libs are found if they are installed to a non-standard location. +# By default cmake builds the targets with full RPATH to everything in the build directory, +# but then removes the RPATH when installing. +# These two options below make it set the RPATH of the installed targets to all +# RPATH directories outside the current CMAKE_BINARY_DIR and also the library +# install directory, but only if this directory is not a default system library directory. Alex +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + +list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${LIB_INSTALL_DIR}" _isSystemLibDir) +if("${_isSystemLibDir}" STREQUAL "-1") + set(CMAKE_INSTALL_RPATH "${LIB_INSTALL_DIR}") +endif() + +if(NOT DEFINED AKONADI_BUNDLE_PATH) + set(AKONADI_BUNDLE_PATH "/Applications/KDE4") +endif() + +if(APPLE) + message(STATUS "MacOS Bundle Path: ${AKONADI_BUNDLE_PATH}") + set(CMAKE_INSTALL_NAME_DIR ${LIB_INSTALL_DIR}) +endif() + +############### Generate files ############### +# Used in configure_file() and install(EXPORT). Must be set before setting the AkonadiConfig.cmake vars. +set(AKONADI_TARGET_PREFIX Akonadi__) + +if(Soprano_FOUND) + set(HAVE_SOPRANO TRUE) +endif() +configure_file(akonadi-prefix.h.cmake ${Akonadi_BINARY_DIR}/akonadi-prefix.h) +configure_file(config-akonadi.h.cmake ${Akonadi_BINARY_DIR}/config-akonadi.h) + +# This command will replace the installation dirs with absolute paths in AkonadiConfig.cmake +configure_package_config_file(AkonadiConfig.cmake.in ${Akonadi_BINARY_DIR}/AkonadiConfig.cmake + INSTALL_DESTINATION ${LIB_INSTALL_DIR}/cmake/Akonadi + PATH_VARS BIN_INSTALL_DIR INCLUDE_INSTALL_DIR + LIB_INSTALL_DIR CONFIG_INSTALL_DIR + DBUS_INTERFACES_INSTALL_DIR DBUS_SERVICES_INSTALL_DIR + SHARE_INSTALL_PREFIX XDG_MIME_INSTALL_DIR + ) + +# this file is used by to check if the installed version can be used. +write_basic_package_version_file(${Akonadi_BINARY_DIR}/AkonadiConfigVersion.cmake + VERSION ${AKONADI_VERSION} + COMPATIBILITY SameMajorVersion + ) + +if(NOT WIN32) + configure_file(${Akonadi_SOURCE_DIR}/akonadi.pc.cmake ${Akonadi_BINARY_DIR}/akonadi.pc @ONLY) +endif() + +############### build targets ############### + +include_directories(${Akonadi_SOURCE_DIR} ${Akonadi_BINARY_DIR} ${QT_INCLUDES} ${Boost_INCLUDE_DIR}) + +add_subdirectory(interfaces) +add_subdirectory(libs) +set(AKONADI_PROTOCOLINTERNALS_LIBS ${akonadiprotocolinternals_LIB_DEPENDS} akonadiprotocolinternals) + +add_subdirectory(shared) +add_subdirectory(agentserver) +add_subdirectory(server) + +add_subdirectory(rds) +if(NOT WIN32) + add_subdirectory(asapcat) +endif() +if (NOT QT5_BUILD) + if(SQLITE_FOUND) + option(SQLITE_LINK_STATIC "link libsqlite3 statically" FALSE) + add_subdirectory(qsqlite) + endif() +endif() + +############### install stuff ############### + +install(FILES ${Akonadi_BINARY_DIR}/AkonadiConfigVersion.cmake + ${Akonadi_BINARY_DIR}/AkonadiConfig.cmake + DESTINATION ${LIB_INSTALL_DIR}/cmake/Akonadi) + +install(FILES akonadi-mime.xml DESTINATION ${XDG_MIME_INSTALL_DIR}) +update_xdg_mimetypes(${XDG_MIME_INSTALL_DIR}) + +if(NOT WIN32) + install(FILES ${Akonadi_BINARY_DIR}/akonadi.pc DESTINATION ${LIB_INSTALL_DIR}/pkgconfig) +endif() + + +feature_summary(WHAT ALL + INCLUDE_QUIET_PACKAGES + FATAL_ON_MISSING_REQUIRED_PACKAGES + ) + +# Install the file with the exported targets +install(EXPORT akonadiLibraryTargets + NAMESPACE ${AKONADI_TARGET_PREFIX} + DESTINATION ${LIB_INSTALL_DIR}/cmake/Akonadi + FILE AkonadiTargetsWithPrefix.cmake) diff --git a/akonadi/CTestConfig.cmake b/akonadi/CTestConfig.cmake new file mode 100644 index 00000000..23e8d6e4 --- /dev/null +++ b/akonadi/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/akonadi/CTestCustom.cmake b/akonadi/CTestCustom.cmake new file mode 100644 index 00000000..7ac7e58d --- /dev/null +++ b/akonadi/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_" "/libs/tests" "/server/tests" "qrc_" "adaptor.h$" "adaptor.cpp$" "/server/[^/]+interface\\.") diff --git a/akonadi/CodingStyle.txt b/akonadi/CodingStyle.txt new file mode 100644 index 00000000..2cd436ca --- /dev/null +++ b/akonadi/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/akonadi/INSTALL b/akonadi/INSTALL new file mode 100644 index 00000000..0d455b46 --- /dev/null +++ b/akonadi/INSTALL @@ -0,0 +1,64 @@ +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 +* INSTALL_QSQLITE_IN_QT_PREFIX (Default: FALSE): Useful for distributions. + Once enabled, the qsqlite3 backend will be installed in ${QT_PLUGINS_DIR}/sqldrivers +* STATIC_LIBRARY (Default: FALSE): Build the Akonadi server libraries statically. Only useful for certain cases (eg: WINCE builds). +* DATABASE_BACKEND (Default: MYSQL, available: MYSQL, POSTGRES, SQLITE): Define which database driver to use by default. + MYSQL is preferred, SQLITE should be avoided. +* QT5_BUILD (Default: FALSE): Build against Qt5 instead of Qt4. +* WITH_SOPRANO (Default: TRUE): Don't build with Soprano/Nepomuk support + +=== Build Requirements === + +Required: + +* Boost (http://www.boost.org) +* CMake (http://www.cmake.org) >= 2.8.8 +* Qt4 >= 4.6.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 +* Soprano (http://soprano.sourceforge.net) + +=== 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/akonadi/Info.plist.template b/akonadi/Info.plist.template new file mode 100644 index 00000000..c39ddb95 --- /dev/null +++ b/akonadi/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/akonadi/Mainpage.dox b/akonadi/Mainpage.dox new file mode 100644 index 00000000..75a098c9 --- /dev/null +++ b/akonadi/Mainpage.dox @@ -0,0 +1,340 @@ +/** +\mainpage %Akonadi Server + +

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

+ +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. + +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. +
    +
+*/ + +// DOXYGEN_EXCLUDE = sqlplugin server/control server/akonadictl server/tests +// DOXYGEN_PROJECTNAME=Akonadi +// DOXYGEN_PROJECTVERSION=1.10.43 + +// vim:ts=4:sw=4:expandtab:filetype=doxygen diff --git a/akonadi/NEWS b/akonadi/NEWS new file mode 100644 index 00000000..c06a3202 --- /dev/null +++ b/akonadi/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/akonadi/README b/akonadi/README new file mode 100644 index 00000000..a72c6095 --- /dev/null +++ b/akonadi/README @@ -0,0 +1,36 @@ +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. diff --git a/akonadi/README.sqlite b/akonadi/README.sqlite new file mode 100644 index 00000000..85c17ec1 --- /dev/null +++ b/akonadi/README.sqlite @@ -0,0 +1,53 @@ +== 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 ${QT_PLUGINS_DIR}/sqldrivers +if you enable the INSTALL_QSQLITE_IN_QT_PREFIX 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/agentserver/CMakeLists.txt b/akonadi/agentserver/CMakeLists.txt new file mode 100644 index 00000000..72321df4 --- /dev/null +++ b/akonadi/agentserver/CMakeLists.txt @@ -0,0 +1,55 @@ +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_ENABLE_EXCEPTIONS}") + +# Agent server +set(akonadi_agent_server_srcs + agentpluginloader.cpp + agentserver.cpp + agentthread.cpp + main.cpp +) + +add_executable(akonadi_agent_server ${akonadi_agent_server_srcs}) + +target_link_libraries(akonadi_agent_server + akonadi_shared + ${QT_QTGUI_LIBRARIES} + ${QT_QTDBUS_LIBRARY} + ${AKONADI_SYSTEM_LIBS} + ${AKONADI_PROTOCOLINTERNALS_LIBS} +) + +target_link_libraries(akonadi_agent_server ${Boost_PROGRAM_OPTIONS_LIBRARY}) + +# Agent plugin launcher +set(akonadi_agent_launcher_SRCS + agentpluginloader.cpp + agentlauncher.cpp +) + +add_executable(akonadi_agent_launcher MACOSX_BUNDLE ${akonadi_agent_launcher_SRCS}) +target_link_libraries(akonadi_agent_launcher + akonadi_shared + ${QT_QTGUI_LIBRARIES} + ${AKONADI_SYSTEM_LIBS} + ${AKONADI_PROTOCOLINTERNALS_LIBS} + ${Boost_PROGRAM_OPTIONS_LIBRARY} +) + +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 + DESTINATION ${BIN_INSTALL_DIR}) + diff --git a/akonadi/agentserver/TODO b/akonadi/agentserver/TODO new file mode 100644 index 00000000..fb58b825 --- /dev/null +++ b/akonadi/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/akonadi/agentserver/agentlauncher.cpp b/akonadi/agentserver/agentlauncher.cpp new file mode 100644 index 00000000..001b2c2d --- /dev/null +++ b/akonadi/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 +#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} + qDebug() << "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 ) { + qDebug() << "Agent instance created in separate process."; + } else { + qDebug() << "Agent instance creation in separate process failed"; + return 2; + } + + const int rv = app.exec(); + delete instance; + return rv; +} diff --git a/akonadi/agentserver/agentpluginloader.cpp b/akonadi/agentserver/agentpluginloader.cpp new file mode 100644 index 00000000..0440fb66 --- /dev/null +++ b/akonadi/agentserver/agentpluginloader.cpp @@ -0,0 +1,52 @@ +/* + 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 "libs/xdgbasedirs_p.h" +#include "shared/akdebug.h" + +using namespace Akonadi; + +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/akonadi/agentserver/agentpluginloader.h b/akonadi/agentserver/agentpluginloader.h new file mode 100644 index 00000000..aa55c608 --- /dev/null +++ b/akonadi/agentserver/agentpluginloader.h @@ -0,0 +1,46 @@ +/* + 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: + /** + 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: + QHash m_pluginLoaders; +}; + +#endif // AGENTPLUGINLOADER_H diff --git a/akonadi/agentserver/agentserver.cpp b/akonadi/agentserver/agentserver.cpp new file mode 100644 index 00000000..0171ab50 --- /dev/null +++ b/akonadi/agentserver/agentserver.cpp @@ -0,0 +1,134 @@ +/* + 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 "agentthread.h" +#include "libs/xdgbasedirs_p.h" +#include "libs/protocol_p.h" +#include "shared/akdebug.h" + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +AgentServer::AgentServer( QObject *parent ) + : QObject( parent ) + , m_processingConfigureRequests( false ) + , m_quiting( false ) +{ + QDBusConnection::sessionBus().registerObject( QLatin1String( AKONADI_DBUS_AGENTSERVER_PATH ), + this, QDBusConnection::ExportScriptableSlots ); +} + +AgentServer::~AgentServer() +{ + qDebug() << 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, SLOT(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/akonadi/agentserver/agentserver.h b/akonadi/agentserver/agentserver.h new file mode 100644 index 00000000..6b001c08 --- /dev/null +++ b/akonadi/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/akonadi/agentserver/agentthread.cpp b/akonadi/agentserver/agentthread.cpp new file mode 100644 index 00000000..1a33cf9c --- /dev/null +++ b/akonadi/agentserver/agentthread.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 "agentthread.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 ) { + qDebug() << Q_FUNC_INFO << "agent instance created: " << m_instance; + } else { + qDebug() << 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/akonadi/agentserver/agentthread.h b/akonadi/agentserver/agentthread.h new file mode 100644 index 00000000..91ce231b --- /dev/null +++ b/akonadi/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/akonadi/agentserver/main.cpp b/akonadi/agentserver/main.cpp new file mode 100644 index 00000000..51096202 --- /dev/null +++ b/akonadi/agentserver/main.cpp @@ -0,0 +1,50 @@ +/* + 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 ) +{ + AkGuiApplication app( argc, argv ); + app.setDescription( QLatin1String( "Akonadi Agent Server\nDo not run manually, use 'akonadictl' instead to start/stop Akonadi." ) ); + app.parseCommandLine(); + qApp->setQuitOnLastWindowClosed( false ); + + if ( !QDBusConnection::sessionBus().interface()->isServiceRegistered( AkDBus::serviceName( AkDBus::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( AkDBus::serviceName( AkDBus::AgentServer ) ) ) { + akFatal() << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message(); + } + + return app.exec(); +} diff --git a/akonadi/akonadi-mime.xml b/akonadi/akonadi-mime.xml new file mode 100644 index 00000000..13fcc44b --- /dev/null +++ b/akonadi/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/akonadi-prefix.h.cmake b/akonadi/akonadi-prefix.h.cmake new file mode 100644 index 00000000..276e7247 --- /dev/null +++ b/akonadi/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/akonadi.pc.cmake b/akonadi/akonadi.pc.cmake new file mode 100644 index 00000000..a6472b3e --- /dev/null +++ b/akonadi/akonadi.pc.cmake @@ -0,0 +1,11 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=@CMAKE_INSTALL_PREFIX@ +libdir=@LIB_INSTALL_DIR@ +includedir=@CMAKE_INSTALL_PREFIX@/include + +Name: Akonadi +Description: Akonadi server and infrastructure needed to build client libraries and applications +Version: @AKONADI_VERSION@ +Requires: QtCore QtSql QtDBus +Libs: -L${libdir} -lakonadiprotocolinternals +Cflags: -I${includedir} diff --git a/akonadi/asapcat/CMakeLists.txt b/akonadi/asapcat/CMakeLists.txt new file mode 100644 index 00000000..8bea799c --- /dev/null +++ b/akonadi/asapcat/CMakeLists.txt @@ -0,0 +1,15 @@ +project(asapcat) + +#set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_ENABLE_EXCEPTIONS}" ) +set(AKONADI_PROTOCOLINTERNALS_LIBS ${akonadiprotocolinternals_LIB_DEPENDS} akonadiprotocolinternals) + +set(asapcat_srcs + main.cpp + session.cpp +) + +add_executable(asapcat ${asapcat_srcs}) + +target_link_libraries(asapcat akonadi_shared ${QT_QTCORE_LIBRARY} ${QT_QTNETWORK_LIBRARY} ${AKONADI_PROTOCOLINTERNALS_LIBS} ${Boost_PROGRAM_OPTIONS_LIBRARY}) + +install(TARGETS asapcat DESTINATION ${BIN_INSTALL_DIR}) diff --git a/akonadi/asapcat/main.cpp b/akonadi/asapcat/main.cpp new file mode 100644 index 00000000..387bc94d --- /dev/null +++ b/akonadi/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 + +#include +#include + +#include +#include + +int main( int argc, char **argv ) +{ + AkCoreApplication app( argc, argv ); + app.setDescription( QLatin1String( "Akonadi ASAP cat\n" + "This is a development tool, only use this if you know what you are doing.\n\n" + "Usage: asapcat [input]" ) ); + + boost::program_options::options_description options; + options.add_options() + ( "input", boost::program_options::value()->default_value( "-" ), "input to read commands from" ); + app.addCommandLineOptions( options ); + app.addPositionalCommandLineOption( "input", 1 ); + + app.parseCommandLine(); + + Session session( QString::fromStdString( app.commandLineArguments()["input"].as() ) ); + QObject::connect( &session, SIGNAL(disconnected()), QCoreApplication::instance(), SLOT(quit()) ); + QMetaObject::invokeMethod( &session, "connectToHost", Qt::QueuedConnection ); + + const int result = app.exec(); + session.printStats(); + return result; +} diff --git a/akonadi/asapcat/session.cpp b/akonadi/asapcat/session.cpp new file mode 100644 index 00000000..2197871b --- /dev/null +++ b/akonadi/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, SIGNAL(activated(int)), SLOT(inputAvailable()) ); + } + m_input = file; +} + +Session::~Session() +{ +} + +void Session::connectToHost() +{ + const QSettings connectionSettings( AkStandardDirs::connectionConfigFile(), QSettings::IniFormat ); + + QString serverAddress; +#ifdef Q_OS_WIN + serverAddress = connectionSettings.value( QLatin1String( "Data/NamedPipe" ), QString() ).toString(); +#else + serverAddress = connectionSettings.value( QLatin1String( "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, SIGNAL(disconnected()), SLOT(serverDisconnected()) ); + connect( socket, SIGNAL(readyRead()), SLOT(serverRead()) ); + connect( socket, SIGNAL(connected()), SLOT(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/akonadi/asapcat/session.h b/akonadi/asapcat/session.h new file mode 100644 index 00000000..0edc04c1 --- /dev/null +++ b/akonadi/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/akonadi/asapcat/tests/imap-4.10-sync.asap b/akonadi/asapcat/tests/imap-4.10-sync.asap new file mode 100644 index 00000000..d13ebf18 --- /dev/null +++ b/akonadi/asapcat/tests/imap-4.10-sync.asap @@ -0,0 +1,4 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 471 +3 FETCH 1:* CACHEONLY EXTERNALPAYLOAD (UID REMOTEID REMOTEREVISION COLLECTIONID FLAGS SIZE) +4 LOGOUT diff --git a/akonadi/asapcat/tests/imap-4.11-body-check.asap b/akonadi/asapcat/tests/imap-4.11-body-check.asap new file mode 100644 index 00000000..c1b86d35 --- /dev/null +++ b/akonadi/asapcat/tests/imap-4.11-body-check.asap @@ -0,0 +1,4 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 471 +3 FETCH 1:* CACHEONLY CHECKCACHEDPARTSONLY EXTERNALPAYLOAD (UID REMOTEID REMOTEREVISION COLLECTIONID FLAGS SIZE PLD:RFC822) +4 LOGOUT diff --git a/akonadi/asapcat/tests/kmail-4.10-folder-listing.asap b/akonadi/asapcat/tests/kmail-4.10-folder-listing.asap new file mode 100644 index 00000000..680781c0 --- /dev/null +++ b/akonadi/asapcat/tests/kmail-4.10-folder-listing.asap @@ -0,0 +1,5 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 471 +3 FETCH 1:* IGNOREERRORS ANCESTORS INF EXTERNALPAYLOAD (UID REMOTEID REMOTEREVISION COLLECTIONID FLAGS SIZE DATETIME PLD:ENVELOPE) +4 LOGOUT + diff --git a/akonadi/asapcat/tests/kmail-4.11-folder-listing.asap b/akonadi/asapcat/tests/kmail-4.11-folder-listing.asap new file mode 100644 index 00000000..f5bd4f4f --- /dev/null +++ b/akonadi/asapcat/tests/kmail-4.11-folder-listing.asap @@ -0,0 +1,5 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 471 +3 FETCH 1:* IGNOREERRORS ANCESTORS INF EXTERNALPAYLOAD (UID REMOTEID REMOTEREVISION COLLECTIONID FLAGS SIZE PLD:ENVELOPE) +4 LOGOUT + diff --git a/akonadi/asapcat/tests/kmail-4.12-folder-listing.asap b/akonadi/asapcat/tests/kmail-4.12-folder-listing.asap new file mode 100644 index 00000000..f3acf203 --- /dev/null +++ b/akonadi/asapcat/tests/kmail-4.12-folder-listing.asap @@ -0,0 +1,5 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 496 +3 FETCH 1:* IGNOREERRORS ANCESTORS INF EXTERNALPAYLOAD (UID COLLECTIONID FLAGS SIZE PLD:ENVELOPE) +4 LOGOUT + diff --git a/akonadi/cmake/modules/BasicFindPackageVersion.cmake.in b/akonadi/cmake/modules/BasicFindPackageVersion.cmake.in new file mode 100644 index 00000000..ae5d3125 --- /dev/null +++ b/akonadi/cmake/modules/BasicFindPackageVersion.cmake.in @@ -0,0 +1,30 @@ +# This is a very basic file for the new style find_package() search mode, +# i.e. Config-mode. It is used by MACRO_WRITE_BASIC_CMAKE_VERSION_FILE() from +# MacroWriteBasicCMakeVersionFile.cmake. +# In this mode find_package() searches for a Config.cmake +# file and an associated Version.cmake file, which it loads to check +# the version number. +# This file can be used with configure_file() to generate such a file for a project +# with very basic logic. +# It sets PACKAGE_VERSION_EXACT if the current version string and the requested +# version string are exactly the same and it sets PACKAGE_VERSION_COMPATIBLE +# if the current version is >= requested version. +# If this is not good enough for your project, you need to write your own +# improved Version.cmake file. +# This file requires the following three variables to be set: +# PROJECT_VERSION_MAJOR +# PROJECT_VERSION_MINOR +# PROJECT_VERSION_PATCH + + +set(PACKAGE_VERSION @PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@) + +if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}" ) + set(PACKAGE_VERSION_COMPATIBLE FALSE) +else("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}" ) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + if( "${PACKAGE_FIND_VERSION}" STREQUAL "${PACKAGE_VERSION}") + set(PACKAGE_VERSION_EXACT TRUE) + endif( "${PACKAGE_FIND_VERSION}" STREQUAL "${PACKAGE_VERSION}") +endif("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}" ) + diff --git a/akonadi/cmake/modules/COPYING-CMAKE-SCRIPTS b/akonadi/cmake/modules/COPYING-CMAKE-SCRIPTS new file mode 100644 index 00000000..4b417765 --- /dev/null +++ b/akonadi/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/akonadi/cmake/modules/ECMQt4To5Porting.cmake b/akonadi/cmake/modules/ECMQt4To5Porting.cmake new file mode 100644 index 00000000..7d6458a7 --- /dev/null +++ b/akonadi/cmake/modules/ECMQt4To5Porting.cmake @@ -0,0 +1,119 @@ +#============================================================================= +# Copyright 2005-2011 Kitware, Inc. +# 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 name of Kitware, Inc. nor the names of its +# 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. +#============================================================================= + +# Portability helpers. + +set(QT_QTGUI_LIBRARIES + ${Qt5Gui_LIBRARIES} + ${Qt5Widgets_LIBRARIES} + ${Qt5PrintSupport_LIBRARIES} + ${Qt5Svg_LIBRARIES} +) + +set(QT_INCLUDES + ${Qt5Gui_INCLUDE_DIRS} + ${Qt5Widgets_INCLUDE_DIRS} + ${Qt5PrintSupport_INCLUDE_DIRS} + ${Qt5Svg_INCLUDE_DIRS} +) + +set(_qt_modules + Core + Declarative + Widgets + Script + ScriptTools + DBus + Network + Test + Designer + Concurrent + Xml + UiTools + WebKit + Sql + OpenGL +) + +foreach(_module ${_qt_modules}) + string(TOUPPER ${_module} _module_upper) + set(QT_QT${_module_upper}_LIBRARIES ${Qt5${_module}_LIBRARIES}) + set(QT_QT${_module_upper}_LIBRARY ${QT_QT${_module_upper}_LIBRARIES}) + list(APPEND QT_INCLUDES ${Qt5${_module}_INCLUDE_DIRS}) + set(QT_QT${_module_upper}_FOUND ${Qt5${_module}_FOUND}) +endforeach() + +get_target_property(QT_QMAKE_EXECUTABLE Qt5::qmake LOCATION) +get_target_property(QT_RCC_EXECUTABLE Qt5::rcc LOCATION) +if (TARGET Qt5::uic) + get_target_property(QT_UIC_EXECUTABLE Qt5::uic LOCATION) +endif() + +if (TARGET Qt5::qdbuscpp2xml) + get_target_property(QT_QDBUSCPP2XML_EXECUTABLE Qt5::qdbuscpp2xml LOCATION) +endif() + +if (TARGET Qt5::qdbusxml2cpp) + get_target_property(QT_QDBUSXML2CPP_EXECUTABLE Qt5::qdbusxml2cpp LOCATION) +endif() + +macro(qt4_wrap_ui) + qt5_wrap_ui(${ARGN}) +endmacro() + +macro(qt4_wrap_cpp) + qt5_wrap_cpp(${ARGN}) +endmacro() + +macro(qt4_generate_moc) + qt5_generate_moc(${ARGN}) +endmacro() + +macro(qt4_add_dbus_adaptor) + qt5_add_dbus_adaptor(${ARGN}) +endmacro() + +macro(qt4_add_dbus_interfaces) + qt5_add_dbus_interfaces(${ARGN}) +endmacro() + +macro(qt4_add_dbus_interface) + qt5_add_dbus_interface(${ARGN}) +endmacro() + +macro(qt4_generate_dbus_interface) + qt5_generate_dbus_interface(${ARGN}) +endmacro() + +macro(qt4_add_resources) + qt5_add_resources(${ARGN}) +endmacro() diff --git a/akonadi/cmake/modules/FindBacktrace.cmake b/akonadi/cmake/modules/FindBacktrace.cmake new file mode 100644 index 00000000..0af8d1bc --- /dev/null +++ b/akonadi/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/akonadi/cmake/modules/FindLibraryWithDebug.cmake b/akonadi/cmake/modules/FindLibraryWithDebug.cmake new file mode 100644 index 00000000..58cd7308 --- /dev/null +++ b/akonadi/cmake/modules/FindLibraryWithDebug.cmake @@ -0,0 +1,113 @@ +# +# FIND_LIBRARY_WITH_DEBUG +# -> enhanced FIND_LIBRARY to allow the search for an +# optional debug library with a WIN32_DEBUG_POSTFIX similar +# to CMAKE_DEBUG_POSTFIX when creating a shared lib +# it has to be the second and third argument + +# Copyright (c) 2007, Christian Ehrlicher, +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +MACRO(FIND_LIBRARY_WITH_DEBUG var_name win32_dbg_postfix_name dgb_postfix libname) + + IF(NOT "${win32_dbg_postfix_name}" STREQUAL "WIN32_DEBUG_POSTFIX") + + # no WIN32_DEBUG_POSTFIX -> simply pass all arguments to FIND_LIBRARY + FIND_LIBRARY(${var_name} + ${win32_dbg_postfix_name} + ${dgb_postfix} + ${libname} + ${ARGN} + ) + + ELSE(NOT "${win32_dbg_postfix_name}" STREQUAL "WIN32_DEBUG_POSTFIX") + + IF(NOT WIN32) + # on non-win32 we don't need to take care about WIN32_DEBUG_POSTFIX + + FIND_LIBRARY(${var_name} ${libname} ${ARGN}) + + ELSE(NOT WIN32) + + # 1. get all possible libnames + SET(args ${ARGN}) + SET(newargs "") + SET(libnames_release "") + SET(libnames_debug "") + + LIST(LENGTH args listCount) + + IF("${libname}" STREQUAL "NAMES") + SET(append_rest 0) + LIST(APPEND args " ") + + FOREACH(i RANGE ${listCount}) + LIST(GET args ${i} val) + + IF(append_rest) + LIST(APPEND newargs ${val}) + ELSE(append_rest) + IF("${val}" STREQUAL "PATHS") + LIST(APPEND newargs ${val}) + SET(append_rest 1) + ELSE("${val}" STREQUAL "PATHS") + LIST(APPEND libnames_release "${val}") + LIST(APPEND libnames_debug "${val}${dgb_postfix}") + ENDIF("${val}" STREQUAL "PATHS") + ENDIF(append_rest) + + ENDFOREACH(i) + + ELSE("${libname}" STREQUAL "NAMES") + + # just one name + LIST(APPEND libnames_release "${libname}") + LIST(APPEND libnames_debug "${libname}${dgb_postfix}") + + SET(newargs ${args}) + + ENDIF("${libname}" STREQUAL "NAMES") + + # search the release lib + FIND_LIBRARY(${var_name}_RELEASE + NAMES ${libnames_release} + ${newargs} + ) + + # search the debug lib + FIND_LIBRARY(${var_name}_DEBUG + NAMES ${libnames_debug} + ${newargs} + ) + + IF(${var_name}_RELEASE AND ${var_name}_DEBUG) + + # both libs found + SET(${var_name} optimized ${${var_name}_RELEASE} + debug ${${var_name}_DEBUG}) + + ELSE(${var_name}_RELEASE AND ${var_name}_DEBUG) + + IF(${var_name}_RELEASE) + + # only release found + SET(${var_name} ${${var_name}_RELEASE}) + + ELSE(${var_name}_RELEASE) + + # only debug (or nothing) found + SET(${var_name} ${${var_name}_DEBUG}) + + ENDIF(${var_name}_RELEASE) + + ENDIF(${var_name}_RELEASE AND ${var_name}_DEBUG) + + MARK_AS_ADVANCED(${var_name}_RELEASE) + MARK_AS_ADVANCED(${var_name}_DEBUG) + + ENDIF(NOT WIN32) + + ENDIF(NOT "${win32_dbg_postfix_name}" STREQUAL "WIN32_DEBUG_POSTFIX") + +ENDMACRO(FIND_LIBRARY_WITH_DEBUG) diff --git a/akonadi/cmake/modules/FindSharedMimeInfo.cmake b/akonadi/cmake/modules/FindSharedMimeInfo.cmake new file mode 100644 index 00000000..2c23e728 --- /dev/null +++ b/akonadi/cmake/modules/FindSharedMimeInfo.cmake @@ -0,0 +1,67 @@ +# - Try to find the shared-mime-info package +# Once done this will define +# +# SharedMimeInfo_FOUND - system has the shared-mime-info package +# UPDATE_MIME_DATABASE_EXECUTABLE - the update-mime-database executable + +# Copyright (c) 2007, Pino Toscano, +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +# the minimum version of shared-mime-database we require +if (NOT SHARED_MIME_INFO_MINIMUM_VERSION) + set(SHARED_MIME_INFO_MINIMUM_VERSION "0.18") +endif (NOT SHARED_MIME_INFO_MINIMUM_VERSION) + +if (UPDATE_MIME_DATABASE_EXECUTABLE) + + # in cache already + set(SharedMimeInfo_FOUND TRUE) + +else (UPDATE_MIME_DATABASE_EXECUTABLE) + + include (MacroEnsureVersion) + + find_program (UPDATE_MIME_DATABASE_EXECUTABLE NAMES update-mime-database) + + if (UPDATE_MIME_DATABASE_EXECUTABLE) + + exec_program (${UPDATE_MIME_DATABASE_EXECUTABLE} ARGS -v RETURN_VALUE _null OUTPUT_VARIABLE _smiVersionRaw) + + string(REGEX REPLACE "update-mime-database \\([a-zA-Z\\-]+\\) ([0-9]\\.[0-9]+).*" + "\\1" smiVersion "${_smiVersionRaw}") + set (SharedMimeInfo_FOUND TRUE) + endif (UPDATE_MIME_DATABASE_EXECUTABLE) + + if (SharedMimeInfo_FOUND) + if (NOT SharedMimeInfo_FIND_QUIETLY) + message(STATUS "Found shared-mime-info version: ${smiVersion}") + macro_ensure_version(${SHARED_MIME_INFO_MINIMUM_VERSION} ${smiVersion} _smiVersion_OK) + if (NOT _smiVersion_OK) + message(FATAL_ERROR "The found version of shared-mime-info (${smiVersion}) is below the minimum required (${SHARED_MIME_INFO_MINIMUM_VERSION})") + endif (NOT _smiVersion_OK) + + endif (NOT SharedMimeInfo_FIND_QUIETLY) + else (SharedMimeInfo_FOUND) + if (SharedMimeInfo_FIND_REQUIRED) + message(FATAL_ERROR "Could NOT find shared-mime-info. See http://freedesktop.org/wiki/Software/shared-mime-info.") + endif (SharedMimeInfo_FIND_REQUIRED) + endif (SharedMimeInfo_FOUND) + +endif (UPDATE_MIME_DATABASE_EXECUTABLE) + +macro(UPDATE_XDG_MIMETYPES _path) + get_filename_component(_xdgmimeDir "${_path}" NAME) + if("${_xdgmimeDir}" STREQUAL packages ) + get_filename_component(_xdgmimeDir "${_path}" PATH) + else("${_xdgmimeDir}" STREQUAL packages ) + set(_xdgmimeDir "${_path}") + endif("${_xdgmimeDir}" STREQUAL packages ) + install(CODE " +set(DESTDIR_VALUE \"\$ENV{DESTDIR}\") +if (NOT DESTDIR_VALUE) + execute_process(COMMAND ${UPDATE_MIME_DATABASE_EXECUTABLE} ${_xdgmimeDir}) +endif (NOT DESTDIR_VALUE) +") +endmacro (UPDATE_XDG_MIMETYPES) diff --git a/akonadi/cmake/modules/FindSoprano.cmake b/akonadi/cmake/modules/FindSoprano.cmake new file mode 100644 index 00000000..6ce5f49d --- /dev/null +++ b/akonadi/cmake/modules/FindSoprano.cmake @@ -0,0 +1,209 @@ +# +# Find an installation of Soprano +# +# Sets the following variables: +# Soprano_FOUND - true is Soprano has been found +# SOPRANO_INCLUDE_DIR - The include directory +# SOPRANO_LIBRARIES - The Soprano core library to link to (libsoprano) +# SOPRANO_INDEX_LIBRARIES - The Soprano index library (libsopranoindex) +# SOPRANO_CLIENT_LIBRARIES - The Soprano client library (libsopranoclient) +# SOPRANO_SERVER_LIBRARIES - The Soprano server library (libsopranoserver) +# SOPRANO_VERSION - The Soprano version (string value) +# +# SOPRANO_PLUGIN_NQUADPARSER_FOUND - true if the nquadparser plugin is found +# SOPRANO_PLUGIN_NQUADSERIALIZER_FOUND - true if the nquadserializer plugin is found +# SOPRANO_PLUGIN_RAPTORPARSER_FOUND - true if the raptorparser plugin is found +# SOPRANO_PLUGIN_RAPTORSERIALIZER_FOUND - true if the raptorserializer plugin is found +# SOPRANO_PLUGIN_REDLANDBACKEND_FOUND - true if the redlandbackend plugin is found +# SOPRANO_PLUGIN_SESAME2BACKEND_FOUND - true if the sesame2backend plugin is found +# SOPRANO_PLUGIN_VIRTUOSOBACKEND_FOUND - true if the virtuosobackend plugin is found +# +# Options: +# Set SOPRANO_MIN_VERSION to set the minimum required Soprano version (default: 1.99) +# + +# Copyright (c) 2008, Sebastian Trueg, +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + + include(FindLibraryWithDebug) + + # have packageconfig set variables to find Soprano: + # package config dirs are used as secondary search paths after install_dir + find_package(PkgConfig) + if (PKG_CONFIG_FOUND) + pkg_check_modules(Soprano_PKGCONF soprano) + endif() + + find_path(SOPRANO_INCLUDE_DIR + NAMES + soprano/soprano.h + HINTS + ${INCLUDE_INSTALL_DIR} + ${Soprano_PKGCONF_INCLUDE_DIRS} + ) + + find_library_with_debug(SOPRANO_INDEX_LIBRARIES + WIN32_DEBUG_POSTFIX d + NAMES + sopranoindex + HINTS + ${LIB_INSTALL_DIR} + ${Soprano_PKGCONF_LIBRARY_DIRS} + ) + + find_library_with_debug(SOPRANO_CLIENT_LIBRARIES + WIN32_DEBUG_POSTFIX d + NAMES + sopranoclient + HINTS + ${LIB_INSTALL_DIR} + ${Soprano_PKGCONF_LIBRARY_DIRS} + ) + + find_library_with_debug(SOPRANO_LIBRARIES + WIN32_DEBUG_POSTFIX d + NAMES soprano + HINTS + ${LIB_INSTALL_DIR} + ${Soprano_PKGCONF_LIBRARY_DIRS} + ) + + find_library_with_debug(SOPRANO_SERVER_LIBRARIES + WIN32_DEBUG_POSTFIX d + NAMES + sopranoserver + HINTS + ${LIB_INSTALL_DIR} + ${Soprano_PKGCONF_LIBRARY_DIRS} + ) + + # check for all the libs as required to make sure that we do not try to compile with an old version + + if(SOPRANO_INCLUDE_DIR AND SOPRANO_LIBRARIES) + set(Soprano_FOUND TRUE) + endif(SOPRANO_INCLUDE_DIR AND SOPRANO_LIBRARIES) + + if(Soprano_FOUND AND SOPRANO_INDEX_LIBRARIES) + set(SopranoIndex_FOUND TRUE) + endif(Soprano_FOUND AND SOPRANO_INDEX_LIBRARIES) + + if(Soprano_FOUND AND SOPRANO_CLIENT_LIBRARIES) + set(SopranoClient_FOUND TRUE) + endif(Soprano_FOUND AND SOPRANO_CLIENT_LIBRARIES) + + if(Soprano_FOUND AND SOPRANO_SERVER_LIBRARIES) + set(SopranoServer_FOUND TRUE) + endif(Soprano_FOUND AND SOPRANO_SERVER_LIBRARIES) + + # check Soprano version + + # We set a default for the minimum required version to be backwards compatible + if(NOT SOPRANO_MIN_VERSION) + set(SOPRANO_MIN_VERSION "1.99") + endif(NOT SOPRANO_MIN_VERSION) + + if(Soprano_FOUND) + file(READ ${SOPRANO_INCLUDE_DIR}/soprano/version.h SOPRANO_VERSION_CONTENT) + string(REGEX MATCH "SOPRANO_VERSION_STRING \".*\"\n" SOPRANO_VERSION_MATCH ${SOPRANO_VERSION_CONTENT}) + if(SOPRANO_VERSION_MATCH) + string(REGEX REPLACE "SOPRANO_VERSION_STRING \"(.*)\"\n" "\\1" SOPRANO_VERSION ${SOPRANO_VERSION_MATCH}) + if(SOPRANO_VERSION STRLESS "${SOPRANO_MIN_VERSION}") + set(Soprano_FOUND FALSE) + if(Soprano_FIND_REQUIRED) + message(FATAL_ERROR "Soprano version ${SOPRANO_VERSION} is too old. Please install ${SOPRANO_MIN_VERSION} or newer") + else(Soprano_FIND_REQUIRED) + message(STATUS "Soprano version ${SOPRANO_VERSION} is too old. Please install ${SOPRANO_MIN_VERSION} or newer") + endif(Soprano_FIND_REQUIRED) + endif(SOPRANO_VERSION STRLESS "${SOPRANO_MIN_VERSION}") + endif(SOPRANO_VERSION_MATCH) + endif(Soprano_FOUND) + + + #look for parser plugins + if(Soprano_FOUND) + find_path(SOPRANO_PLUGIN_DIR + NAMES + soprano/plugins + PATHS + ${SOPRANO_INCLUDE_DIR}/../share + ${SHARE_INSTALL_PREFIX} + /usr/share + /usr/local/share + NO_DEFAULT_PATH + NO_SYSTEM_ENVIRONMENT_PATH + ) + set(SOPRANO_PLUGIN_DIR "${SOPRANO_PLUGIN_DIR}/soprano/plugins") + + if(EXISTS ${SOPRANO_PLUGIN_DIR}/nquadparser.desktop) + set(SOPRANO_PLUGIN_NQUADPARSER_FOUND TRUE) + set(_plugins "${_plugins} nquadparser") + endif(EXISTS ${SOPRANO_PLUGIN_DIR}/nquadparser.desktop) + + if(EXISTS ${SOPRANO_PLUGIN_DIR}/nquadserializer.desktop) + set(SOPRANO_PLUGIN_NQUADSERIALIZER_FOUND TRUE) + set(_plugins "${_plugins} nquadserializer") + endif(EXISTS ${SOPRANO_PLUGIN_DIR}/nquadserializer.desktop) + + if(EXISTS ${SOPRANO_PLUGIN_DIR}/raptorparser.desktop) + set(SOPRANO_PLUGIN_RAPTORPARSER_FOUND TRUE) + set(_plugins "${_plugins} raptorparser") + endif(EXISTS ${SOPRANO_PLUGIN_DIR}/raptorparser.desktop) + + if(EXISTS ${SOPRANO_PLUGIN_DIR}/raptorserializer.desktop) + set(SOPRANO_PLUGIN_RAPTORSERIALIZER_FOUND TRUE) + set(_plugins "${_plugins} raptorserializer") + endif(EXISTS ${SOPRANO_PLUGIN_DIR}/raptorserializer.desktop) + + if(EXISTS ${SOPRANO_PLUGIN_DIR}/redlandbackend.desktop) + set(SOPRANO_PLUGIN_REDLANDBACKEND_FOUND TRUE) + set(_plugins "${_plugins} redlandbackend") + endif(EXISTS ${SOPRANO_PLUGIN_DIR}/redlandbackend.desktop) + + if(EXISTS ${SOPRANO_PLUGIN_DIR}/sesame2backend.desktop) + set(SOPRANO_PLUGIN_SESAME2BACKEND_FOUND TRUE) + set(_plugins "${_plugins} sesame2backend") + endif(EXISTS ${SOPRANO_PLUGIN_DIR}/sesame2backend.desktop) + + if(EXISTS ${SOPRANO_PLUGIN_DIR}/virtuosobackend.desktop) + set(SOPRANO_PLUGIN_VIRTUOSOBACKEND_FOUND TRUE) + set(_plugins "${_plugins} virtuosobackend") + endif(EXISTS ${SOPRANO_PLUGIN_DIR}/virtuosobackend.desktop) + + endif(Soprano_FOUND) + + if(Soprano_FOUND) + if(NOT Soprano_FIND_QUIETLY) + message(STATUS "Found Soprano version ${SOPRANO_VERSION}: ${SOPRANO_LIBRARIES}") + message(STATUS "Found Soprano includes: ${SOPRANO_INCLUDE_DIR}") + message(STATUS "Found Soprano Index: ${SOPRANO_INDEX_LIBRARIES}") + message(STATUS "Found Soprano Client: ${SOPRANO_CLIENT_LIBRARIES}") + message(STATUS "Found Soprano Plugin Dir: ${SOPRANO_PLUGIN_DIR}") + message(STATUS "Found Soprano Plugins:${_plugins}") + endif(NOT Soprano_FIND_QUIETLY) + else(Soprano_FOUND) + if(Soprano_FIND_REQUIRED) + if(NOT SOPRANO_INCLUDE_DIR) + message(FATAL_ERROR "Could not find Soprano includes.") + endif(NOT SOPRANO_INCLUDE_DIR) + if(NOT SOPRANO_LIBRARIES) + message(FATAL_ERROR "Could not find Soprano library.") + endif(NOT SOPRANO_LIBRARIES) + else(Soprano_FIND_REQUIRED) + if(NOT SOPRANO_INCLUDE_DIR) + message(STATUS "Could not find Soprano includes.") + endif(NOT SOPRANO_INCLUDE_DIR) + if(NOT SOPRANO_LIBRARIES) + message(STATUS "Could not find Soprano library.") + endif(NOT SOPRANO_LIBRARIES) + endif(Soprano_FIND_REQUIRED) + endif(Soprano_FOUND) + +mark_as_advanced(SOPRANO_CLIENT_LIBRARIES + SOPRANO_INDEX_LIBRARIES + SOPRANO_LIBRARIES + SOPRANO_SERVER_LIBRARIES + SOPRANO_INCLUDE_DIR + SOPRANO_PLUGIN_DIR) diff --git a/akonadi/cmake/modules/FindSqlite.cmake b/akonadi/cmake/modules/FindSqlite.cmake new file mode 100644 index 00000000..ad8cdb4d --- /dev/null +++ b/akonadi/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 INCLUDE_DIRECTORIES ${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/akonadi/cmake/modules/InstallSettings.cmake b/akonadi/cmake/modules/InstallSettings.cmake new file mode 100644 index 00000000..b8a79bbc --- /dev/null +++ b/akonadi/cmake/modules/InstallSettings.cmake @@ -0,0 +1,90 @@ +# Copyright (c) 2008 Kevin Krammer +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +set (LIB_SUFFIX "" CACHE STRING "Define suffix of directory name (32/64)") + +if (WIN32) + # use relative install prefix to avoid hardcoded install paths in cmake_install.cmake files + + set (EXEC_INSTALL_PREFIX "") # Base directory for executables and libraries + + ## ${CMAKE_INSTALL_PREFIX}/* + set (BIN_INSTALL_DIR "bin") # The install dir for executables (default ${EXEC_INSTALL_PREFIX}/bin) + set (INCLUDE_INSTALL_DIR "include") # The subdirectory to the header prefix + set (LIB_INSTALL_DIR "lib${LIB_SUFFIX}") # The subdirectory relative to the install prefix where libraries will be installed (default is ${EXEC_INSTALL_PREFIX}/lib${LIB_SUFFIX}) + set (SHARE_INSTALL_PREFIX "share") # Base directory for files which go to share/ + + ## ${CMAKE_INSTALL_PREFIX}/share/* + set (CONFIG_INSTALL_DIR "share/config") # The config file install dir + set (DBUS_INTERFACES_INSTALL_DIR "share/dbus-1/interfaces") # The DBus interfaces install dir (default ${SHARE_INSTALL_PREFIX}/dbus-1/interfaces)") + set (DBUS_SERVICES_INSTALL_DIR "share/dbus-1/services") # The DBus services install dir (default ${SHARE_INSTALL_PREFIX}/dbus-1/services)") + set (XDG_MIME_INSTALL_DIR "share/mime/packages") # The install dir for the xdg mimetypes + +else (WIN32) + # this macro implements some very special logic how to deal with the cache + # by default the various install locations inherit their value from theit "parent" variable + # so if you set CMAKE_INSTALL_PREFIX, then EXEC_INSTALL_PREFIX, PLUGIN_INSTALL_DIR will + # calculate their value by appending subdirs to CMAKE_INSTALL_PREFIX + # this would work completely without using the cache. + # but if somebody wants e.g. a different EXEC_INSTALL_PREFIX this value has to go into + # the cache, otherwise it will be forgotten on the next cmake run. + # Once a variable is in the cache, it doesn't depend on its "parent" variables + # anymore and you can only change it by editing it directly. + # this macro helps in this regard, because as long as you don't set one of the + # variables explicitely to some location, it will always calculate its value from its + # parents. So modifying CMAKE_INSTALL_PREFIX later on will have the desired effect. + # But once you decide to set e.g. EXEC_INSTALL_PREFIX to some special location + # this will go into the cache and it will no longer depend on CMAKE_INSTALL_PREFIX. + macro(_SET_FANCY _var _value _comment) + SET (predefinedvalue "${_value}") + + if (NOT DEFINED ${_var}) + SET (${_var} ${predefinedvalue}) + else (NOT DEFINED ${_var}) + SET (${_var} "${${_var}}" CACHE PATH "${_comment}") + endif (NOT DEFINED ${_var}) + endmacro() + + + _set_fancy(EXEC_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" "Base directory for executables and libraries") + + ## ${CMAKE_INSTALL_PREFIX}/* + _set_fancy(BIN_INSTALL_DIR "${EXEC_INSTALL_PREFIX}/bin" "The install dir for executables (default ${EXEC_INSTALL_PREFIX}/bin)") + _set_fancy(INCLUDE_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/include" "The subdirectory to the header prefix") + _set_fancy(LIB_INSTALL_DIR "${EXEC_INSTALL_PREFIX}/lib${LIB_SUFFIX}" "The subdirectory relative to the install prefix where libraries will be installed (default is ${EXEC_INSTALL_PREFIX}/lib${LIB_SUFFIX})") + _set_fancy(SHARE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}/share" "Base directory for files which go to share/") + + + ## ${CMAKE_INSTALL_PREFIX}/share/* + _set_fancy(CONFIG_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/config" "The config file install dir") + _set_fancy(DBUS_INTERFACES_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/dbus-1/interfaces" "The DBus interfaces install dir (default ${SHARE_INSTALL_PREFIX}/dbus-1/interfaces)") + _set_fancy(DBUS_SERVICES_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/dbus-1/services" "The DBus services install dir (default ${SHARE_INSTALL_PREFIX}/dbus-1/services)") + _set_fancy(XDG_MIME_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/mime/packages" "The install dir for the xdg mimetypes") + +endif (WIN32) + +# The INSTALL_TARGETS_DEFAULT_ARGS variable should be used when libraries are installed. +# The arguments are also ok for regular executables, i.e. executables which don't go +# into sbin/ or libexec/, but for installing executables the basic syntax +# INSTALL(TARGETS kate DESTINATION "${BIN_INSTALL_DIR}") +# is enough, so using this variable there doesn't help a lot. +# The variable must not be used for installing plugins. +# Usage is like this: +# install(TARGETS kdecore kdeui ${INSTALL_TARGETS_DEFAULT_ARGS}) +# +# This will install libraries correctly under UNIX, OSX and Windows (i.e. dll's go +# into bin/. +# Later on it will be possible to extend this for installing OSX frameworks +# The COMPONENT Devel argument has the effect that static libraries belong to the +# "Devel" install component. If we use this also for all install() commands +# for header files, it will be possible to install +# -everything: make install OR cmake -P cmake_install.cmake +# -only the development files: cmake -DCOMPONENT=Devel -P cmake_install.cmake +# -everything except the development files: cmake -DCOMPONENT=Unspecified -P cmake_install.cmake +# This can then also be used for packaging with cpack. + +SET (INSTALL_TARGETS_DEFAULT_ARGS RUNTIME DESTINATION "${BIN_INSTALL_DIR}" +LIBRARY DESTINATION "${LIB_INSTALL_DIR}" +ARCHIVE DESTINATION "${LIB_INSTALL_DIR}" COMPONENT Devel) diff --git a/akonadi/cmake/modules/MacroEnsureVersion.cmake b/akonadi/cmake/modules/MacroEnsureVersion.cmake new file mode 100644 index 00000000..6797e5b7 --- /dev/null +++ b/akonadi/cmake/modules/MacroEnsureVersion.cmake @@ -0,0 +1,117 @@ +# This file defines the following macros for developers to use in ensuring +# that installed software is of the right version: +# +# MACRO_ENSURE_VERSION - test that a version number is greater than +# or equal to some minimum +# MACRO_ENSURE_VERSION_RANGE - test that a version number is greater than +# or equal to some minimum and less than some +# maximum +# MACRO_ENSURE_VERSION2 - deprecated, do not use in new code +# + +# MACRO_ENSURE_VERSION +# This macro compares version numbers of the form "x.y.z" or "x.y" +# MACRO_ENSURE_VERSION( FOO_MIN_VERSION FOO_VERSION_FOUND FOO_VERSION_OK) +# will set FOO_VERSION_OK to true if FOO_VERSION_FOUND >= FOO_MIN_VERSION +# Leading and trailing text is ok, e.g. +# MACRO_ENSURE_VERSION( "2.5.31" "flex 2.5.4a" VERSION_OK) +# which means 2.5.31 is required and "flex 2.5.4a" is what was found on the system + +# Copyright (c) 2006, David Faure, +# Copyright (c) 2007, Will Stephenson +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +# MACRO_ENSURE_VERSION_RANGE +# This macro ensures that a version number of the form +# "x.y.z" or "x.y" falls within a range defined by +# min_version <= found_version < max_version. +# If this expression holds, FOO_VERSION_OK will be set TRUE +# +# Example: MACRO_ENSURE_VERSION_RANGE3( "0.1.0" ${FOOCODE_VERSION} "0.7.0" FOO_VERSION_OK ) +# +# This macro will break silently if any of x,y,z are greater than 100. +# +# Copyright (c) 2007, Will Stephenson +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +# NORMALIZE_VERSION +# Helper macro to convert version numbers of the form "x.y.z" +# to an integer equal to 10^4 * x + 10^2 * y + z +# +# This macro will break silently if any of x,y,z are greater than 100. +# +# Copyright (c) 2006, David Faure, +# Copyright (c) 2007, Will Stephenson +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +# CHECK_RANGE_INCLUSIVE_LOWER +# Helper macro to check whether x <= y < z +# +# Copyright (c) 2007, Will Stephenson +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + +MACRO(NORMALIZE_VERSION _requested_version _normalized_version) + STRING(REGEX MATCH "[^0-9]*[0-9]+\\.[0-9]+\\.[0-9]+.*" _threePartMatch "${_requested_version}") + if (_threePartMatch) + # parse the parts of the version string + STRING(REGEX REPLACE "[^0-9]*([0-9]+)\\.[0-9]+\\.[0-9]+.*" "\\1" _major_vers "${_requested_version}") + STRING(REGEX REPLACE "[^0-9]*[0-9]+\\.([0-9]+)\\.[0-9]+.*" "\\1" _minor_vers "${_requested_version}") + STRING(REGEX REPLACE "[^0-9]*[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" _patch_vers "${_requested_version}") + else (_threePartMatch) + STRING(REGEX REPLACE "([0-9]+)\\.[0-9]+" "\\1" _major_vers "${_requested_version}") + STRING(REGEX REPLACE "[0-9]+\\.([0-9]+)" "\\1" _minor_vers "${_requested_version}") + set(_patch_vers "0") + endif (_threePartMatch) + + # compute an overall version number which can be compared at once + MATH(EXPR ${_normalized_version} "${_major_vers}*10000 + ${_minor_vers}*100 + ${_patch_vers}") +ENDMACRO(NORMALIZE_VERSION) + +MACRO(MACRO_CHECK_RANGE_INCLUSIVE_LOWER _lower_limit _value _upper_limit _ok) + if (${_value} LESS ${_lower_limit}) + set( ${_ok} FALSE ) + elseif (${_value} EQUAL ${_lower_limit}) + set( ${_ok} TRUE ) + elseif (${_value} EQUAL ${_upper_limit}) + set( ${_ok} FALSE ) + elseif (${_value} GREATER ${_upper_limit}) + set( ${_ok} FALSE ) + else (${_value} LESS ${_lower_limit}) + set( ${_ok} TRUE ) + endif (${_value} LESS ${_lower_limit}) +ENDMACRO(MACRO_CHECK_RANGE_INCLUSIVE_LOWER) + +MACRO(MACRO_ENSURE_VERSION requested_version found_version var_too_old) + NORMALIZE_VERSION( ${requested_version} req_vers_num ) + NORMALIZE_VERSION( ${found_version} found_vers_num ) + + if (found_vers_num LESS req_vers_num) + set( ${var_too_old} FALSE ) + else (found_vers_num LESS req_vers_num) + set( ${var_too_old} TRUE ) + endif (found_vers_num LESS req_vers_num) + +ENDMACRO(MACRO_ENSURE_VERSION) + +MACRO(MACRO_ENSURE_VERSION2 requested_version2 found_version2 var_too_old2) + MACRO_ENSURE_VERSION( ${requested_version2} ${found_version2} ${var_too_old2}) +ENDMACRO(MACRO_ENSURE_VERSION2) + +MACRO(MACRO_ENSURE_VERSION_RANGE min_version found_version max_version var_ok) + NORMALIZE_VERSION( ${min_version} req_vers_num ) + NORMALIZE_VERSION( ${found_version} found_vers_num ) + NORMALIZE_VERSION( ${max_version} max_vers_num ) + + MACRO_CHECK_RANGE_INCLUSIVE_LOWER( ${req_vers_num} ${found_vers_num} ${max_vers_num} ${var_ok}) +ENDMACRO(MACRO_ENSURE_VERSION_RANGE) + + diff --git a/akonadi/cmake/modules/cmake-copyright.txt b/akonadi/cmake/modules/cmake-copyright.txt new file mode 100644 index 00000000..35f7e4b6 --- /dev/null +++ b/akonadi/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/akonadi/config-akonadi.h.cmake b/akonadi/config-akonadi.h.cmake new file mode 100644 index 00000000..37223f79 --- /dev/null +++ b/akonadi/config-akonadi.h.cmake @@ -0,0 +1,14 @@ +#cmakedefine01 Backtrace_FOUND +#if Backtrace_FOUND +# include <@Backtrace_HEADER@> +#endif + +#cmakedefine HAVE_UNISTD_H 1 +#cmakedefine HAVE_SOPRANO 1 + +#define AKONADI_VERSION_MAJOR @AKONADI_VERSION_MAJOR@ +#define AKONADI_VERSION_MINOR @AKONADI_VERSION_MINOR@ +#define AKONADI_VERSION_PATCH @AKONADI_VERSION_PATCH@ +#define AKONADI_VERSION_STRING "@AKONADI_VERSION_STRING@" + +#define AKONADI_DATABASE_BACKEND "@AKONADI_DATABASE_BACKEND@" diff --git a/akonadi/interfaces/CMakeLists.txt b/akonadi/interfaces/CMakeLists.txt new file mode 100644 index 00000000..3bda9600 --- /dev/null +++ b/akonadi/interfaces/CMakeLists.txt @@ -0,0 +1,20 @@ +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 ${DBUS_INTERFACES_INSTALL_DIR}) + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.Agent.Control.xml b/akonadi/interfaces/org.freedesktop.Akonadi.Agent.Control.xml new file mode 100644 index 00000000..9aab3c35 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.Agent.Control.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.Agent.Search.xml b/akonadi/interfaces/org.freedesktop.Akonadi.Agent.Search.xml new file mode 100644 index 00000000..35a18fb9 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.Agent.Search.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.Agent.Status.xml b/akonadi/interfaces/org.freedesktop.Akonadi.Agent.Status.xml new file mode 100644 index 00000000..3641b031 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.Agent.Status.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.AgentManager.xml b/akonadi/interfaces/org.freedesktop.Akonadi.AgentManager.xml new file mode 100644 index 00000000..b212f1da --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.AgentManager.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml b/akonadi/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml new file mode 100644 index 00000000..4110421a --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.AgentServer.xml b/akonadi/interfaces/org.freedesktop.Akonadi.AgentServer.xml new file mode 100644 index 00000000..01540e5e --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.AgentServer.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.ControlManager.xml b/akonadi/interfaces/org.freedesktop.Akonadi.ControlManager.xml new file mode 100644 index 00000000..07ad2258 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.ControlManager.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.NotificationManager.xml b/akonadi/interfaces/org.freedesktop.Akonadi.NotificationManager.xml new file mode 100644 index 00000000..84855b35 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.NotificationManager.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.NotificationSource.xml b/akonadi/interfaces/org.freedesktop.Akonadi.NotificationSource.xml new file mode 100644 index 00000000..15c8cbaa --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.NotificationSource.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.Preprocessor.xml b/akonadi/interfaces/org.freedesktop.Akonadi.Preprocessor.xml new file mode 100644 index 00000000..25352946 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.Preprocessor.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml b/akonadi/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml new file mode 100644 index 00000000..bf1ef662 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.Resource.xml b/akonadi/interfaces/org.freedesktop.Akonadi.Resource.xml new file mode 100644 index 00000000..528340a3 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.Resource.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.ResourceManager.xml b/akonadi/interfaces/org.freedesktop.Akonadi.ResourceManager.xml new file mode 100644 index 00000000..3f5b62ce --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.ResourceManager.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.SearchManager.xml b/akonadi/interfaces/org.freedesktop.Akonadi.SearchManager.xml new file mode 100644 index 00000000..b2ba6ba4 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.SearchManager.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.Server.xml b/akonadi/interfaces/org.freedesktop.Akonadi.Server.xml new file mode 100644 index 00000000..7c6da49b --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.Server.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml b/akonadi/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml new file mode 100644 index 00000000..5b02450f --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.Tracer.xml b/akonadi/interfaces/org.freedesktop.Akonadi.Tracer.xml new file mode 100644 index 00000000..09e88a74 --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.Tracer.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/akonadi/interfaces/org.freedesktop.Akonadi.TracerNotification.xml b/akonadi/interfaces/org.freedesktop.Akonadi.TracerNotification.xml new file mode 100644 index 00000000..1e290b6e --- /dev/null +++ b/akonadi/interfaces/org.freedesktop.Akonadi.TracerNotification.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/lgpl-license b/akonadi/lgpl-license new file mode 100644 index 00000000..ec47efc0 --- /dev/null +++ b/akonadi/lgpl-license @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, 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 of the License, or (at your option) any later version. + + This library is distributed in the hope that 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 + +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/akonadi/libs/CMakeLists.txt b/akonadi/libs/CMakeLists.txt new file mode 100644 index 00000000..de6ab0d1 --- /dev/null +++ b/akonadi/libs/CMakeLists.txt @@ -0,0 +1,40 @@ +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_ENABLE_EXCEPTIONS}") + +configure_file(akonadiprotocolinternals_export.h.in ${Akonadi_BINARY_DIR}/akonadiprotocolinternals_export.h) + +# libakonadiprotocolinternals +set(akonadiprotocolinternals_srcs + imapparser.cpp + imapset.cpp + notificationmessage.cpp + notificationmessagev2.cpp + notificationmessagev3.cpp + xdgbasedirs.cpp +) + +add_library(akonadiprotocolinternals ${LIBRARY_TYPE} ${akonadiprotocolinternals_srcs}) + +target_link_libraries(akonadiprotocolinternals ${QT_QTCORE_LIBRARY} ${QT_QTDBUS_LIBRARY}) +set_target_properties(akonadiprotocolinternals PROPERTIES + VERSION ${AKONADI_VERSION} + SOVERSION ${AKONADI_SOVERSION} + DEFINE_SYMBOL MAKE_AKONADIPROTOCOLINTERNALS_LIB + ) + +install(TARGETS akonadiprotocolinternals EXPORT akonadiLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) + +install(FILES + ${Akonadi_BINARY_DIR}/akonadiprotocolinternals_export.h + imapparser_p.h + imapset_p.h + notificationmessage_p.h + notificationmessagev2_p.h + notificationmessagev3_p.h + protocol_p.h + xdgbasedirs_p.h + capabilities_p.h + DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi/private +) + +add_subdirectory(tests) + diff --git a/akonadi/libs/akonadiprotocolinternals_export.h.in b/akonadi/libs/akonadiprotocolinternals_export.h.in new file mode 100644 index 00000000..80dda6cc --- /dev/null +++ b/akonadi/libs/akonadiprotocolinternals_export.h.in @@ -0,0 +1,40 @@ +/* This file is part of the KDE project + Copyright (C) 2007 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 AKONADIPROTOCOLINTERNALS_EXPORT_H +#define AKONADIPROTOCOLINTERNALS_EXPORT_H + +#include + +#cmakedefine AKONADI_STATIC_LIBS + +#ifndef AKONADIPROTOCOLINTERNALS_EXPORT +# if defined(AKONADI_STATIC_LIBS) + /* No export/import for static libraries */ +# define AKONADIPROTOCOLINTERNALS_EXPORT +# elif defined(MAKE_AKONADIPROTOCOLINTERNALS_LIB) + /* We are building this library */ +# define AKONADIPROTOCOLINTERNALS_EXPORT Q_DECL_EXPORT +# else + /* We are using this library */ +# define AKONADIPROTOCOLINTERNALS_EXPORT Q_DECL_IMPORT +# endif +#endif + +#endif diff --git a/akonadi/libs/capabilities_p.h b/akonadi/libs/capabilities_p.h new file mode 100644 index 00000000..190361df --- /dev/null +++ b/akonadi/libs/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/akonadi/libs/imapparser.cpp b/akonadi/libs/imapparser.cpp new file mode 100644 index 00000000..9b437e2f --- /dev/null +++ b/akonadi/libs/imapparser.cpp @@ -0,0 +1,698 @@ +/* + 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; + } + + 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; + 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; + } + } + + // unquoted string + else { + 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() ) { + return QByteArray( "\"\"" ); + } + + 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; + 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/akonadi/libs/imapparser_p.h b/akonadi/libs/imapparser_p.h new file mode 100644 index 00000000..7f9298ac --- /dev/null +++ b/akonadi/libs/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 "akonadiprotocolinternals_export.h" + +#include "imapset_p.h" + +#include +#include +#include + +namespace Akonadi { + +/** + Parser for IMAP messages. +*/ +class AKONADIPROTOCOLINTERNALS_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/akonadi/libs/imapset.cpp b/akonadi/libs/imapset.cpp new file mode 100644 index 00000000..af4cd733 --- /dev/null +++ b/akonadi/libs/imapset.cpp @@ -0,0 +1,270 @@ +/* + 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 "imapparser_p.h" + +#include + +#include + +using 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( const ImapSet &other ) + : d( other.d ) +{ +} + +ImapSet::~ImapSet() +{ +} + +ImapSet &ImapSet::operator=( const ImapSet &other ) +{ + if ( this != &other ) { + d = other.d; + } + + return *this; +} + +void ImapSet::add( const QList &values ) +{ + add( values.toVector() ); +} + +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 +{ + QList rv; + Q_FOREACH ( const ImapInterval &interval, d->intervals ) { + rv << interval.toImapSequence(); + } + + return ImapParser::join( rv, "," ); +} + +ImapInterval::List ImapSet::intervals() const +{ + return d->intervals; +} + +bool ImapSet::isEmpty() const +{ + return d->intervals.isEmpty(); +} + +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/akonadi/libs/imapset_p.h b/akonadi/libs/imapset_p.h new file mode 100644 index 00000000..5460a328 --- /dev/null +++ b/akonadi/libs/imapset_p.h @@ -0,0 +1,220 @@ +/* + 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 "akonadiprotocolinternals_export.h" + +#include +#include +#include +#include +#include + +namespace Akonadi { + +/** + Represents a single interval in an ImapSet. + This class is implicitly shared. +*/ +class AKONADIPROTOCOLINTERNALS_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; +}; + +/** + 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 AKONADIPROTOCOLINTERNALS_EXPORT ImapSet +{ + public: + /** + * Describes the ids stored in the set. + */ + typedef qint64 Id; + + /** + Constructs an empty set. + */ + ImapSet(); + + /** + Copy constructor. + */ + ImapSet( const ImapSet &other ); + + /** + Destructor. + */ + ~ImapSet(); + + /** + Assignment operator. + */ + ImapSet &operator=( const ImapSet &other ); + + /** + 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 + @deprecated Use the QVector version instead. + */ + void add( const QList &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; +}; + +} + +AKONADIPROTOCOLINTERNALS_EXPORT QDebug &operator<<( QDebug &d, const Akonadi::ImapInterval &interval ); +AKONADIPROTOCOLINTERNALS_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/akonadi/libs/notificationmessage.cpp b/akonadi/libs/notificationmessage.cpp new file mode 100644 index 00000000..e65fb7f5 --- /dev/null +++ b/akonadi/libs/notificationmessage.cpp @@ -0,0 +1,432 @@ +/* + 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 "notificationmessage_p.h" +#include "imapparser_p.h" + +#include +#include +#include + +using namespace Akonadi; + +class NotificationMessage::Private : public QSharedData +{ + public: + Private() + : QSharedData() + , type( NotificationMessage::InvalidType ) + , operation( NotificationMessage::InvalidOp ) + , uid( -1 ) + , parentCollection( -1 ) + , parentDestCollection( -1 ) + { + } + + Private( const Private &other ) + : QSharedData( other ) + { + sessionId = other.sessionId; + type = other.type; + operation = other.operation; + uid = other.uid; + remoteId = other.remoteId; + resource = other.resource; + destResource = other.destResource; + parentCollection = other.parentCollection; + parentDestCollection = other.parentDestCollection; + mimeType = other.mimeType; + parts = other.parts; + } + + bool compareWithoutOpAndParts( const Private &other ) const + { + return uid == other.uid + && type == other.type + && sessionId == other.sessionId + && remoteId == other.remoteId + && resource == other.resource + && destResource == other.destResource + && parentCollection == other.parentCollection + && parentDestCollection == other.parentDestCollection + && mimeType == other.mimeType; + } + + bool operator==( const Private &other ) const + { + return operation == other.operation && parts == other.parts && compareWithoutOpAndParts( other ); + } + + QByteArray sessionId; + NotificationMessage::Type type; + NotificationMessage::Operation operation; + Id uid; + QString remoteId; + QByteArray resource; + QByteArray destResource; + Id parentCollection; + Id parentDestCollection; + QString mimeType; + QSet parts; +}; + +NotificationMessage::NotificationMessage() + : d( new Private ) +{ +} + +NotificationMessage::NotificationMessage( const NotificationMessage &other ) + : d( other.d ) +{ +} + +NotificationMessage::~NotificationMessage() +{ +} + +NotificationMessage &NotificationMessage::operator=( const NotificationMessage &other ) +{ + if ( this != &other ) { + d = other.d; + } + + return *this; +} + +bool NotificationMessage::operator==( const NotificationMessage &other ) const +{ + return d == other.d; +} + +void NotificationMessage::registerDBusTypes() +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); +} + +QByteArray NotificationMessage::sessionId() const +{ + return d->sessionId; +} + +void NotificationMessage::setSessionId( const QByteArray &sessionId ) +{ + d->sessionId = sessionId; +} + +NotificationMessage::Type NotificationMessage::type() const +{ + return d->type; +} + +void NotificationMessage::setType( Type type ) +{ + d->type = type; +} + +NotificationMessage::Operation NotificationMessage::operation() const +{ + return d->operation; +} + +void NotificationMessage::setOperation( Operation operation ) +{ + d->operation = operation; +} + +NotificationMessage::Id NotificationMessage::uid() const +{ + return d->uid; +} + +void NotificationMessage::setUid( Id uid ) +{ + d->uid = uid; +} + +QString NotificationMessage::remoteId() const +{ + return d->remoteId; +} + +void NotificationMessage::setRemoteId( const QString &remoteId ) +{ + d->remoteId = remoteId; +} + +QByteArray NotificationMessage::resource() const +{ + return d->resource; +} + +void NotificationMessage::setResource( const QByteArray &resource ) +{ + d->resource = resource; +} + +NotificationMessage::Id NotificationMessage::parentCollection() const +{ + return d->parentCollection; +} + +NotificationMessage::Id NotificationMessage::parentDestCollection() const +{ + return d->parentDestCollection; +} + +void NotificationMessage::setParentCollection( Id parent ) +{ + d->parentCollection = parent; +} + +void NotificationMessage::setParentDestCollection( Id parent ) +{ + d->parentDestCollection = parent; +} + +void NotificationMessage::setDestinationResource( const QByteArray &destResource ) +{ + d->destResource = destResource; +} + +QByteArray NotificationMessage::destinationResource() const +{ + return d->destResource; +} + +QString NotificationMessage::mimeType() const +{ + return d->mimeType; +} + +void NotificationMessage::setMimeType( const QString &mimeType ) +{ + d->mimeType = mimeType; +} + +QSet NotificationMessage::itemParts() const +{ + return d->parts; +} + +void NotificationMessage::setItemParts( const QSet &parts ) +{ + d->parts = parts; +} + +QString NotificationMessage::toString() const +{ + QString rv; + // some tests before making the string + if ( type() == InvalidType ) { + return QLatin1String( "Error: Type is not set" ); + } + if ( uid() == -1 ) { + return QLatin1String( "Error: uid is not set" ); + } + if ( remoteId().isEmpty() ) { + return QLatin1String( "Error: remoteId is empty" ); + } + if ( operation() == InvalidOp ) { + return QLatin1String( "Error: operation is not set" ); + } + + switch ( type() ) { + case Item: + rv += QLatin1String( "Item " ); + break; + case Collection: + rv += QLatin1String( "Collection " ); + break; + case InvalidType: + // already done above + break; + } + + rv += QString::fromLatin1( "(%1, %2) " ).arg( uid() ).arg( remoteId() ); + + if ( parentCollection() >= 0 ) { + if ( parentDestCollection() >= 0 ) { + rv += QString::fromLatin1( "from " ); + } else { + rv += QString::fromLatin1( "in " ); + } + rv += QString::fromLatin1( "collection %1 " ).arg( parentCollection() ); + } else { + rv += QLatin1String( "unspecified parent collection " ); + } + + rv += QString::fromLatin1( "mimetype %1 " ).arg( mimeType().isEmpty() ? QLatin1String( "unknown" ) : mimeType() ); + + switch ( operation() ) { + case Add: + rv += QLatin1String( "added" ); + break; + case Modify: + rv += QLatin1String( "modified parts (" ); + rv += QString::fromLatin1( ImapParser::join( itemParts().toList(), ", " ) ); + rv += QLatin1String( ")" ); + break; + case Move: + rv += QLatin1String( "moved" ); + break; + case Remove: + rv += QLatin1String( "removed" ); + break; + case Link: + rv += QLatin1String( "linked" ); + break; + case Unlink: + rv += QLatin1String( "unlinked" ); + break; + case Subscribe: + rv += QLatin1String( "subscribed" ); + break; + case Unsubscribe: + rv += QLatin1String( "unsubscribed" ); + break; + case InvalidOp: + // already done above + break; + } + + if ( parentDestCollection() >= 0 ) { + rv += QString::fromLatin1( " to collection %1" ).arg( parentDestCollection() ); + } + + return rv; +} + +void NotificationMessage::appendAndCompress( NotificationMessage::List &list, const NotificationMessage &msg ) +{ + bool appended; + appendAndCompress( list, msg, &appended ); +} + +void NotificationMessage::appendAndCompress( NotificationMessage::List &list, const NotificationMessage &msg, bool *appended ) +{ + // fast-path for stuff that is not considered during O(n) compression below + if ( msg.operation() != Add && msg.operation() != Link && msg.operation() != Unlink && msg.operation() != Subscribe && msg.operation() != Unsubscribe && msg.operation() != Move ) { + NotificationMessage::List::Iterator end = list.end(); + for ( NotificationMessage::List::Iterator it = list.begin(); it != end; ) { + if ( msg.d.constData()->compareWithoutOpAndParts( *( ( *it ).d.constData() ) ) ) { + // same operation: merge changed parts and drop the new one + if ( msg.operation() == ( *it ).operation() ) { + ( *it ).setItemParts( ( *it ).itemParts() + msg.itemParts() ); + *appended = false; + return; + } + // new one is a modification, the existing one not, so drop the new one + else if ( msg.operation() == Modify ) { + *appended = false; + return; + } + // new on is a deletion, erase the existing modification ones (and keep going, in case there are more) + else if ( msg.operation() == Remove && ( *it ).operation() == Modify ) { + it = list.erase( it ); + end = list.end(); + } + // keep looking + else { + ++it; + } + } else { + ++it; + } + } + } + *appended = true; + list.append( msg ); +} + +QDBusArgument &operator<<( QDBusArgument &arg, const NotificationMessage &msg ) +{ + arg.beginStructure(); + arg << msg.sessionId(); + arg << msg.type(); + arg << msg.operation(); + arg << msg.uid(); + arg << msg.remoteId(); + arg << msg.resource(); + arg << msg.parentCollection(); + arg << msg.parentDestCollection(); + arg << msg.mimeType(); + + QStringList itemParts; + if ( msg.operation() == NotificationMessage::Move ) { + // encode destination resource in parts, as a backward compat hack + itemParts.push_back( QString::fromLatin1( msg.destinationResource() ) ); + } else { + Q_FOREACH ( const QByteArray &itemPart, msg.itemParts() ) { + itemParts.append( QString::fromLatin1( itemPart ) ); + } + } + + arg << itemParts; + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>( const QDBusArgument &arg, NotificationMessage &msg ) +{ + arg.beginStructure(); + QByteArray b; + arg >> b; + msg.setSessionId( b ); + int i; + arg >> i; + msg.setType( static_cast( i ) ); + arg >> i; + msg.setOperation( static_cast( i ) ); + NotificationMessage::Id id; + arg >> id; + msg.setUid( id ); + QString s; + arg >> s; + msg.setRemoteId( s ); + arg >> b; + msg.setResource( b ); + arg >> id; + msg.setParentCollection( id ); + arg >> id; + msg.setParentDestCollection( id ); + arg >> s; + msg.setMimeType( s ); + QStringList l; + arg >> l; + + QSet itemParts; + if ( msg.operation() == NotificationMessage::Move && l.size() >= 1 ) { + // decode destination resource, which is stored in parts as a backward compat hack + msg.setDestinationResource( l.first().toLatin1() ); + } else { + Q_FOREACH ( const QString &itemPart, l ) { + itemParts.insert( itemPart.toLatin1() ); + } + } + + msg.setItemParts( itemParts ); + arg.endStructure(); + return arg; +} + +uint qHash( const Akonadi::NotificationMessage &msg ) +{ + return qHash( msg.uid() + ( msg.type() << 31 ) + ( msg.operation() << 28 ) ); +} diff --git a/akonadi/libs/notificationmessage_p.h b/akonadi/libs/notificationmessage_p.h new file mode 100644 index 00000000..efb87601 --- /dev/null +++ b/akonadi/libs/notificationmessage_p.h @@ -0,0 +1,136 @@ +/* + 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_NOTIFICATIONMESSAGE_P_H +#define AKONADI_NOTIFICATIONMESSAGE_P_H + +#include "akonadiprotocolinternals_export.h" + +#include +#include +#include +#include + +namespace Akonadi { + +/** + @internal + Used for sending notification signals over DBus. + DBus type: (ayiiisayisas) +*/ + +class AKONADIPROTOCOLINTERNALS_EXPORT NotificationMessage +{ + public: + typedef QList List; + typedef qint64 Id; + + enum Type { + InvalidType, + Collection, + Item + }; + + enum Operation { + InvalidOp, + Add, + Modify, + Move, + Remove, + Link, + Unlink, + Subscribe, + Unsubscribe + }; + + NotificationMessage(); + NotificationMessage( const NotificationMessage &other ); + ~NotificationMessage(); + + NotificationMessage &operator=( const NotificationMessage &other ); + bool operator==( const NotificationMessage &other ) const; + + static void registerDBusTypes(); + + QByteArray sessionId() const; + void setSessionId( const QByteArray &sessionId ); + + Type type() const; + void setType( Type type ); + + Operation operation() const; + void setOperation( Operation operation ); + + Id uid() const; + void setUid( Id uid ); + + QString remoteId() const; + void setRemoteId( const QString &remoteId ); + + 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 ); + + QString mimeType() const; + void setMimeType( const QString &mimeType ); + + QSet itemParts() const; + void setItemParts( const QSet &parts ); + + QString toString() const; + + /** + Adds a new notification message to the given list and compresses notifications + where possible. + */ + static void appendAndCompress( NotificationMessage::List &list, const NotificationMessage &msg ); + // BIC: make the above return bool. + static void appendAndCompress( NotificationMessage::List &list, const NotificationMessage &msg, bool *appended ); + + private: + class Private; + QSharedDataPointer d; +}; + +} + +QDBusArgument &operator<<( QDBusArgument &arg, const Akonadi::NotificationMessage &msg ); +const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessage &msg ); + +uint qHash( const Akonadi::NotificationMessage &msg ); + +Q_DECLARE_TYPEINFO( Akonadi::NotificationMessage, Q_MOVABLE_TYPE ); + +Q_DECLARE_METATYPE( Akonadi::NotificationMessage ) +Q_DECLARE_METATYPE( Akonadi::NotificationMessage::List ) + +// V2 is used in NotificationSource.xml interface, so it must be +// defined so that old clients that only include this header +// will compile +#include "notificationmessagev2_p.h" + +#endif diff --git a/akonadi/libs/notificationmessagev2.cpp b/akonadi/libs/notificationmessagev2.cpp new file mode 100644 index 00000000..e52d7a7d --- /dev/null +++ b/akonadi/libs/notificationmessagev2.cpp @@ -0,0 +1,585 @@ +/* + Copyright (c) 2007 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 "notificationmessagev2_p.h" +#include "notificationmessagev2_p_p.h" +#include "notificationmessage_p.h" +#include "imapparser_p.h" + +#include +#include +#include +#include + +using namespace Akonadi; + +class NotificationMessageV2::Private : public QSharedData +{ + public: + Private() + : QSharedData() + , type( InvalidType ) + , operation( InvalidOp ) + , parentCollection( -1 ) + , parentDestCollection( -1 ) + { + } + + Private( const Private &other ) + : QSharedData( other ) + { + sessionId = other.sessionId; + type = other.type; + operation = other.operation; + items = other.items; + resource = other.resource; + destResource = other.destResource; + parentCollection = other.parentCollection; + parentDestCollection = other.parentDestCollection; + parts = other.parts; + addedFlags = other.addedFlags; + removedFlags = other.removedFlags; + addedTags = other.addedTags; + removedTags = other.removedTags; + } + + QByteArray sessionId; + NotificationMessageV2::Type type; + NotificationMessageV2::Operation operation; + QMap items; + QByteArray resource; + QByteArray destResource; + Id parentCollection; + Id parentDestCollection; + QSet parts; + QSet addedFlags; + QSet removedFlags; + QSet addedTags; + QSet removedTags; +}; + +NotificationMessageV2::NotificationMessageV2(): + d( new Private ) +{ +} + +NotificationMessageV2::NotificationMessageV2( const NotificationMessageV2 &other ): + d( other.d ) +{ +} + +NotificationMessageV2::~NotificationMessageV2() +{ +} + +NotificationMessageV2 &NotificationMessageV2::operator=( const NotificationMessageV2 &other ) +{ + if ( this != &other ) { + d = other.d; + } + + return *this; +} + +bool NotificationMessageV2::operator==( const NotificationMessageV2 &other ) const +{ + return d->operation == other.d->operation + && d->parts == other.d->parts + && d->addedFlags == other.d->addedFlags + && d->removedFlags == other.d->removedFlags + && d->addedTags == other.d->addedTags + && d->removedTags == other.d->removedTags + && NotificationMessageHelpers::compareWithoutOpAndParts( *this, other ); +} + +void NotificationMessageV2::registerDBusTypes() +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType >(); + qDBusRegisterMetaType >(); +} + +bool NotificationMessageV2::isValid() const +{ + return d->operation != Akonadi::NotificationMessageV2::InvalidOp + && d->type != Akonadi::NotificationMessageV2::InvalidType + && !d->items.isEmpty(); +} + +void NotificationMessageV2::addEntity( Id id, const QString &remoteId, const QString &remoteRevision, const QString &mimeType ) +{ + NotificationMessageV2::Entity item; + item.id = id; + item.remoteId = remoteId; + item.remoteRevision = remoteRevision; + item.mimeType = mimeType; + + d->items.insert( id, item ); +} + +void NotificationMessageV2::setEntities( const QList &items ) +{ + clearEntities(); + Q_FOREACH ( const NotificationMessageV2::Entity &item, items ) { + d->items.insert( item.id, item ); + } +} + +void NotificationMessageV2::clearEntities() +{ + d->items.clear(); +} + +QMap NotificationMessageV2::entities() const +{ + return d->items; +} + +NotificationMessageV2::Entity NotificationMessageV2::entity( NotificationMessageV2::Id id ) const +{ + return d->items.value( id ); +} + +QList NotificationMessageV2::uids() const +{ + return d->items.keys(); +} + +QByteArray NotificationMessageV2::sessionId() const +{ + return d->sessionId; +} + +void NotificationMessageV2::setSessionId( const QByteArray &sessionId ) +{ + d->sessionId = sessionId; +} + +NotificationMessageV2::Type NotificationMessageV2::type() const +{ + return d->type; +} + +void NotificationMessageV2::setType( Type type ) +{ + d->type = type; +} + +NotificationMessageV2::Operation NotificationMessageV2::operation() const +{ + return d->operation; +} + +void NotificationMessageV2::setOperation( Operation operation ) +{ + d->operation = operation; +} + +QByteArray NotificationMessageV2::resource() const +{ + return d->resource; +} + +void NotificationMessageV2::setResource( const QByteArray &resource ) +{ + d->resource = resource; +} + +NotificationMessageV2::Id NotificationMessageV2::parentCollection() const +{ + return d->parentCollection; +} + +NotificationMessageV2::Id NotificationMessageV2::parentDestCollection() const +{ + return d->parentDestCollection; +} + +void NotificationMessageV2::setParentCollection( Id parent ) +{ + d->parentCollection = parent; +} + +void NotificationMessageV2::setParentDestCollection( Id parent ) +{ + d->parentDestCollection = parent; +} + +void NotificationMessageV2::setDestinationResource( const QByteArray &destResource ) +{ + d->destResource = destResource; +} + +QByteArray NotificationMessageV2::destinationResource() const +{ + return d->destResource; +} + +QSet NotificationMessageV2::itemParts() const +{ + return d->parts; +} + +void NotificationMessageV2::setItemParts( const QSet &parts ) +{ + d->parts = parts; +} + +QSet NotificationMessageV2::addedFlags() const +{ + return d->addedFlags; +} + +void NotificationMessageV2::setAddedFlags( const QSet &addedFlags ) +{ + d->addedFlags = addedFlags; +} + +QSet NotificationMessageV2::removedFlags() const +{ + return d->removedFlags; +} + +void NotificationMessageV2::setRemovedFlags( const QSet &removedFlags ) +{ + d->removedFlags = removedFlags; +} + +QSet NotificationMessageV2::addedTags() const +{ + return d->addedTags; +} + +void NotificationMessageV2::setAddedTags( const QSet &addedTags ) +{ + d->addedTags = addedTags; +} + +QSet NotificationMessageV2::removedTags() const +{ + return d->removedTags; +} + +void NotificationMessageV2::setRemovedTags( const QSet &removedTags ) +{ + d->removedTags = removedTags; +} + +QString NotificationMessageV2::toString() const +{ + QString rv; + + switch ( d->type ) { + case Items: + rv += QLatin1String( "Items " ); + break; + case Collections: + rv += QLatin1String( "Collections " ); + break; + case Tags: + rv += QLatin1String( "Tags " ); + break; + case InvalidType: + return QLatin1String( "*INVALID TYPE* " ); + } + + QSet items; + Q_FOREACH ( const NotificationMessageV2::Entity &item, d->items ) { + QString itemStr = QString::fromLatin1( "(%1,%2" ).arg( item.id ).arg( item.remoteId ); + if ( !item.remoteRevision.isEmpty() ) { + itemStr += QString::fromLatin1( ",%1" ).arg( item.remoteRevision ); + } + if ( !item.mimeType.isEmpty() ) { + itemStr += QString::fromLatin1( ",%1" ).arg( item.mimeType ); + } + itemStr += QLatin1String( ")" ); + items << itemStr.toLatin1(); + } + rv += QLatin1String( "(" ) + QString::fromLatin1( ImapParser::join( items, ", " ) ) + QLatin1String( ")" ); + + if ( d->parentDestCollection >= 0 ) { + rv += QLatin1String( " from " ); + } else { + rv += QLatin1String( " in " ); + } + + if ( d->parentCollection >= 0 ) { + rv += QString::fromLatin1( "collection %1 " ).arg( d->parentCollection ); + } else { + rv += QLatin1String( "unspecified parent collection " ); + } + + switch ( d->operation ) { + case Add: + rv += QLatin1String( "added" ); + break; + case Modify: + rv += QLatin1String( "modified parts (" ); + rv += QString::fromLatin1( ImapParser::join( d->parts.toList(), ", " ) ); + rv += QLatin1String( ")" ); + break; + case ModifyFlags: + rv += QLatin1String( "added flags (" ); + rv += QString::fromLatin1( ImapParser::join( d->addedFlags.toList(), ", " ) ); + rv += QLatin1String( ") " ); + + rv += QLatin1String( "removed flags (" ); + rv += QString::fromLatin1( ImapParser::join( d->removedFlags.toList(), ", " ) ); + rv += QLatin1String( ") " ); + break; + case ModifyTags: { + rv += QLatin1String( "added tags (" ); + QList tags; + Q_FOREACH ( qint64 tagId, d->addedTags ) { + tags << QByteArray::number( tagId ); + } + rv += QString::fromLatin1( ImapParser::join( tags, ", " ) ); + rv += QLatin1String( ") " ); + + tags.clear(); + Q_FOREACH ( qint64 tagId, d->removedTags ) { + tags << QByteArray::number( tagId ); + } + rv += QLatin1String( "removed tags (" ); + rv += QString::fromLatin1( ImapParser::join( tags, ", " ) ); + rv += QLatin1String( ") " ); + break; + } + case Move: + rv += QLatin1String( "moved" ); + break; + case Remove: + rv += QLatin1String( "removed" ); + break; + case Link: + rv += QLatin1String( "linked" ); + break; + case Unlink: + rv += QLatin1String( "unlinked" ); + break; + case Subscribe: + rv += QLatin1String( "subscribed" ); + break; + case Unsubscribe: + rv += QLatin1String( "unsubscribed" ); + break; + case InvalidOp: + return QLatin1String( "*INVALID OPERATION*" ); + } + + if ( d->parentDestCollection >= 0 ) { + rv += QString::fromLatin1( " to collection %1" ).arg( d->parentDestCollection ); + } + + return rv; +} + +QDBusArgument &operator<<( QDBusArgument &arg, const Akonadi::NotificationMessageV2 &msg ) +{ + arg.beginStructure(); + arg << msg.sessionId(); + arg << static_cast( msg.type() ); + arg << static_cast( msg.operation() ); + arg << msg.entities().values(); + arg << msg.resource(); + arg << msg.destinationResource(); + arg << msg.parentCollection(); + arg << msg.parentDestCollection(); + + QStringList itemParts; + Q_FOREACH ( const QByteArray &itemPart, msg.itemParts() ) { + itemParts.append( QString::fromLatin1( itemPart ) ); + } + arg << itemParts; + + arg << msg.addedFlags().toList(); + arg << msg.removedFlags().toList(); + + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV2 &msg ) +{ + QByteArray ba; + int i; + QList items; + NotificationMessageV2::Id id; + QString str; + QStringList strl; + QList bal; + + arg.beginStructure(); + arg >> ba; + msg.setSessionId( ba ); + arg >> i; + msg.setType( static_cast( i ) ); + arg >> i; + msg.setOperation( static_cast( i ) ); + arg >> items; + msg.setEntities( items ); + arg >> ba; + msg.setResource( ba ); + arg >> ba; + msg.setDestinationResource( ba ); + arg >> id; + msg.setParentCollection( id ); + arg >> id; + msg.setParentDestCollection( id ); + + arg >> strl; + + QSet itemParts; + Q_FOREACH ( const QString &itemPart, strl ) { + itemParts.insert( itemPart.toLatin1() ); + } + msg.setItemParts( itemParts ); + + arg >> bal; + msg.setAddedFlags( bal.toSet() ); + arg >> bal; + msg.setRemovedFlags( bal.toSet() ); + + arg.endStructure(); + return arg; +} + +QDBusArgument &operator<<( QDBusArgument &arg, const Akonadi::NotificationMessageV2::Entity &item ) +{ + arg.beginStructure(); + arg << item.id; + arg << item.remoteId; + arg << item.remoteRevision; + arg << item.mimeType; + arg.endStructure(); + + return arg; +} + +const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV2::Entity &item ) +{ + arg.beginStructure(); + arg >> item.id; + arg >> item.remoteId; + arg >> item.remoteRevision; + arg >> item.mimeType; + arg.endStructure(); + + return arg; +} + +QDBusArgument &operator<<( QDBusArgument &arg, Akonadi::NotificationMessageV2::Type type ) +{ + arg.beginStructure(); + arg << static_cast( type ); + arg.endStructure(); + + return arg; +} + +const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV2::Type &type ) +{ + int t; + arg.beginStructure(); + arg >> t; + arg.endStructure(); + type = static_cast( t ); + + return arg; +} + +uint qHash( const Akonadi::NotificationMessageV2 &msg ) +{ + uint i = 0; + Q_FOREACH ( const NotificationMessageV2::Entity &item, msg.entities() ) { + i += item.id; + } + + return qHash( i + ( msg.type() << 31 ) + ( msg.operation() << 28 ) ); +} + +QVector NotificationMessageV2::toNotificationV1() const +{ + QVector v1; + + Q_FOREACH ( const Entity &item, d->items ) { + NotificationMessage msgv1; + msgv1.setSessionId( d->sessionId ); + msgv1.setUid( item.id ); + msgv1.setRemoteId( item.remoteId ); + msgv1.setMimeType( item.mimeType ); + msgv1.setType( static_cast( d->type ) ); + if ( d->operation == ModifyFlags ) { + msgv1.setOperation( NotificationMessage::Modify ); + } else { + msgv1.setOperation( static_cast( d->operation ) ); + } + + msgv1.setResource( d->resource ); + msgv1.setDestinationResource( d->destResource ); + msgv1.setParentCollection( d->parentCollection ); + msgv1.setParentDestCollection( d->parentDestCollection ); + + // Backward compatibility hack + QSet parts; + if ( d->operation == Remove ) { + QByteArray rr = item.remoteRevision.toLatin1(); + parts << ( rr.isEmpty() ? "1" : rr ); + } else if ( d->operation == ModifyFlags ) { + parts << "FLAGS"; + } else { + parts = d->parts; + } + msgv1.setItemParts( parts ); + + v1 << msgv1; + } + + return v1; +} + +bool NotificationMessageV2::appendAndCompress( NotificationMessageV2::List &list, const NotificationMessageV2 &msg ) +{ + return NotificationMessageHelpers::appendAndCompressImpl( list, msg ); +} + +bool NotificationMessageV2::appendAndCompress( QList &list, const NotificationMessageV2 &msg ) +{ + return NotificationMessageHelpers::appendAndCompressImpl, NotificationMessageV2>( list, msg ); +} + +QDebug operator<<( QDebug dbg, const NotificationMessageV2::Entity &entity) +{ + dbg.nospace() << "(ID: " << entity.id; + if (!entity.remoteId.isEmpty()) { + dbg.nospace() << " RID: " << entity.remoteId; + } + if (!entity.remoteRevision.isEmpty()) { + dbg.nospace() << " RREV: " << entity.remoteRevision; + } + if (!entity.mimeType.isEmpty()) { + dbg.nospace() << " MimeType: " << entity.mimeType; + } + dbg.nospace() << ")"; + return dbg; +} diff --git a/akonadi/libs/notificationmessagev2_p.h b/akonadi/libs/notificationmessagev2_p.h new file mode 100644 index 00000000..16371feb --- /dev/null +++ b/akonadi/libs/notificationmessagev2_p.h @@ -0,0 +1,190 @@ +/* + Copyright (c) 2007 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 AKONADI_NOTIFICATIONMESSAGEV2_H +#define AKONADI_NOTIFICATIONMESSAGEV2_H + +#include "akonadiprotocolinternals_export.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "notificationmessage_p.h" + +namespace Akonadi +{ + +/** + @internal + Used for sending notification signals over DBus. + DBus type: (ayiia(xsss)ayayxxasaayaay) +*/ +class AKONADIPROTOCOLINTERNALS_EXPORT NotificationMessageV2 +{ + public: + typedef QVector List; + typedef qint64 Id; + + enum Type { + InvalidType, + Collections, + Items, + Tags + }; + + // NOTE: Keep this BC with NotificationMessage - i.e. append new stuff to the end + enum Operation { + InvalidOp, + Add, + Modify, + Move, + Remove, + Link, + Unlink, + Subscribe, + Unsubscribe, + ModifyFlags, + ModifyTags + }; + + class Entity + { + public: + Entity() + : id( -1 ) + { + } + + 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; + }; + + NotificationMessageV2(); + NotificationMessageV2( const NotificationMessageV2 &other ); + ~NotificationMessageV2(); + + NotificationMessageV2 &operator=( const NotificationMessageV2 &other ); + bool operator==( const NotificationMessageV2 &other ) const; + bool operator!=( const NotificationMessageV2 &other ) const + { + return !operator==(other); + } + + static void registerDBusTypes(); + + bool isValid() const; + + NotificationMessageV2::Type type() const; + void setType( NotificationMessageV2::Type type ); + + NotificationMessageV2::Operation operation() const; + void setOperation( NotificationMessageV2::Operation operation ); + + QByteArray sessionId() const; + void setSessionId( const QByteArray &session ); + + void addEntity( Id id, const QString &remoteId = QString(), const QString &remoteRevision = QString(), const QString &mimeType = QString() ); + void setEntities( const QList &items ); + QMap entities() const; + NotificationMessageV2::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 ); + + QString toString() const; + + QVector toNotificationV1() const; + + static bool appendAndCompress( NotificationMessageV2::List &list, const NotificationMessageV2 &msg ); + static bool appendAndCompress( QList &list, const NotificationMessageV2 &msg ); + + protected: + class Private; + QSharedDataPointer d; +}; + +} // namespace Akonadi + +AKONADIPROTOCOLINTERNALS_EXPORT QDebug operator<<( QDebug debug, const Akonadi::NotificationMessageV2::Entity &entity ); + +const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV2 &msg ); +QDBusArgument &operator<<( QDBusArgument &arg, const Akonadi::NotificationMessageV2 &msg ); +const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV2::Entity &item ); +QDBusArgument &operator<<( QDBusArgument &arg, const Akonadi::NotificationMessageV2::Entity &item ); +const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV2::Type &type ); +QDBusArgument &operator<<( QDBusArgument &qrg, Akonadi::NotificationMessageV2::Type type ); +uint qHash( const Akonadi::NotificationMessageV2 &msg ); + +Q_DECLARE_TYPEINFO( Akonadi::NotificationMessageV2, Q_MOVABLE_TYPE ); + +Q_DECLARE_METATYPE( Akonadi::NotificationMessageV2 ) +Q_DECLARE_METATYPE( Akonadi::NotificationMessageV2::Entity ) +Q_DECLARE_METATYPE( Akonadi::NotificationMessageV2::List ) +Q_DECLARE_METATYPE( Akonadi::NotificationMessageV2::Type ) +Q_DECLARE_METATYPE( QVector ) +Q_DECLARE_METATYPE( QVector ) +Q_DECLARE_METATYPE( QVector ) + +// V3 is used in NotificationSource.xml interface, so it must be +// defined so that old clients that only include this header +// will compile +#include "notificationmessagev3_p.h" + +#endif // NOTIFICATIONMESSAGEV2_H diff --git a/akonadi/libs/notificationmessagev2_p_p.h b/akonadi/libs/notificationmessagev2_p_p.h new file mode 100644 index 00000000..ac592e2e --- /dev/null +++ b/akonadi/libs/notificationmessagev2_p_p.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_NOTIFICATIONMESSAGEV2_P_H +#define AKONADI_NOTIFICATIONMESSAGEV2_P_H + +#include "notificationmessagev2_p.h" + +namespace Akonadi +{ + +class NotificationMessageHelpers +{ + public: + template + static bool compareWithoutOpAndParts( const T &left, const T &right ) + { + return left.entities() == right.entities() + && left.type() == right.type() + && left.sessionId() == right.sessionId() + && left.resource() == right.resource() + && left.destinationResource() == right.destinationResource() + && left.parentCollection() == right.parentCollection() + && left.parentDestCollection() == right.parentDestCollection(); + } + + template + static bool appendAndCompressImpl( List &list, const Msg &msg ) + { + // fast-path for stuff that is not considered during O(n) compression below + if ( msg.operation() != NotificationMessageV2::Add && msg.operation() != NotificationMessageV2::Link + && msg.operation() != NotificationMessageV2::Unlink && msg.operation() != NotificationMessageV2::Subscribe + && msg.operation() != NotificationMessageV2::Unsubscribe && msg.operation() != NotificationMessageV2::Move ) { + + typename List::Iterator end = list.end(); + for ( typename List::Iterator it = list.begin(); it != end; ) { + if ( compareWithoutOpAndParts( msg, ( *it ) ) ) { + + // both are modifications, merge them together and drop the new one + if ( msg.operation() == NotificationMessageV2::Modify && it->operation() == NotificationMessageV2::Modify ) { + ( *it ).setItemParts( ( *it ).itemParts() + msg.itemParts() ); + return false; + } + + else if ( msg.operation() == NotificationMessageV2::ModifyFlags && it->operation() == NotificationMessageV2::ModifyFlags ) { + ( *it ).setAddedFlags( ( *it ).addedFlags() + msg.addedFlags() ); + ( *it ).setRemovedFlags( ( *it ).removedFlags() + msg.removedFlags() ); + + // If merged notifications result in no-change notification, drop both. + if ( ( *it ).addedFlags() == ( *it ).removedFlags() ) { + it = list.erase( it ); + end = list.end(); + } + + return false; + } + + else if ( msg.operation() == NotificationMessageV2::ModifyTags && it->operation() == NotificationMessageV2::ModifyTags ) { + ( *it ).setAddedTags( ( *it ).addedTags() + msg.addedTags() ); + ( *it ).setRemovedTags( ( *it ).removedTags() + msg.removedTags() ); + + // If merged notification results in no-change notification, drop both + if ( ( *it ).addedTags() == ( *it ).removedTags() ) { + it = list.erase( it ); + end = list.end(); + } + + return false; + } + // new one is a modification, the existing one not, so drop the new one + else if ( ( ( msg.operation() == NotificationMessageV2::Modify ) || ( msg.operation() == NotificationMessageV2::ModifyFlags ) ) + && ( ( *it ).operation() != NotificationMessageV2::Modify ) + && ( *it ).operation() != NotificationMessageV2::ModifyFlags + && ( *it ).operation() != NotificationMessageV2::ModifyTags ) { + return false; + } + // new one is a deletion, erase the existing modification ones (and keep going, in case there are more) + else if ( msg.operation() == NotificationMessageV2::Remove && ( ( *it ).operation() == NotificationMessageV2::Modify || ( *it ).operation() == NotificationMessageV2::ModifyFlags || ( *it ).operation() == NotificationMessageV2::ModifyTags ) ) { + it = list.erase( it ); + end = list.end(); + } + // keep looking + else { + ++it; + } + } else { + ++it; + } + } + } + + list.append( msg ); + return true; + } +}; + +} + +#endif diff --git a/akonadi/libs/notificationmessagev3.cpp b/akonadi/libs/notificationmessagev3.cpp new file mode 100644 index 00000000..c89607a3 --- /dev/null +++ b/akonadi/libs/notificationmessagev3.cpp @@ -0,0 +1,215 @@ +/* + 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 "notificationmessagev3_p.h" +#include "notificationmessagev2_p_p.h" + +#include +#include + +using namespace Akonadi; + +NotificationMessageV3::NotificationMessageV3() + : NotificationMessageV2() +{ +} + +NotificationMessageV3::NotificationMessageV3( const NotificationMessageV3 &other ) + : NotificationMessageV2( other ) +{ +} + +NotificationMessageV3::~NotificationMessageV3() +{ +} + +void NotificationMessageV3::registerDBusTypes() +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); +} + +NotificationMessageV2::List NotificationMessageV3::toV2List( const NotificationMessageV3::List &list ) +{ + NotificationMessageV2::List out; + out.reserve( list.size() ); + Q_FOREACH ( const NotificationMessageV3 &v3, list ) { + out << static_cast( v3 ); + } + return out; +} + +bool NotificationMessageV3::appendAndCompress( NotificationMessageV3::List &list, const NotificationMessageV3 &msg ) +{ + return NotificationMessageHelpers::appendAndCompressImpl( list, msg ); +} + +bool NotificationMessageV3::appendAndCompress( QList &list, const NotificationMessageV3 &msg ) +{ + return NotificationMessageHelpers::appendAndCompressImpl, NotificationMessageV3>( list, msg ); +} + +const QDBusArgument &operator>>( const QDBusArgument &arg, NotificationMessageV3 &msg ) +{ + QByteArray ba; + int i; + QList items; + NotificationMessageV2::Id id; + QString str; + QStringList strl; + QList bal; + QList intl; + + arg.beginStructure(); + arg >> ba; + msg.setSessionId( ba ); + arg >> i; + msg.setType( static_cast( i ) ); + arg >> i; + msg.setOperation( static_cast( i ) ); + arg >> items; + msg.setEntities( items ); + arg >> ba; + msg.setResource( ba ); + arg >> ba; + msg.setDestinationResource( ba ); + arg >> id; + msg.setParentCollection( id ); + arg >> id; + msg.setParentDestCollection( id ); + + arg >> strl; + + QSet itemParts; + Q_FOREACH ( const QString &itemPart, strl ) { + itemParts.insert( itemPart.toLatin1() ); + } + msg.setItemParts( itemParts ); + + arg >> bal; + msg.setAddedFlags( bal.toSet() ); + arg >> bal; + msg.setRemovedFlags( bal.toSet() ); + arg >> intl; + msg.setAddedTags( intl.toSet() ); + arg >> intl; + msg.setRemovedTags( intl.toSet() ); + + arg.endStructure(); + return arg; +} + +QDBusArgument &operator<<( QDBusArgument &arg, const NotificationMessageV3 &msg ) +{ + arg.beginStructure(); + arg << msg.sessionId(); + arg << static_cast( msg.type() ); + arg << static_cast( msg.operation() ); + arg << msg.entities().values(); + arg << msg.resource(); + arg << msg.destinationResource(); + arg << msg.parentCollection(); + arg << msg.parentDestCollection(); + + QStringList itemParts; + Q_FOREACH ( const QByteArray &itemPart, msg.itemParts() ) { + itemParts.append( QString::fromLatin1( itemPart ) ); + } + arg << itemParts; + + arg << msg.addedFlags().toList(); + arg << msg.removedFlags().toList(); + arg << msg.addedTags().toList(); + arg << msg.removedTags().toList(); + + arg.endStructure(); + return arg; +} + +QDebug operator<<( QDebug dbg, const NotificationMessageV3 &msg ) +{ + dbg.nospace() << "NotificationMessageV3 {\n"; + dbg.nospace() << "\tType: "; + switch (msg.type()) { + case NotificationMessageV2::InvalidType: + dbg.nospace() << QLatin1String("Invalid"); + break; + case NotificationMessageV2::Items: + dbg.nospace() << QLatin1String("Items"); + break; + case NotificationMessageV2::Collections: + dbg.nospace() << QLatin1String("Collections"); + break; + case NotificationMessageV2::Tags: + dbg.nospace() << QLatin1String("Tags"); + break; + } + dbg.nospace() << '\n'; + + dbg.nospace() << "\tOperation: "; + switch( msg.operation()) { + case NotificationMessageV2::InvalidOp: + dbg.nospace() << QLatin1String("Invalid"); + break; + case NotificationMessageV2::Add: + dbg.nospace() << QLatin1String("Add"); + break; + case NotificationMessageV2::Modify: + dbg.nospace() << QLatin1String("Modify"); + break; + case NotificationMessageV2::Move: + dbg.nospace() << QLatin1String("Move"); + break; + case NotificationMessageV2::Remove: + dbg.nospace() << QLatin1String("Remove"); + break; + case NotificationMessageV2::Link: + dbg.nospace() << QLatin1String("Link"); + break; + case NotificationMessageV2::Unlink: + dbg.nospace() << QLatin1String("Unlink"); + break; + case NotificationMessageV2::Subscribe: + dbg.nospace() << QLatin1String("Subscribe"); + break; + case NotificationMessageV2::Unsubscribe: + dbg.nospace() << QLatin1String("Unsubscribe"); + break; + case NotificationMessageV2::ModifyFlags: + dbg.nospace() << QLatin1String("ModifyFlags"); + break; + case NotificationMessageV2::ModifyTags: + dbg.nospace() << QLatin1String("ModifyTags"); + break; + } + dbg.nospace() << "\n"; + dbg.nospace() << "\tSession: " << msg.sessionId() << "\n"; + dbg.nospace() << "\tEntities: " << msg.entities().values() << "\n"; + dbg.nospace() << "\tResource: " << msg.resource() << "\n"; + dbg.nospace() << "\tCollection: " << msg.parentCollection() << "\n"; + dbg.nospace() << "\tDestination resource: " << msg.destinationResource() << "\n"; + dbg.nospace() << "\tDestination collection: " << msg.parentDestCollection() << "\n"; + dbg.nospace() << "\tParts: " << msg.itemParts() << "\n"; + dbg.nospace() << "\tAdded flags: " << msg.addedFlags() << "\n"; + dbg.nospace() << "\tRemoved flags: " << msg.removedFlags() << "\n"; + dbg.nospace() << "\tAdded tags: " << msg.addedTags() << "\n"; + dbg.nospace() << "\tRemoved tags: " << msg.removedTags() << "\n"; + dbg.nospace() << "}" << "\n"; + return dbg; +} diff --git a/akonadi/libs/notificationmessagev3_p.h b/akonadi/libs/notificationmessagev3_p.h new file mode 100644 index 00000000..d5805bd0 --- /dev/null +++ b/akonadi/libs/notificationmessagev3_p.h @@ -0,0 +1,60 @@ +/* + 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_NOTIFICATIONMESSAGEV3_H +#define AKONADI_NOTIFICATIONMESSAGEV3_H + +#include "akonadiprotocolinternals_export.h" + +#include "notificationmessagev2_p.h" +#include +#include + +namespace Akonadi +{ + +class AKONADIPROTOCOLINTERNALS_EXPORT NotificationMessageV3 : public Akonadi::NotificationMessageV2 +{ + public: + typedef QVector List; + + static void registerDBusTypes(); + + NotificationMessageV3(); + NotificationMessageV3( const NotificationMessageV3 &other ); + ~NotificationMessageV3(); + + static NotificationMessageV2::List toV2List( const NotificationMessageV3::List &list ); + static bool appendAndCompress( NotificationMessageV3::List &list, const NotificationMessageV3 &msg ); + static bool appendAndCompress( QList &list, const NotificationMessageV3 &msg ); + +}; + +} + +AKONADIPROTOCOLINTERNALS_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::NotificationMessageV3 &msg); + +const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV3 &msg ); +QDBusArgument &operator<<( QDBusArgument &arg, const Akonadi::NotificationMessageV3 &msg ); + +Q_DECLARE_TYPEINFO( Akonadi::NotificationMessageV3, Q_MOVABLE_TYPE ); +Q_DECLARE_METATYPE( Akonadi::NotificationMessageV3 ) +Q_DECLARE_METATYPE( Akonadi::NotificationMessageV3::List ) + +#endif // AKONADI_NOTIFICATIONMESSAGEV3_H diff --git a/akonadi/libs/protocol_p.h b/akonadi/libs/protocol_p.h new file mode 100644 index 00000000..002abe4e --- /dev/null +++ b/akonadi/libs/protocol_p.h @@ -0,0 +1,170 @@ +/* + 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_PROTOCOL_P_H +#define AKONADI_PROTOCOL_P_H + +/** + @file protocol_p.h Shared constants used in the communication protocol between + the Akonadi server and its clients. + + @todo Fill this file with command names, item/collection property names + item part names, etc. and replace the usages accordingly. +*/ + +// D-Bus service names +#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" + +#define AKONADI_DBUS_AGENTMANAGER_PATH "/AgentManager" +#define AKONADI_DBUS_AGENTSERVER_PATH "/AgentServer" +#define AKONADI_DBUS_STORAGEJANITOR_PATH "/Janitor" + +// Commands +#define AKONADI_CMD_APPEND "APPEND" +#define AKONADI_CMD_BEGIN "BEGIN" +#define AKONADI_CMD_CAPABILITY "CAPABILITY" +#define AKONADI_CMD_COLLECTION "COLLECTION" +#define AKONADI_CMD_COLLECTIONCOPY "COLCOPY" +#define AKONADI_CMD_COLLECTIONMOVE "COLMOVE" +#define AKONADI_CMD_COMMIT "COMMIT" +#define AKONADI_CMD_ITEMCOPY "COPY" +#define AKONADI_CMD_COLLECTIONCREATE "CREATE" +#define AKONADI_CMD_COLLECTIONDELETE "DELETE" +#define AKONADI_CMD_EXPUNGE "EXPUNGE" +#define AKONADI_CMD_ITEMFETCH "FETCH" +#define AKONADI_CMD_GID "GID" +#define AKONADI_CMD_HRID "HRID" +#define AKONADI_CMD_ITEMLINK "LINK" +#define AKONADI_CMD_LIST "LIST" +#define AKONADI_CMD_LOGIN "LOGIN" +#define AKONADI_CMD_LOGOUT "LOGOUT" +#define AKONADI_CMD_LSUB "LSUB" +#define AKONADI_CMD_MERGE "MERGE" +#define AKONADI_CMD_COLLECTIONMODIFY "MODIFY" +#define AKONADI_CMD_ITEMMOVE "MOVE" +#define AKONADI_CMD_ITEMDELETE "REMOVE" +#define AKONADI_CMD_RESOURCESELECT "RESSELECT" +#define AKONADI_CMD_RID "RID" +#define AKONADI_CMD_ROLLBACK "ROLLBACK" +#define AKONADI_CMD_SUBSCRIBE "SUBSCRIBE" +#define AKONADI_CMD_SEARCH "SEARCH" +#define AKONADI_CMD_SEARCH_RESULT "SEARCH_RESULT" +#define AKONADI_CMD_SEARCH_STORE "SEARCH_STORE" +#define AKONADI_CMD_SELECT "SELECT" +#define AKONADI_CMD_STATUS "STATUS" +#define AKONADI_CMD_ITEMMODIFY "STORE" +#define AKONADI_CMD_TAGAPPEND "TAGAPPEND" +#define AKONADI_CMD_TAGFETCH "TAGFETCH" +#define AKONADI_CMD_TAGREMOVE "TAGREMOVE" +#define AKONADI_CMD_TAGSTORE "TAGSTORE" +#define AKONADI_CMD_UID "UID" +#define AKONADI_CMD_ITEMUNLINK "UNLINK" +#define AKONADI_CMD_UNSUBSCRIBE "UNSUBSCRIBE" +#define AKONADI_CMD_ITEMCREATE "X-AKAPPEND" +#define AKONADI_CMD_X_AKLIST "X-AKLIST" +#define AKONADI_CMD_X_AKLSUB "X-AKLSUB" + +// Command parameters +#define AKONADI_PARAM_CAPABILITY_AKAPPENDSTREAMING "AKAPPENDSTREAMING" +#define AKONADI_PARAM_ALLATTRIBUTES "ALLATTR" +#define AKONADI_PARAM_ANCESTORS "ANCESTORS" +#define AKONADI_PARAM_ATR "ATR:" +#define AKONADI_PARAM_CACHEONLY "CACHEONLY" +#define AKONADI_PARAM_CACHEDPARTS "CACHEDPARTS" +#define AKONADI_PARAM_CACHETIMEOUT "CACHETIMEOUT" +#define AKONADI_PARAM_CACHEPOLICY "CACHEPOLICY" +#define AKONADI_PARAM_CHANGEDSINCE "CHANGEDSINCE" +#define AKONADI_PARAM_CHARSET "CHARSET" +#define AKONADI_PARAM_CHECKCACHEDPARTSONLY "CHECKCACHEDPARTSONLY" +#define AKONADI_PARAM_COLLECTION "COLLECTION" +#define AKONADI_PARAM_COLLECTIONID "COLLECTIONID" +#define AKONADI_PARAM_COLLECTIONS "COLLECTIONS" +#define AKONADI_PARAM_MTIME "DATETIME" +#define AKONADI_PARAM_CAPABILITY_DIRECTSTREAMING "DIRECTSTREAMING" +#define AKONADI_PARAM_UNDIRTY "DIRTY" +#define AKONADI_PARAM_DISPLAY "DISPLAY" +#define AKONADI_PARAM_EXTERNALPAYLOAD "EXTERNALPAYLOAD" +#define AKONADI_PARAM_ENABLED "ENABLED" +#define AKONADI_PARAM_FLAGS "FLAGS" +#define AKONADI_PARAM_TAGS "TAGS" +#define AKONADI_PARAM_FULLPAYLOAD "FULLPAYLOAD" +#define AKONADI_PARAM_GID "GID" +#define AKONADI_PARAM_IGNOREERRORS "IGNOREERRORS" +#define AKONADI_PARAM_INDEX "INDEX" +#define AKONADI_PARAM_INHERIT "INHERIT" +#define AKONADI_PARAM_INTERVAL "INTERVAL" +#define AKONADI_PARAM_INVALIDATECACHE "INVALIDATECACHE" +#define AKONADI_PARAM_MIMETYPE "MIMETYPE" +#define AKONADI_PARAM_MERGE "MERGE" +#define AKONADI_PARAM_LOCALPARTS "LOCALPARTS" +#define AKONADI_PARAM_NAME "NAME" +#define AKONADI_PARAM_CAPABILITY_NOTIFY "NOTIFY" +#define AKONADI_PARAM_CAPABILITY_NOPAYLOADPATH "NOPAYLOADPATH" +#define AKONADI_PARAM_PARENT "PARENT" +#define AKONADI_PARAM_PERSISTENTSEARCH "PERSISTENTSEARCH" +#define AKONADI_PARAM_PARTS "PARTS" +#define AKONADI_PARAM_PLD "PLD:" +#define AKONADI_PARAM_PLD_RFC822 "PLD:RFC822" +#define AKONADI_PARAM_PERSISTENTSEARCH_QUERYCOLLECTIONS "QUERYCOLLECTIONS" +#define AKONADI_PARAM_PERSISTENTSEARCH_QUERYLANG "QUERYLANGUAGE" +#define AKONADI_PARAM_PERSISTENTSEARCH_QUERYSTRING "QUERYSTRING" +#define AKONADI_PARAM_QUERY "QUERY" +#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_RESOURCE "RESOURCE" +#define AKONADI_PARAM_REVISION "REV" +#define AKONADI_PARAM_SILENT "SILENT" +#define AKONADI_PARAM_DOT_SILENT ".SILENT" +#define AKONADI_PARAM_CAPABILITY_SERVERSEARCH "SERVERSEARCH" +#define AKONADI_PARAM_SIZE "SIZE" +#define AKONADI_PARAM_STATISTICS "STATISTICS" +#define AKONADI_PARAM_SYNC "SYNC" +#define AKONADI_PARAM_SYNCONDEMAND "SYNCONDEMAND" +#define AKONADI_PARAM_TAG "TAG" +#define AKONADI_PARAM_TAGID "TAGID" +#define AKONADI_PARAM_UID "UID" +#define AKONADI_PARAM_VIRTREF "VIRTREF" +#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/akonadi/libs/tests/CMakeLists.txt b/akonadi/libs/tests/CMakeLists.txt new file mode 100644 index 00000000..123000f7 --- /dev/null +++ b/akonadi/libs/tests/CMakeLists.txt @@ -0,0 +1,14 @@ +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) + +macro(add_unit_test _source) + set(_test ${_source}) + get_filename_component(_name ${_source} NAME_WE) + add_executable(${_name} ${_source}) + add_test(akonadi-${_name} ${_name}) + target_link_libraries(${_name} akonadiprotocolinternals ${QT_QTGUI_LIBRARY} ${QT_QTTEST_LIBRARIES}) +endmacro() + +add_unit_test(notificationmessagetest.cpp) +add_unit_test(notificationmessagev2test.cpp) +add_unit_test(imapparserbenchmark.cpp) diff --git a/akonadi/libs/tests/imapparserbenchmark.cpp b/akonadi/libs/tests/imapparserbenchmark.cpp new file mode 100644 index 00000000..17dac660 --- /dev/null +++ b/akonadi/libs/tests/imapparserbenchmark.cpp @@ -0,0 +1,101 @@ +/* + 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 "../imapparser_p.h" + +using namespace Akonadi; + +Q_DECLARE_METATYPE( QList ) + +class ImapParserBenchmark : public QObject +{ + Q_OBJECT + private Q_SLOTS: + void quote_data() + { + QTest::addColumn( "input" ); + QTest::newRow( "empty" ) << QByteArray(); + QTest::newRow( "10-idle" ) << QByteArray( "ababababab" ); + QTest::newRow( "10-quote" ) << QByteArray( "\"abababab\"" ); + QTest::newRow( "50-idle" ) << QByteArray( "ababababababababababababababababababababababababab" ); + QTest::newRow( "50-quote" ) << QByteArray( "\"abababab\ncabababab\ncabababab\ncabababab\ncabababab\"" ); + } + + void quote() + { + QFETCH( QByteArray, input ); + QBENCHMARK { + ImapParser::quote( input ); + } + } + + void join_data() + { + QTest::addColumn >( "list" ); + QTest::newRow( "empty" ) << QList(); + QTest::newRow( "single" ) << ( QList() << "ababab" ); + QTest::newRow( "two" ) << ( QList() << "ababab" << "ababab" ); + QTest::newRow( "five" ) << ( QList() << "ababab" << "ababab" << "ababab" << "ababab" << "ababab" ); + QList list; + for ( int i = 0; i < 50; ++i ) { + list << "ababab"; + } + QTest::newRow( "a lot" ) << list; + } + + void join() + { + QFETCH( QList, list ); + QBENCHMARK { + ImapParser::join( list, " " ); + } + } + + void parseParenthesizedList_data() + { + QTest::addColumn( "data" ); + QTest::newRow( "empty" ) << QByteArray(); + QTest::newRow( "unnested" ) << QByteArray("(\"Foo Bar\" NIL \"foobar\" \"test.com\")"); + QTest::newRow( "nested" ) << QByteArray("((\"Foo Bar\" NIL \"foobar\" \"test.com\"))"); + QTest::newRow( "nested-long" ) << QByteArray("(UID 86 REV 0 MIMETYPE \"message/rfc822\" COLLECTIONID 13 SIZE 6114 FLAGS (\\SEEN)" + " ANCESTORS ((13 \"/INBOX\") (12 \"imap://mail@mail.test.com/\") (0 \"\")) PLD:ENVELOPE[1] {396}" + " (\"Fri, 04 Jun 2010 09:07:54 +0200\" \"Re: [ADMIN] foobar available again!\"" + " ((\"Foo Bar\" NIL \"foobar\" \"test.com\"))" + " NIL NIL" + " ((\"Asdf Bla Blub\" NIL \"asdf.bla.blub\" \"123test.org\"))" + " ((NIL NIL \"muh.kuh\" \"lalala.com\") (\"Konqi KDE\" NIL \"konqi\" \"kde.org\") (NIL NIL \"all\" \"test.com\"))" + " NIL \"<201006040905.33367.foo.bar@test.com>\" \"<4C08A64A.9020205@123test.org>\"" + " \"<201006040142.56540.muh.kuh@lalala.com> <201006040704.39648.konqi@kde.org> <201006040905.33367.foo.bar@test.com>\"" + "))"); + } + + void parseParenthesizedList() + { + QFETCH( QByteArray, data ); + QVarLengthArray result; + QBENCHMARK { + ImapParser::parseParenthesizedList( data, result, 0 ); + } + } +}; + +#include "imapparserbenchmark.moc" + +QTEST_APPLESS_MAIN( ImapParserBenchmark ) diff --git a/akonadi/libs/tests/notificationmessagetest.cpp b/akonadi/libs/tests/notificationmessagetest.cpp new file mode 100644 index 00000000..b28164de --- /dev/null +++ b/akonadi/libs/tests/notificationmessagetest.cpp @@ -0,0 +1,155 @@ +/* + 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; + +Q_DECLARE_METATYPE( NotificationMessage::Type ) + +void NotificationMessageTest::testCompress() +{ + NotificationMessage::List list; + NotificationMessage msg; + msg.setType( NotificationMessage::Item ); + msg.setOperation( NotificationMessage::Add ); + + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setOperation( NotificationMessage::Modify ); + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessage::Add ); + + msg.setOperation( NotificationMessage::Remove ); + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 2 ); // should be 2 for collections, 0 for items? +} + +void NotificationMessageTest::testCompress2() +{ + NotificationMessage::List list; + NotificationMessage msg; + msg.setType( NotificationMessage::Item ); + msg.setOperation( NotificationMessage::Modify ); + + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setOperation( NotificationMessage::Remove ); + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessage::Remove ); +} + +void NotificationMessageTest::testCompress3() +{ + NotificationMessage::List list; + NotificationMessage msg; + msg.setType( NotificationMessage::Item ); + msg.setOperation( NotificationMessage::Modify ); + + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); +} + +void NotificationMessageTest::testCompressWithItemParts() +{ + NotificationMessage::List list; + NotificationMessage msg; + msg.setType( NotificationMessage::Item ); + msg.setOperation( NotificationMessage::Add ); + + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setOperation( NotificationMessage::Modify ); + QSet changes; + changes.insert( "FLAGS" ); + msg.setItemParts( changes ); + NotificationMessage::appendAndCompress( list, msg ); + NotificationMessage::appendAndCompress( list, msg ); + + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessage::Add ); + QCOMPARE( list.first().itemParts(), QSet() ); + + list.clear(); + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setOperation( NotificationMessage::Remove ); + msg.setItemParts( QSet() ); + NotificationMessage::appendAndCompress( list, msg ); + + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessage::Remove ); + QCOMPARE( list.first().itemParts(), QSet() ); +} + +void NotificationMessageTest::testNoCompress() +{ + NotificationMessage::List list; + NotificationMessage msg; + msg.setType( NotificationMessage::Item ); + msg.setOperation( NotificationMessage::Modify ); + + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setType( NotificationMessage::Collection ); + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 2 ); +} + +void NotificationMessageTest::testPartModificationMerge_data() +{ + QTest::addColumn( "type" ); + QTest::newRow( "item" ) << NotificationMessage::Item; + QTest::newRow( "collection" ) << NotificationMessage::Collection; +} + +void NotificationMessageTest::testPartModificationMerge() +{ + QFETCH( NotificationMessage::Type, type ); + + NotificationMessage::List list; + NotificationMessage msg; + msg.setType( type ); + msg.setOperation( NotificationMessage::Modify ); + msg.setItemParts( QSet() << "PART1" ); + + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setItemParts( QSet() << "PART2" ); + NotificationMessage::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().itemParts(), ( QSet() << "PART1" << "PART2" ) ); +} diff --git a/akonadi/libs/tests/notificationmessagetest.h b/akonadi/libs/tests/notificationmessagetest.h new file mode 100644 index 00000000..6d8e990a --- /dev/null +++ b/akonadi/libs/tests/notificationmessagetest.h @@ -0,0 +1,38 @@ +/* + 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 testCompressWithItemParts(); + void testNoCompress(); + void testPartModificationMerge_data(); + void testPartModificationMerge(); +}; + +#endif diff --git a/akonadi/libs/tests/notificationmessagev2test.cpp b/akonadi/libs/tests/notificationmessagev2test.cpp new file mode 100644 index 00000000..fb3846b1 --- /dev/null +++ b/akonadi/libs/tests/notificationmessagev2test.cpp @@ -0,0 +1,248 @@ +/* + Copyright (c) 2007 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 "notificationmessagev2test.h" +#include + +#include +#include + +QTEST_APPLESS_MAIN( NotificationMessageV2Test ) + +using namespace Akonadi; + +void NotificationMessageV2Test::testCompress() +{ + NotificationMessageV2::List list; + NotificationMessageV2 msg; + msg.setType( NotificationMessageV2::Items ); + msg.setOperation( NotificationMessageV2::Add ); + + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setOperation( NotificationMessageV2::Modify ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessageV2::Add ); + + msg.setOperation( NotificationMessageV2::Remove ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 2 ); // should be 2 for collections, 0 for items? +} + +void NotificationMessageV2Test::testCompress2() +{ + NotificationMessageV2::List list; + NotificationMessageV2 msg; + msg.setType( NotificationMessageV2::Items ); + msg.setOperation( NotificationMessageV2::Modify ); + + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setOperation( NotificationMessageV2::Remove ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessageV2::Remove ); +} + +void NotificationMessageV2Test::testCompress3() +{ + NotificationMessageV2::List list; + + NotificationMessageV2 msg; + msg.setType( NotificationMessageV2::Items ); + msg.setOperation( NotificationMessageV2::Modify ); + msg.setItemParts( QSet() << "PART1" ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + NotificationMessageV2 msg2; + msg2.setType( NotificationMessageV2::Items ); + msg2.setOperation( NotificationMessageV2::Modify ); + msg2.setItemParts( QSet() << "PART2" ); + NotificationMessageV2::appendAndCompress( list, msg2 ); + + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().itemParts(), QSet() << "PART1" << "PART2" ); +} + +void NotificationMessageV2Test::testCompress4() +{ + NotificationMessageV2::List list; + NotificationMessageV2 msg; + msg.setType( NotificationMessageV2::Items ); + msg.setOperation( NotificationMessageV2::ModifyFlags ); + + QSet set; + set << "FLAG1"; + msg.setAddedFlags( set ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + set.clear(); + msg.setAddedFlags( set ); + set << "FLAG2"; + msg.setRemovedFlags( set ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().addedFlags(), ( QSet() << "FLAG1" ) ); + QCOMPARE( list.first().removedFlags(), ( QSet() << "FLAG2" ) ); +} + +void NotificationMessageV2Test::testCompress5() +{ + NotificationMessageV2::List list; + NotificationMessageV2 msg; + msg.setType( NotificationMessageV2::Items ); + msg.setOperation( NotificationMessageV2::ModifyFlags ); + + msg.setAddedFlags( QSet() << "FLAG1" << "FLAG2" ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + NotificationMessageV2 msg2; + msg2.setType( NotificationMessageV2::Items ); + msg2.setOperation( NotificationMessageV2::Add ); + msg2.setAddedFlags( QSet() ); + NotificationMessageV2::appendAndCompress( list, msg2 ); + + msg.setAddedFlags( QSet() ); + msg.setRemovedFlags( QSet() << "FLAG2" << "FLAG1" ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessageV2::Add ); +} + +void NotificationMessageV2Test::testCompress6() +{ + NotificationMessageV2::List list; + NotificationMessageV2 msg; + msg.setType( NotificationMessageV2::Items ); + msg.setOperation( NotificationMessageV2::ModifyFlags ); + msg.setAddedFlags( QSet() << "FLAG1" ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + NotificationMessageV2 msg2; + msg2.setType( NotificationMessageV2::Items ); + msg2.setOperation( NotificationMessageV2::ModifyFlags ); + msg2.setAddedFlags( QSet() << "FLAG2" ); + NotificationMessageV2::appendAndCompress( list, msg2 ); + + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessageV2::ModifyFlags ); + QCOMPARE( list.first().addedFlags(), QSet() << "FLAG1" << "FLAG2" ); +} + +void NotificationMessageV2Test::testCompress7() +{ + NotificationMessageV2::List list; + NotificationMessageV2 msg; + msg.setType( NotificationMessageV2::Items ); + msg.setOperation( NotificationMessageV2::Modify ); + msg.setItemParts( QSet() << "PART1" ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + NotificationMessageV2 msg2; + msg2.setType( NotificationMessageV2::Items ); + msg2.setOperation( NotificationMessageV2::ModifyFlags ); + msg2.setAddedFlags( QSet() << "FLAG1" ); + NotificationMessageV2::appendAndCompress( list, msg2 ); + + QCOMPARE( list.count(), 2 ); +} + +void NotificationMessageV2Test::testCompressWithItemParts() +{ + NotificationMessageV2::List list; + NotificationMessageV2 msg; + msg.setType( NotificationMessageV2::Items ); + msg.setOperation( NotificationMessageV2::Add ); + + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setOperation( NotificationMessageV2::Modify ); + QSet parts; + parts << "SomePart"; + msg.setAddedFlags( parts ); + NotificationMessageV2::appendAndCompress( list, msg ); + NotificationMessageV2::appendAndCompress( list, msg ); + + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessageV2::Add ); + QCOMPARE( list.first().itemParts(), QSet() ); + + list.clear(); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setOperation( NotificationMessageV2::Remove ); + msg.setItemParts( QSet() ); + NotificationMessageV2::appendAndCompress( list, msg ); + + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().operation(), NotificationMessageV2::Remove ); + QCOMPARE( list.first().itemParts(), QSet() ); +} + +void NotificationMessageV2Test::testNoCompress() +{ + NotificationMessageV2::List list; + NotificationMessageV2 msg; + msg.setType( NotificationMessageV2::Items ); + msg.setOperation( NotificationMessageV2::Modify ); + + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setType( NotificationMessageV2::Collections ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 2 ); +} + +void NotificationMessageV2Test::testPartModificationMerge_data() +{ + QTest::addColumn( "type" ); + QTest::newRow( "item" ) << NotificationMessageV2::Items; + QTest::newRow( "collection" ) << NotificationMessageV2::Collections; +} + +void NotificationMessageV2Test::testPartModificationMerge() +{ + QFETCH( NotificationMessageV2::Type, type ); + + NotificationMessageV2::List list; + NotificationMessageV2 msg; + msg.setType( type ); + msg.setOperation( NotificationMessageV2::Modify ); + msg.setItemParts( QSet() << "PART1" ); + + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + + msg.setItemParts( QSet() << "PART2" ); + NotificationMessageV2::appendAndCompress( list, msg ); + QCOMPARE( list.count(), 1 ); + QCOMPARE( list.first().itemParts(), ( QSet() << "PART1" << "PART2" ) ); +} diff --git a/akonadi/libs/tests/notificationmessagev2test.h b/akonadi/libs/tests/notificationmessagev2test.h new file mode 100644 index 00000000..f8102c58 --- /dev/null +++ b/akonadi/libs/tests/notificationmessagev2test.h @@ -0,0 +1,43 @@ +/* + Copyright (c) 2007 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 AKONADI_NOTIFICATIONMESSAGEV2TEST_H +#define AKONADI_NOTIFICATIONMESSAGEV2TEST_H + +#include + +class NotificationMessageV2Test : public QObject +{ + Q_OBJECT + private Q_SLOTS: + void testCompress(); + void testCompress2(); + void testCompress3(); + void testCompress4(); + void testCompress5(); + void testCompress6(); + void testCompress7(); + void testCompressWithItemParts(); + void testNoCompress(); + void testPartModificationMerge_data(); + void testPartModificationMerge(); +}; + +#endif diff --git a/akonadi/libs/xdgbasedirs.cpp b/akonadi/libs/xdgbasedirs.cpp new file mode 100644 index 00000000..d4001d3f --- /dev/null +++ b/akonadi/libs/xdgbasedirs.cpp @@ -0,0 +1,501 @@ +/*************************************************************************** + * 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 +#include +#include +#include +#include +#include + +#include + +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 +} + +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 ) +{ + if ( qstrncmp( "data", resource, 4 ) == 0 ) { + if ( instance()->mDataHome.isEmpty() ) { + instance()->mDataHome = instance()->homePath( "XDG_DATA_HOME", ".local/share" ); + } + return instance()->mDataHome; + } else if ( qstrncmp( "config", resource, 6 ) == 0 ) { + if ( instance()->mConfigHome.isEmpty() ) { + instance()->mConfigHome = instance()->homePath( "XDG_CONFIG_HOME", ".config" ); + } + return instance()->mConfigHome; + } + + return QString(); +} + +QStringList XdgBaseDirs::systemPathList( const char *resource ) +{ + if ( qstrncmp( "data", resource, 4 ) == 0 ) { + if ( instance()->mDataDirs.isEmpty() ) { +#ifdef Q_OS_WIN + QDir dir( QCoreApplication::applicationDirPath() ); + dir.cdUp(); + const QString defaultPathList = dir.absoluteFilePath( QLatin1String( "share" ) ); + QStringList 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 = QLatin1String( AKONADIDATA ); +#endif + if ( !dataDirs.contains( prefixDataDir ) ) { + dataDirs << prefixDataDir; + } + +#if QT_VERSION < 0x050000 + // fallback for users with KDE in a different prefix and not correctly set up XDG_DATA_DIRS, hi David ;-) + QProcess proc; + // ### should probably rather be --path xdg-something + const QStringList args = QStringList() << QLatin1String( "--prefix" ); + proc.start( QLatin1String( "kde4-config" ), args ); + if ( proc.waitForStarted() && proc.waitForFinished() && proc.exitCode() == 0 ) { + proc.setReadChannel( QProcess::StandardOutput ); + Q_FOREACH ( const QString &basePath, splitPathList( QString::fromLocal8Bit( proc.readLine().trimmed() ) ) ) { + const QString path = basePath + QDir::separator() + QLatin1String( "share" ); + if ( !dataDirs.contains( path ) ) { + dataDirs << path; + } + } + } +#endif + + 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 + QDir dir( QCoreApplication::applicationDirPath() ); + dir.cdUp(); + const QString defaultPathList = dir.absoluteFilePath( QLatin1String( "etc" ) ) + QLatin1Char( ';' ) + dir.absoluteFilePath( QLatin1String( "share/config" ) ); + QStringList 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 = QLatin1String( 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 = QLatin1String( 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 + qWarning() << "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 "/qt4/plugins/:" AKONADILIB "/kde4/:" AKONADILIB "/kde4/plugins/:/usr/lib/qt4/plugins/" ); + + if ( QCoreApplication::instance() != 0 ) { + Q_FOREACH ( const QString &libraryPath, QCoreApplication::instance()->libraryPaths() ) { + if ( !pluginDirs.contains( libraryPath ) ) { + pluginDirs << libraryPath; + } + } + } + + // fallback for users with KDE in a different prefix and not correctly set up XDG_DATA_DIRS, hi David ;-) + QProcess proc; + // ### should probably rather be --path xdg-something + const QStringList args = QStringList() << QLatin1String( "--path" ) << QLatin1String( "module" ); + proc.start( QLatin1String( "kde4-config" ), args ); + if ( proc.waitForStarted() && proc.waitForFinished() && proc.exitCode() == 0 ) { + proc.setReadChannel( QProcess::StandardOutput ); + Q_FOREACH ( const QString &path, splitPathList( QString::fromLocal8Bit( proc.readLine().trimmed() ) ) ) { + if ( !pluginDirs.contains( path ) ) { + pluginDirs.append( path ); + } + } + } + + qWarning() << "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 { + qWarning() << "XdgBaseDirs::saveDir: '" << fileInfo.absoluteFilePath() + << "' exists but is not a directory"; + } + } else { + if ( !QDir::home().mkpath( fileInfo.absoluteFilePath() ) ) { + qWarning() << "XdgBaseDirs::saveDir: failed to create directory '" + << fileInfo.absoluteFilePath() << "'"; + } else { + return fullPath; + } + } + + return QString(); +} + +QString XdgBaseDirs::akonadiServerConfigFile( FileAccessMode openMode ) +{ + return akonadiConfigFile( QLatin1String( "akonadiserverrc" ), openMode ); +} + +QString XdgBaseDirs::akonadiConnectionConfigFile( FileAccessMode openMode ) +{ + return akonadiConfigFile( QLatin1String( "akonadiconnectionrc" ), openMode ); +} + +QString XdgBaseDirs::akonadiConfigFile( const QString &file, FileAccessMode openMode ) +{ + const QString akonadiDir = QLatin1String( "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/akonadi/libs/xdgbasedirs_p.h b/akonadi/libs/xdgbasedirs_p.h new file mode 100644 index 00000000..208b4e99 --- /dev/null +++ b/akonadi/libs/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 "akonadiprotocolinternals_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 AKONADIPROTOCOLINTERNALS_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/akonadi/qsqlite/.no_coding_style b/akonadi/qsqlite/.no_coding_style new file mode 100644 index 00000000..40fa8f60 --- /dev/null +++ b/akonadi/qsqlite/.no_coding_style @@ -0,0 +1 @@ +# this directory will not be tested. diff --git a/akonadi/qsqlite/CMakeLists.txt b/akonadi/qsqlite/CMakeLists.txt new file mode 100644 index 00000000..eb75ef9a --- /dev/null +++ b/akonadi/qsqlite/CMakeLists.txt @@ -0,0 +1,46 @@ +set(QSqlite_SRCS + src/sqlite_blocking.cpp + src/qsql_sqlite.cpp + src/smain.cpp +) + +message(STATUS "Building QSQLITE3 driver") + +if(INSTALL_QSQLITE_IN_QT_PREFIX) + set(QSQLITE_INSTALL_PREFIX "${QT_PLUGINS_DIR}/sqldrivers") +else() + set(QSQLITE_INSTALL_PREFIX "${LIB_INSTALL_DIR}/plugins/sqldrivers") +endif() + +add_definitions(-Wall) +add_definitions(${QT_DEFINITIONS}) +add_definitions(-DQT_PLUGIN) +add_definitions(-DQT_SHARED) + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${SQLITE_INCLUDE_DIR} +) + +if(AKONADI_STATIC_SQLITE) + add_definitions(-DQT_STATICPLUGIN) + add_library(qsqlite3 STATIC ${QSqlite_SRCS} ${QSqlite_MOC_SRCS}) +else() + add_library(qsqlite3 SHARED ${QSqlite_SRCS} ${QSqlite_MOC_SRCS}) +endif() + +target_link_libraries(qsqlite3 ${QT_QTCORE_LIBRARY} ${QT_QTSQL_LIBRARY}) + +if(SQLITE_LINK_STATIC) + message(STATUS "SQlite ${SQLITE_STATIC_LIBRARIES}") + target_link_libraries(qsqlite3 ${SQLITE_STATIC_LIBRARIES}) +else() + message(STATUS "SQlite ${SQLITE_LIBRARIES}") + target_link_libraries(qsqlite3 ${SQLITE_LIBRARIES}) +endif() + +INSTALL(TARGETS qsqlite3 + RUNTIME DESTINATION ${QSQLITE_INSTALL_PREFIX} + LIBRARY DESTINATION ${QSQLITE_INSTALL_PREFIX} + ARCHIVE DESTINATION ${QSQLITE_INSTALL_PREFIX} +) diff --git a/akonadi/qsqlite/README b/akonadi/qsqlite/README new file mode 100644 index 00000000..72864b6c --- /dev/null +++ b/akonadi/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/akonadi/qsqlite/src/QtSql/private/qsqlcachedresult_p.h b/akonadi/qsqlite/src/QtSql/private/qsqlcachedresult_p.h new file mode 100644 index 00000000..ce15cfe7 --- /dev/null +++ b/akonadi/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/akonadi/qsqlite/src/qsql_sqlite.cpp b/akonadi/qsqlite/src/qsql_sqlite.cpp new file mode 100644 index 00000000..5da232f9 --- /dev/null +++ b/akonadi/qsqlite/src/qsql_sqlite.cpp @@ -0,0 +1,724 @@ +/**************************************************************************** +** +** 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 + +#if defined Q_OS_WIN +# include +#else +# include +#endif + +#include + +#include +#include "sqlite_blocking.h" + +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('"'), QLatin1String("\"\"")); + res.prepend(QLatin1Char('"')).append(QLatin1Char('"')); + res.replace(QLatin1Char('.'), QLatin1String("\".\"")); + } + 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.startsWith(QLatin1String("numeric"))) + return QVariant::Double; + if (typeName == QLatin1String("blob")) + return QVariant::ByteArray; + return QVariant::String; +} + +static QSqlError qMakeError(sqlite3 *access, const QString &descr, QSqlError::ErrorType type, + int errorCode = -1) +{ + return QSqlError(descr, + QString::fromUtf16(static_cast(sqlite3_errmsg16(access))), + type, errorCode); +} + +class QSQLiteDriverPrivate +{ +public: + inline QSQLiteDriverPrivate() : access(0) {} + sqlite3 *access; +}; + + +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))); + + int dotIdx = colName.lastIndexOf(QLatin1Char('.')); + QSqlField fld(colName.mid(dotIdx == -1 ? 0 : dotIdx + 1), qGetColumnType(typeName)); + + // sqlite3_column_type is documented to have undefined behavior if the result set is empty + int stp = emptyResultset ? -1 : sqlite3_column_type(stmt, i); + 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::fromUtf16(static_cast( + sqlite3_column_text16(stmt, i)), + sqlite3_column_bytes16(stmt, i) / sizeof(ushort)); + 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_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->access; +} + +QSQLiteResult::~QSQLiteResult() +{ + d->cleanup(); + delete d; +} + +void QSQLiteResult::virtual_hook(int id, void *data) +{ + switch (id) { + case QSqlResult::DetachFromResultSet: + if (d->stmt) + sqlite3_reset(d->stmt); + break; + default: + 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); + +#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, 0); +#else + int res = sqlite3_prepare16(d->access, query.constData(), (query.size() + 1) * sizeof(QChar), + &d->stmt, 0); +#endif + + if (res != SQLITE_OK) { + setLastError(qMakeError(d->access, QCoreApplication::translate("QSQLiteResult", + "Unable to execute statement"), QSqlError::StatementError, res)); + 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: + 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::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; +} + +QVariant QSQLiteResult::handle() const +{ + return qVariantFromValue(d->stmt); +} + +///////////////////////////////////////////////////////// + +QSQLiteDriver::QSQLiteDriver(QObject * parent) + : QSqlDriver(parent) +{ + d = new QSQLiteDriverPrivate(); +} + +QSQLiteDriver::QSQLiteDriver(sqlite3 *connection, QObject *parent) + : QSqlDriver(parent) +{ + d = new QSQLiteDriverPrivate(); + d->access = connection; + setOpen(true); + setOpenError(false); +} + + +QSQLiteDriver::~QSQLiteDriver() +{ + delete d; +} + +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: + return false; + } + return false; +} + +static int qGetSqliteTimeout(QString opts) +{ + enum { DefaultTimeout = 5000 }; + + opts.remove(QLatin1Char(' ')); + Q_FOREACH(const QString &option, opts.split(QLatin1Char(';'))) { + if (option.startsWith(QLatin1String("QSQLITE_BUSY_TIMEOUT="))) { + bool ok; + int nt = option.mid(21).toInt(&ok); + if (ok) + return nt; + } + } + return DefaultTimeout; +} + +static int qGetSqliteOpenMode(QString opts) +{ + opts.remove(QLatin1Char(' ')); + Q_FOREACH(const QString &option, opts.split(QLatin1Char(';'))) { + if (option == QLatin1String("QSQLITE_OPEN_READONLY")) + return SQLITE_OPEN_READONLY; + } + // The SQLITE_OPEN_NOMUTEX flag causes the database connection to be in the multi-thread mode + return SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_SHAREDCACHE; +} + +/* + 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) +{ + if (isOpen()) + close(); + + if (db.isEmpty()) + return false; + + sqlite3_enable_shared_cache(1); + if (sqlite3_open_v2(db.toUtf8().constData(), &d->access, qGetSqliteOpenMode(conOpts), NULL) == SQLITE_OK) { + sqlite3_busy_timeout(d->access, qGetSqliteTimeout(conOpts)); + sqlite3_extended_result_codes(d->access, 1); + setOpen(true); + setOpenError(false); + return true; + } else { + setLastError(qMakeError(d->access, tr("Error opening database"), + QSqlError::ConnectionError)); + setOpenError(true); + return false; + } +} + +void QSQLiteDriver::close() +{ + if (isOpen()) { + 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(QLatin1String("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(QLatin1String("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(QLatin1String("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 = QLatin1String("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(QLatin1String("type='table' OR type='view'")); + else if (type & QSql::Tables) + sql = sql.arg(QLatin1String("type='table'")); + else if (type & QSql::Views) + sql = sql.arg(QLatin1String("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(QLatin1String("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(QLatin1String("PRAGMA ") + schema + QLatin1String("table_info (") + _q_escapeIdentifier(table) + QLatin1String(")")); + + 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 +{ + return qVariantFromValue(d->access); +} + +QString QSQLiteDriver::escapeIdentifier(const QString &identifier, IdentifierType type) const +{ + Q_UNUSED(type); + return _q_escapeIdentifier(identifier); +} + +QT_END_NAMESPACE diff --git a/akonadi/qsqlite/src/qsql_sqlite.h b/akonadi/qsqlite/src/qsql_sqlite.h new file mode 100644 index 00000000..fcf91570 --- /dev/null +++ b/akonadi/qsqlite/src/qsql_sqlite.h @@ -0,0 +1,123 @@ +/**************************************************************************** +** +** 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 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 virtual_hook(int id, void *data); + +private: + QSQLiteResultPrivate* d; +}; + +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: + QSQLiteDriverPrivate* d; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QSQL_SQLITE_H diff --git a/akonadi/qsqlite/src/smain.cpp b/akonadi/qsqlite/src/smain.cpp new file mode 100644 index 00000000..e23560f0 --- /dev/null +++ b/akonadi/qsqlite/src/smain.cpp @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** 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 +{ +public: + QSQLiteDriverPlugin(); + + QSqlDriver* create(const QString &); + QStringList keys() const; +}; + +QSQLiteDriverPlugin::QSQLiteDriverPlugin() + : QSqlDriverPlugin() +{ +} + +QSqlDriver* QSQLiteDriverPlugin::create(const QString &name) +{ + if (name == QLatin1String("QSQLITE3")) { + QSQLiteDriver* driver = new QSQLiteDriver(); + return driver; + } + return 0; +} + +QStringList QSQLiteDriverPlugin::keys() const +{ + QStringList l; + l << QLatin1String("QSQLITE3"); + return l; +} + +Q_EXPORT_STATIC_PLUGIN(QSQLiteDriverPlugin) +Q_EXPORT_PLUGIN2(qsqlite3, QSQLiteDriverPlugin) + +QT_END_NAMESPACE diff --git a/akonadi/qsqlite/src/sqlite_blocking.cpp b/akonadi/qsqlite/src/sqlite_blocking.cpp new file mode 100644 index 00000000..fa42360a --- /dev/null +++ b/akonadi/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/akonadi/qsqlite/src/sqlite_blocking.h b/akonadi/qsqlite/src/sqlite_blocking.h new file mode 100644 index 00000000..9f13946c --- /dev/null +++ b/akonadi/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/akonadi/rds/CMakeLists.txt b/akonadi/rds/CMakeLists.txt new file mode 100644 index 00000000..935e0c0b --- /dev/null +++ b/akonadi/rds/CMakeLists.txt @@ -0,0 +1,19 @@ +project(akonadi_rds) + +#set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_ENABLE_EXCEPTIONS}" ) +set(AKONADI_PROTOCOLINTERNALS_LIBS ${akonadiprotocolinternals_LIB_DEPENDS} akonadiprotocolinternals) + +########### 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 ${QT_QTCORE_LIBRARY} ${QT_QTNETWORK_LIBRARY} ${AKONADI_PROTOCOLINTERNALS_LIBS} ${Boost_PROGRAM_OPTIONS_LIBRARY}) + +install(TARGETS akonadi_rds DESTINATION ${BIN_INSTALL_DIR}) + diff --git a/akonadi/rds/bridgeconnection.cpp b/akonadi/rds/bridgeconnection.cpp new file mode 100644 index 00000000..9a5209a2 --- /dev/null +++ b/akonadi/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( AkStandardDirs::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 = AkStandardDirs::saveDir( "data" ); + const QString path = connectionSettings.value( QLatin1String( "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( QLatin1String( "=(.*)[,$]" ) ); + 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, SIGNAL(disconnected()), SLOT(deleteLater()) ); + connect( m_localSocket, SIGNAL(readyRead()), SLOT(slotDataAvailable()) ); + connect( m_remoteSocket, SIGNAL(readyRead()), SLOT(slotDataAvailable()) ); + connect( m_localSocket, SIGNAL(connected()), SLOT(slotDataAvailable()) ); +} diff --git a/akonadi/rds/bridgeconnection.h b/akonadi/rds/bridgeconnection.h new file mode 100644 index 00000000..22306462 --- /dev/null +++ b/akonadi/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/akonadi/rds/bridgeserver.cpp b/akonadi/rds/bridgeserver.cpp new file mode 100644 index 00000000..d6ccd296 --- /dev/null +++ b/akonadi/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, SIGNAL(newConnection()), SLOT(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/akonadi/rds/bridgeserver.h b/akonadi/rds/bridgeserver.h new file mode 100644 index 00000000..9fe7f35f --- /dev/null +++ b/akonadi/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/akonadi/rds/exception.h b/akonadi/rds/exception.h new file mode 100644 index 00000000..a44f243c --- /dev/null +++ b/akonadi/rds/exception.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * 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 ) +#ifdef QT_NO_STL + : Ex( std::string( qPrintable( message ) ) ) +#else + : Ex( message.toStdString() ) +#endif + { + } + + ~Exception() throw() + { + } +}; + +#endif diff --git a/akonadi/rds/main.cpp b/akonadi/rds/main.cpp new file mode 100644 index 00000000..3d7d25ef --- /dev/null +++ b/akonadi/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( QLatin1String( "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/akonadi/server/AkonadiServerProtocol.txt b/akonadi/server/AkonadiServerProtocol.txt new file mode 100644 index 00000000..1fb6ae10 --- /dev/null +++ b/akonadi/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/akonadi/server/CMakeLists.txt b/akonadi/server/CMakeLists.txt new file mode 100644 index 00000000..e4829f3b --- /dev/null +++ b/akonadi/server/CMakeLists.txt @@ -0,0 +1,294 @@ +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() + +include_directories(${Akonadi_SOURCE_DIR}/libs) +include_directories(${Akonadi_BINARY_DIR}/libs) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/handler) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/nepomuk) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/tests) +include_directories(${Akonadi_SOURCE_DIR}/shared) +if(Soprano_FOUND) + include_directories(${SOPRANO_INCLUDE_DIR}) +endif() +include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) +include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}) + +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}/server/src/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}/server/src/storage/schema.xsl + ${_schemaXml} + DEPENDS ${Akonadi_SOURCE_DIR}/server/src/storage/schema.xsl + ${Akonadi_SOURCE_DIR}/server/src/storage/schema-header.xsl + ${Akonadi_SOURCE_DIR}/server/src/storage/schema-source.xsl + ${_schemaXml} +) +endmacro() + + +add_subdirectory(akonadictl) +add_subdirectory(control) +add_subdirectory(src) +add_subdirectory(tests) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_ENABLE_EXCEPTIONS}") +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}/src/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}/src/storage/entities.xsl + ${AKONADI_DB_SCHEME} + COMMAND ${XSLTPROC_EXECUTABLE} + --output ${CMAKE_CURRENT_BINARY_DIR}/entities.cpp + --stringparam code source + ${CMAKE_CURRENT_SOURCE_DIR}/src/storage/entities.xsl + ${AKONADI_DB_SCHEME} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/storage/entities.xsl + ${CMAKE_CURRENT_SOURCE_DIR}/src/storage/entities-header.xsl + ${CMAKE_CURRENT_SOURCE_DIR}/src/storage/entities-source.xsl + ${AKONADI_DB_SCHEME} +) + +akonadi_generate_schema(${AKONADI_DB_SCHEME} AkonadiSchema akonadischema) + +set(libakonadiprivate_SRCS + src/akonadi.cpp + src/commandcontext.cpp + src/connection.cpp + src/connectionthread.cpp + src/collectionscheduler.cpp + src/clientcapabilities.cpp + src/clientcapabilityaggregator.cpp + src/dbusconnectionpool.cpp + src/handler.cpp + src/handlerhelper.cpp + src/intervalcheck.cpp + src/response.cpp + src/collectionreferencemanager.cpp + src/handler/akappend.cpp + src/handler/append.cpp + src/handler/copy.cpp + src/handler/colcopy.cpp + src/handler/colmove.cpp + src/handler/create.cpp + src/handler/capability.cpp + src/handler/delete.cpp + src/handler/expunge.cpp + src/handler/fetch.cpp + src/handler/fetchhelper.cpp + src/handler/fetchscope.cpp + src/handler/link.cpp + src/handler/list.cpp + src/handler/login.cpp + src/handler/logout.cpp + src/handler/merge.cpp + src/handler/modify.cpp + src/handler/move.cpp + src/handler/remove.cpp + src/handler/resourceselect.cpp + src/handler/scope.cpp + src/handler/search.cpp + src/handler/searchhelper.cpp + src/handler/searchpersistent.cpp + src/handler/searchresult.cpp + src/handler/select.cpp + src/handler/subscribe.cpp + src/handler/status.cpp + src/handler/store.cpp + src/handler/tagappend.cpp + src/handler/tagfetch.cpp + src/handler/tagfetchhelper.cpp + src/handler/tagremove.cpp + src/handler/tagstore.cpp + src/handler/transaction.cpp + src/search/agentsearchengine.cpp + src/search/agentsearchinstance.cpp + src/search/searchtaskmanager.cpp + src/search/searchtaskmanagerthread.cpp + src/search/searchrequest.cpp + src/search/searchmanager.cpp + + src/storage/collectionqueryhelper.cpp + src/storage/entity.cpp + ${CMAKE_CURRENT_BINARY_DIR}/entities.cpp + ${CMAKE_CURRENT_BINARY_DIR}/akonadischema.cpp + src/storage/datastore.cpp + src/storage/dbconfig.cpp + src/storage/dbconfigmysql.cpp + src/storage/dbconfigpostgresql.cpp + src/storage/dbconfigsqlite.cpp + src/storage/dbexception.cpp + src/storage/dbinitializer.cpp + src/storage/dbinitializer_p.cpp + src/storage/dbintrospector.cpp + src/storage/dbintrospector_impl.cpp + src/storage/dbupdater.cpp + src/storage/dbtype.cpp + src/storage/itemqueryhelper.cpp + src/storage/itemretriever.cpp + src/storage/itemretrievalmanager.cpp + src/storage/itemretrievalthread.cpp + src/storage/itemretrievaljob.cpp + src/storage/notificationcollector.cpp + src/storage/parthelper.cpp + src/storage/parttypehelper.cpp + src/storage/query.cpp + src/storage/querybuilder.cpp + src/storage/querycache.cpp + src/storage/queryhelper.cpp + src/storage/schematypes.cpp + src/storage/transaction.cpp + src/storage/parthelper.cpp + src/storage/partstreamer.cpp + src/storage/storagedebugger.cpp + src/tracer.cpp + src/utils.cpp + src/dbustracer.cpp + src/filetracer.cpp + src/notificationmanager.cpp + src/notificationsource.cpp + src/resourcemanager.cpp + src/cachecleaner.cpp + src/debuginterface.cpp + src/imapstreamparser.cpp + src/preprocessorinstance.cpp + src/preprocessormanager.cpp + src/storagejanitor.cpp +) + +if (Soprano_FOUND) + set(libakonadiprivate_SRCS + ${libakonadiprivate_SRCS} + src/nepomuksearch.cpp + src/search/nepomuksearchengine.cpp + src/nepomuk/dbusoperators.cpp + src/nepomuk/queryserviceclient.cpp + src/nepomuk/result.cpp + ) + + set_source_files_properties(src/nepomuk/org.kde.nepomuk.QueryService.xml PROPERTIES INCLUDE "dbusoperators.h") + set_source_files_properties(src/nepomuk/org.kde.nepomuk.Query.xml PROPERTIES INCLUDE "result.h") + + qt4_add_dbus_interface(libakonadiprivate_SRCS src/nepomuk/org.kde.nepomuk.QueryService.xml queryserviceinterface) + qt4_add_dbus_interface(libakonadiprivate_SRCS src/nepomuk/org.kde.nepomuk.Query.xml queryinterface) +endif() + +qt4_generate_dbus_interface(src/debuginterface.h org.freedesktop.Akonadi.DebugInterface.xml) + +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.TracerNotification.xml dbustracer.h Akonadi::Server::DBusTracer) +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Tracer.xml tracer.h Akonadi::Server::Tracer) +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.NotificationManager.xml notificationmanager.h Akonadi::Server::NotificationManager) +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Server.xml akonadi.h Akonadi::Server::AkonadiServer) +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.NotificationSource.xml notificationsource.h Akonadi::Server::NotificationSource) +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml storage/storagedebugger.h Akonadi::Server::StorageDebugger) +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.DebugInterface.xml debuginterface.h Akonadi::Server::DebugInterface) +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.ResourceManager.xml resourcemanager.h Akonadi::Server::ResourceManager) +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml preprocessormanager.h Akonadi::Server::PreprocessorManager) +qt4_add_dbus_adaptor(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.SearchManager.xml search/searchmanager.h Akonadi::Server::SearchManager) +qt4_add_dbus_interfaces(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.AgentManager.xml ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Resource.xml) +qt4_add_dbus_interface(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Preprocessor.xml preprocessorinterface) +qt4_add_dbus_interface(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Agent.Control.xml agentcontrolinterface) +qt4_add_dbus_interface(libakonadiprivate_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Agent.Search.xml agentsearchinterface) + +qt4_add_resources(libakonadiprivate_SRCS src/storage/akonadidb.qrc) + + +add_library(akonadiprivate STATIC ${libakonadiprivate_SRCS}) + +if(QT5_BUILD) + # TODO: Remove when Qt 4 is no longer supported. + set_property(TARGET akonadiprivate APPEND PROPERTY COMPILE_DEFINITIONS QT_DISABLE_DEPRECATED_BEFORE=0) +endif() + +target_link_libraries(akonadiprivate + akonadi_shared + ${QT_QTCORE_LIBRARY} + ${QT_QTNETWORK_LIBRARY} + ${QT_QTSQL_LIBRARY} + ${QT_QTDBUS_LIBRARY} + ${QT_QTXML_LIBRARY} + ${AKONADI_PROTOCOLINTERNALS_LIBS} + ${AKONADI_SYSTEM_LIBS} + ${Boost_PROGRAM_OPTIONS_LIBRARY} +) + +if(Soprano_FOUND) + target_link_libraries(akonadiprivate ${SOPRANO_LIBRARIES}) +endif() + +set(akonadiserver_bin_SRCS src/main.cpp) + +if(AKONADI_STATIC_SQLITE) + add_definitions(-DQT_STATICPLUGIN) +endif() + +add_executable(akonadiserver_bin ${akonadiserver_bin_SRCS}) +set_target_properties(akonadiserver_bin PROPERTIES OUTPUT_NAME akonadiserver) +target_link_libraries(akonadiserver_bin akonadi_shared akonadiprivate ${QT_QTCORE_LIBRARY}) +if(AKONADI_STATIC_SQLITE) + target_link_libraries(akonadiserver_bin qsqlite3) +endif() + +install(TARGETS akonadiserver_bin DESTINATION ${BIN_INSTALL_DIR}) +install(FILES + src/storage/mysql-global.conf + src/storage/mysql-global-mobile.conf + DESTINATION ${CONFIG_INSTALL_DIR}/akonadi) + +install(FILES + src/search/abstractsearchplugin.h + DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi) + +## DBus XML files +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.DebugInterface.xml +DESTINATION ${DBUS_INTERFACES_INSTALL_DIR}) diff --git a/akonadi/server/akonadictl/CMakeLists.txt b/akonadi/server/akonadictl/CMakeLists.txt new file mode 100644 index 00000000..b5967bc5 --- /dev/null +++ b/akonadi/server/akonadictl/CMakeLists.txt @@ -0,0 +1,27 @@ +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_ENABLE_EXCEPTIONS}") +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +########### next target ############### + +set(akonadictl_SRCS + akonadistarter.cpp + main.cpp +) + +qt4_add_dbus_interfaces(akonadictl_SRCS + ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.ControlManager.xml +) + +add_executable(akonadictl ${akonadictl_SRCS}) +set_target_properties(akonadictl PROPERTIES OUTPUT_NAME akonadictl) +target_link_libraries(akonadictl + akonadi_shared + akonadiprotocolinternals + ${QT_QTCORE_LIBRARY} + ${QT_QTDBUS_LIBRARY} + ${AKONADI_PROTOCOLINTERNALS_LIBS} + ${AKONADI_SYSTEM_LIBS} + ${Boost_PROGRAM_OPTIONS_LIBRARY} +) + +install(TARGETS akonadictl DESTINATION ${BIN_INSTALL_DIR}) diff --git a/akonadi/server/akonadictl/akonadistarter.cpp b/akonadi/server/akonadictl/akonadistarter.cpp new file mode 100644 index 00000000..be519601 --- /dev/null +++ b/akonadi/server/akonadictl/akonadistarter.cpp @@ -0,0 +1,85 @@ +/* + 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 + +AkonadiStarter::AkonadiStarter( QObject *parent ) + : QObject( parent ) + , mRegistered( false ) +{ + QDBusServiceWatcher *watcher = new QDBusServiceWatcher( AkDBus::serviceName( AkDBus::ControlLock ), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForOwnerChange, this ); + + connect( watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)), + this, SLOT(serviceOwnerChanged(QString,QString,QString)) ); +} + +bool AkonadiStarter::start() +{ + akDebug() << "Starting Akonadi Server..."; + + QStringList serverArgs; + if ( AkApplication::hasInstanceIdentifier() ) { + serverArgs << QLatin1String( "--instance" ) << AkApplication::instanceIdentifier(); + } + + const bool ok = QProcess::startDetached( QLatin1String( "akonadi_control" ), serverArgs ); + if ( !ok ) { + akError() << "Error: unable to execute binary akonadi_control"; + return false; + } + + // safety timeout + QTimer::singleShot( 5000, QCoreApplication::instance(), SLOT(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/akonadi/server/akonadictl/akonadistarter.h b/akonadi/server/akonadictl/akonadistarter.h new file mode 100644 index 00000000..d5dd1937 --- /dev/null +++ b/akonadi/server/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/akonadi/server/akonadictl/main.cpp b/akonadi/server/akonadictl/main.cpp new file mode 100644 index 00000000..633a9dfd --- /dev/null +++ b/akonadi/server/akonadictl/main.cpp @@ -0,0 +1,231 @@ +/*************************************************************************** + * 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 + +#include +#include +#include +#include "protocol_p.h" + +#include "controlmanagerinterface.h" +#include "akonadistarter.h" +#include "xdgbasedirs_p.h" +#include + +#if defined(HAVE_UNISTD_H) && !defined(Q_WS_WIN) +#include +#else +#define WIN32_LEAN_AND_MEAN +#include +#endif + +static bool startServer() +{ + if ( QDBusConnection::sessionBus().interface()->isServiceRegistered( AkDBus::serviceName( AkDBus::Control ) ) + || QDBusConnection::sessionBus().interface()->isServiceRegistered( AkDBus::serviceName( AkDBus::Server ) ) ) { + qWarning() << "Akonadi is already running."; + return false; + } + AkonadiStarter starter; + return starter.start(); +} + +static bool stopServer() +{ + org::freedesktop::Akonadi::ControlManager iface( AkDBus::serviceName( AkDBus::Control ), + QLatin1String( "/ControlManager" ), + QDBusConnection::sessionBus(), 0 ); + if ( !iface.isValid() ) { + qWarning() << "Akonadi is not running."; + return false; + } + + iface.shutdown(); + + return true; +} + +static bool checkAkonadiControlStatus() +{ + const bool registered = QDBusConnection::sessionBus().interface()->isServiceRegistered( AkDBus::serviceName( AkDBus::Control ) ); + fprintf( stderr, "Akonadi Control: %s\n", registered ? "running" : "stopped" ); + return registered; +} + +static bool checkAkonadiServerStatus() +{ + const bool registered = QDBusConnection::sessionBus().interface()->isServiceRegistered( AkDBus::serviceName( AkDBus::Server ) ); + fprintf( stderr, "Akonadi Server: %s\n", registered ? "running" : "stopped" ); + return registered; +} + +static bool checkSearchSupportStatus() +{ + QStringList searchMethods; + searchMethods << QLatin1String( "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() << QLatin1String( "*.desktop" ), QDir::Files ); + Q_FOREACH ( const QString &desktopFileName, desktopFiles ) { + QSettings desktop( pluginDir + QLatin1String( "/akonadi/" ) + desktopFileName, QSettings::IniFormat ); + desktop.beginGroup( QLatin1String( "Desktop Entry" ) ); + if ( desktop.value( QLatin1String( "Type" ) ).toString() != QLatin1String( "AkonadiSearchPlugin" ) ) { + continue; + } + if ( !desktop.value( QLatin1String( "X-Akonadi-LoadByDefault" ), true ).toBool() ) { + continue; + } + + searchMethods << desktop.value( QLatin1String( "Name" ) ).toString(); + } + } + } + + // There's always at least server-search available + fprintf( stderr, "Akonadi Server Search Support: available (%s)\n", qPrintable( searchMethods.join( QLatin1String( ", " ) ) ) ); + return true; +} + +static bool checkAvailableAgentTypes() +{ + const QStringList dirs = Akonadi::XdgBaseDirs::findAllResourceDirs( "data", QLatin1String( "akonadi/agents" ) ); + QStringList types; + Q_FOREACH ( const QString &pluginDir, dirs ) { + QDir dir( pluginDir ); + const QStringList plugins = dir.entryList( QStringList() << QLatin1String( "*.desktop" ), QDir::Files ); + Q_FOREACH ( const QString &plugin, plugins ) { + QSettings pluginInfo( pluginDir + QLatin1String("/") + plugin, QSettings::IniFormat ); + pluginInfo.beginGroup( QLatin1String( "Desktop Entry" ) ); + types << pluginInfo.value( QLatin1String( "X-Akonadi-Identifier" ) ).toString(); + } + } + + // Remove duplicates from multiple pluginDirs + types.removeDuplicates(); + types.sort(); + + fprintf( stderr, "Available Agent Types: "); + if ( types.isEmpty() ) { + fprintf( stderr, "No agent types found! \n"); + } else { + fprintf( stderr, "%s\n", qPrintable( types.join( QLatin1String( ", " ) ) ) ); + } + + return true; +} + +static bool statusServer() +{ + checkAkonadiControlStatus(); + checkAkonadiServerStatus(); + checkSearchSupportStatus(); + checkAvailableAgentTypes(); + return true; +} + +int main( int argc, char **argv ) +{ + AkCoreApplication app( argc, argv ); + app.setDescription( QLatin1String( "Akonadi server manipulation tool\n" + "Usage: akonadictl [command]\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 space!)\n" + " fsck : Check (and attempt to fix) consistency of the internal storage (can take some time)" ) ); + + app.parseCommandLine(); + + QStringList optionsList; + optionsList.append( QLatin1String( "start" ) ); + optionsList.append( QLatin1String( "stop" ) ); + optionsList.append( QLatin1String( "status" ) ); + optionsList.append( QLatin1String( "restart" ) ); + optionsList.append( QLatin1String( "vacuum" ) ); + optionsList.append( QLatin1String( "fsck" ) ); + + QStringList arguments = QCoreApplication::instance()->arguments(); + if ( AkApplication::hasInstanceIdentifier() ) { // HACK: we should port all of this to boost::program_options... + arguments.removeFirst(); + arguments.removeFirst(); + } + if ( arguments.count() != 2 ) { + app.printUsage(); + return 1; + } else if ( !optionsList.contains( arguments[1] ) ) { + app.printUsage(); + return 2; + } + + if ( arguments[1] == QLatin1String( "start" ) ) { + if ( !startServer() ) { + return 3; + } + } else if ( arguments[1] == QLatin1String( "stop" ) ) { + if ( !stopServer() ) { + return 4; + } + } else if ( arguments[1] == QLatin1String( "status" ) ) { + if ( !statusServer() ) { + return 5; + } + } else if ( arguments[1] == 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( AkDBus::serviceName( AkDBus::Control ) ) ); + if ( !startServer() ) { + return 3; + } + } + } else if ( arguments[1] == QLatin1String( "vacuum" ) ) { + QDBusInterface iface( AkDBus::serviceName( AkDBus::StorageJanitor ), QLatin1String( AKONADI_DBUS_STORAGEJANITOR_PATH ) ); + iface.call( QDBus::NoBlock, QLatin1String( "vacuum" ) ); + } else if ( arguments[1] == QLatin1String( "fsck" ) ) { + QDBusInterface iface( AkDBus::serviceName( AkDBus::StorageJanitor ), QLatin1String( AKONADI_DBUS_STORAGEJANITOR_PATH ) ); + iface.call( QDBus::NoBlock, QLatin1String( "check" ) ); + } + return 0; +} diff --git a/akonadi/server/control/CMakeLists.txt b/akonadi/server/control/CMakeLists.txt new file mode 100644 index 00000000..547876cf --- /dev/null +++ b/akonadi/server/control/CMakeLists.txt @@ -0,0 +1,61 @@ +include_directories(BEFORE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${Boost_INCLUDE_DIR} +) + +#find_library( AKONADI_PROTOCOLINTERNALS_LIBRARY NAMES akonadiprotocolinternals +# PATHS +# ${CMAKE_LIBRARY_PATH} +# ${CMAKE_INSTALL_PREFIX}/lib +#) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_ENABLE_EXCEPTIONS}") + +########### next target ############### + +set(control_SRCS + agenttype.cpp + agentinstance.cpp + agentprocessinstance.cpp + agentthreadinstance.cpp + agentmanager.cpp + controlmanager.cpp + main.cpp + processcontrol.cpp +) + +qt4_add_dbus_adaptor(control_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.AgentManager.xml agentmanager.h AgentManager) +qt4_add_dbus_adaptor(control_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.ControlManager.xml controlmanager.h ControlManager) +qt4_add_dbus_adaptor(control_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml agentmanager.h AgentManager) +qt4_add_dbus_interfaces(control_SRCS + ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Agent.Control.xml + ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Agent.Status.xml + ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Agent.Search.xml + ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.AgentServer.xml + ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Resource.xml + ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Preprocessor.xml + ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.Server.xml +) +qt4_add_dbus_interface(control_SRCS ${Akonadi_SOURCE_DIR}/interfaces/org.freedesktop.Akonadi.ResourceManager.xml resource_manager) +qt4_add_dbus_interface(control_SRCS ${Akonadi_SOURCE_DIR}/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 + ${QT_QTCORE_LIBRARY} + ${QT_QTDBUS_LIBRARY} + ${AKONADI_PROTOCOLINTERNALS_LIBS} + ${AKONADI_SYSTEM_LIBS} + ${Boost_PROGRAM_OPTIONS_LIBRARY} +) + +install(TARGETS akonadi_control DESTINATION ${BIN_INSTALL_DIR}) + +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 ${CMAKE_INSTALL_PREFIX}/share/dbus-1/services) diff --git a/akonadi/server/control/agentinstance.cpp b/akonadi/server/control/agentinstance.cpp new file mode 100644 index 00000000..07ae9ff1 --- /dev/null +++ b/akonadi/server/control/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( AkDBus::Agent, "/" ); + mAgentStatusInterface = + findInterface( AkDBus::Agent, "/" ); + + if ( mPendingQuit && mAgentControlInterface && mAgentControlInterface->isValid() ) { + mAgentControlInterface->quit(); + mPendingQuit = false; + } + + if ( !mAgentControlInterface || !mAgentStatusInterface ) { + return false; + } + + mSearchInterface = + findInterface( AkDBus::Agent, "/Search" ); + + connect( mAgentStatusInterface, SIGNAL(status(int,QString)), SLOT(statusChanged(int,QString)) ); + connect( mAgentStatusInterface, SIGNAL(advancedStatus(QVariantMap)), SLOT(advancedStatusChanged(QVariantMap)) ); + connect( mAgentStatusInterface, SIGNAL(percent(int)), SLOT(percentChanged(int)) ); + connect( mAgentStatusInterface, SIGNAL(warning(QString)), SLOT(warning(QString)) ); + connect( mAgentStatusInterface, SIGNAL(error(QString)), SLOT(error(QString)) ); + connect( mAgentStatusInterface, SIGNAL(onlineChanged(bool)), SLOT(onlineChanged(bool)) ); + + refreshAgentStatus(); + return true; +} + +bool AgentInstance::obtainResourceInterface() +{ + delete mResourceInterface; + mResourceInterface = + findInterface( AkDBus::Resource, "/" ); + + if ( !mResourceInterface ) { + return false; + } + + connect( mResourceInterface, SIGNAL(nameChanged(QString)), SLOT(resourceNameChanged(QString)) ); + refreshResourceStatus(); + return true; +} + +bool AgentInstance::obtainPreprocessorInterface() +{ + delete mPreprocessorInterface; + mPreprocessorInterface = + findInterface( AkDBus::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( QLatin1String( "status" ), QList(), + this, SLOT(statusStateChanged(int)), + SLOT(errorHandler(QDBusError)) ); + mAgentStatusInterface->callWithCallback( QLatin1String( "statusMessage" ), QList(), + this, SLOT(statusMessageChanged(QString)), + SLOT(errorHandler(QDBusError)) ); + mAgentStatusInterface->callWithCallback( QLatin1String( "progress" ), QList(), + this, SLOT(percentChanged(int)), + SLOT(errorHandler(QDBusError)) ); + mAgentStatusInterface->callWithCallback( QLatin1String( "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( QLatin1String( "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() << QString::fromLatin1( "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( AkDBus::AgentType agentType, const char *path ) +{ + T *iface = new T( AkDBus::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/akonadi/server/control/agentinstance.h b/akonadi/server/control/agentinstance.h new file mode 100644 index 00000000..25f53302 --- /dev/null +++ b/akonadi/server/control/agentinstance.h @@ -0,0 +1,129 @@ +/* + 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 boost::shared_ptr 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( AkDBus::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/akonadi/server/control/agentmanager.cpp b/akonadi/server/control/agentmanager.cpp new file mode 100644 index 00000000..ab383577 --- /dev/null +++ b/akonadi/server/control/agentmanager.cpp @@ -0,0 +1,887 @@ +/*************************************************************************** + * 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 "akdebug.h" +#include "libs/protocol_p.h" +#include "libs/xdgbasedirs_p.h" +#include "preprocessor_manager.h" +#include "processcontrol.h" +#include "resource_manager.h" +#include "serverinterface.h" + +#include +#include +#include + +#include +#include +#include +#ifndef QT_NO_DEBUG +#include +#endif +#include +#include +#include + +#include + +using Akonadi::ProcessControl; + +#if defined(Q_WS_MAEMO_5) +static bool enableAgentServerDefault = true; +#else +static bool enableAgentServerDefault = false; +#endif + +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( QLatin1String( "/AgentManager" ), this ); + + connect( QDBusConnection::sessionBus().interface(), SIGNAL(serviceOwnerChanged(QString,QString,QString)), + this, SLOT(serviceOwnerChanged(QString,QString,QString)) ); + + if ( QDBusConnection::sessionBus().interface()->isServiceRegistered( AkDBus::serviceName( AkDBus::Server ) ) ) { + akFatal() << "akonadiserver already running!"; + } + + const QSettings settings( AkStandardDirs::agentConfigFile( Akonadi::XdgBaseDirs::ReadOnly ), QSettings::IniFormat ); + mAgentServerEnabled = settings.value( QLatin1String( "AgentServer/Enabled" ), enableAgentServerDefault ).toBool(); + + QStringList serviceArgs; + if ( AkApplication::hasInstanceIdentifier() ) { + serviceArgs << QLatin1String( "--instance" ) << AkApplication::instanceIdentifier(); + } + + mStorageController = new Akonadi::ProcessControl; + mStorageController->setShutdownTimeout( 15 * 1000 ); // the server needs more time for shutdown if we are using an internal mysqld + connect( mStorageController, SIGNAL(unableToStart()), SLOT(serverFailure()) ); + mStorageController->start( QLatin1String( "akonadiserver" ), serviceArgs, Akonadi::ProcessControl::RestartOnCrash ); + + if ( mAgentServerEnabled ) { + mAgentServer = new Akonadi::ProcessControl; + connect( mAgentServer, SIGNAL(unableToStart()), SLOT(agentServerFailure()) ); + mAgentServer->start( QLatin1String( "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( AkDBus::serviceName( AkDBus::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" << AkDBus::serviceName( AkDBus::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( AkDBus::serviceName( AkDBus::Server ), QLatin1String( "/Server" ), + QDBusConnection::sessionBus(), this ); + serverIface->quit(); + + if ( mAgentServer ) { + mAgentServer->setCrashPolicy( ProcessControl::StopOnCrash ); + org::freedesktop::Akonadi::AgentServer *agentServerIface = + new org::freedesktop::Akonadi::AgentServer( AkDBus::serviceName( AkDBus::AgentServer ), + QLatin1String( "/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 QString &language ) const +{ + if ( !checkAgentExists( identifier ) ) { + return QString(); + } + + const QString name = mAgents.value( identifier ).name.value( language ); + return name.isEmpty() ? mAgents.value( identifier ).name.value( QLatin1String( "en_US" ) ) : name; +} + +QString AgentManager::agentComment( const QString &identifier, const QString &language ) const +{ + if ( !checkAgentExists( identifier ) ) { + return QString(); + } + + const QString comment = mAgents.value( identifier ).comment.value( language ); + return comment.isEmpty() ? mAgents.value( identifier ).comment.value( QLatin1String( "en_US" ) ) : 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 QLatin1String( "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( QString::fromLatin1( "%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( AkDBus::serviceName( AkDBus::Server ), QLatin1String( "/ResourceManager" ), QDBusConnection::sessionBus(), this ); + resmanager.removeResourceInstance( instance->identifier() ); + + // Kill the preprocessor instance, if any. + org::freedesktop::Akonadi::PreprocessorManager preProcessorManager( + AkDBus::serviceName( AkDBus::Server ), + QLatin1String( "/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, QLatin1String( "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, QLatin1String( "setAgentInstanceOnline" ) ) ) { + return; + } + + mAgentInstances.value( identifier )->statusInterface()->setOnline( state ); +} + +// resource specific methods // +void AgentManager::setAgentInstanceName( const QString &identifier, const QString &name ) +{ + if ( !checkResourceInterface( identifier, QLatin1String( "setAgentInstanceName" ) ) ) { + return; + } + + mAgentInstances.value( identifier )->resourceInterface()->setName( name ); +} + +QString AgentManager::agentInstanceName( const QString &identifier, const QString &language ) 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(); + } + + const QString name = mAgents.value( instance->agentType() ).name.value( language ); + return name.isEmpty() ? mAgents.value( instance->agentType() ).name.value( QLatin1String( "en_US" ) ) : name; +} + +void AgentManager::agentInstanceSynchronize( const QString &identifier ) +{ + if ( !checkResourceInterface( identifier, QLatin1String( "agentInstanceSynchronize" ) ) ) { + return; + } + + mAgentInstances.value( identifier )->resourceInterface()->synchronize(); +} + +void AgentManager::agentInstanceSynchronizeCollectionTree( const QString &identifier ) +{ + if ( !checkResourceInterface( identifier, QLatin1String( "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, QLatin1String( "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, QLatin1String( "*.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", QLatin1String( "akonadi/agents" ) ); +} + +void AgentManager::load() +{ + org::freedesktop::Akonadi::ResourceManager resmanager( AkDBus::serviceName( AkDBus::Server ), QLatin1String( "/ResourceManager" ), QDBusConnection::sessionBus(), this ); + const QStringList knownResources = resmanager.resourceInstances(); + + QSettings file( AkStandardDirs::agentConfigFile( Akonadi::XdgBaseDirs::ReadOnly ), QSettings::IniFormat ); + file.beginGroup( QLatin1String( "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( QLatin1String( "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( AkStandardDirs::agentConfigFile( Akonadi::XdgBaseDirs::WriteOnly ), QSettings::IniFormat ); + + Q_FOREACH ( const AgentType &info, mAgents ) { + info.save( &file ); + } + + file.beginGroup( QLatin1String( "Instances" ) ); + file.remove( QString() ); + Q_FOREACH ( const AgentInstance::Ptr &instance, mAgentInstances ) { + file.beginGroup( instance->identifier() ); + file.setValue( QLatin1String( "AgentType" ), instance->agentType() ); + file.endGroup(); + } + + file.endGroup(); + + file.sync(); +} + +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 == AkDBus::serviceName( AkDBus::Server ) || name == AkDBus::serviceName( AkDBus::AgentServer ) ) && !newOwner.isEmpty() ) { + if ( QDBusConnection::sessionBus().interface()->isServiceRegistered( AkDBus::serviceName( AkDBus::Server ) ) + && ( !mAgentServer || QDBusConnection::sessionBus().interface()->isServiceRegistered( AkDBus::serviceName( AkDBus::AgentServer ) ) ) ) { + // server is operational, start agents + continueStartup(); + } + } + + AkDBus::AgentType agentType = AkDBus::Unknown; + const QString agentIdentifier = AkDBus::parseAgentServiceName( name, agentType ); + switch ( agentType ) { + case AkDBus::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 AkDBus::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 AkDBus::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( + AkDBus::serviceName( AkDBus::Server ), + QLatin1String( "/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( QLatin1String( "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( AkDBus::serviceName( AkDBus::AgentServer ), + QLatin1String( "/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 ) ) { + boost::scoped_ptr resmanager( + new org::freedesktop::Akonadi::ResourceManager( AkDBus::serviceName( AkDBus::Server ), + QLatin1String( "/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/akonadi/server/control/agentmanager.h b/akonadi/server/control/agentmanager.h new file mode 100644 index 00000000..a0c1df5e --- /dev/null +++ b/akonadi/server/control/agentmanager.h @@ -0,0 +1,388 @@ +/*************************************************************************** + * 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 and the given @p language. + */ + QString agentName( const QString &identifier, const QString &language = QLatin1String( "en_US" ) ) const; + + /** + * Returns the i18n'ed comment of the agent type for + * the given @p identifier and the given @p language. + */ + QString agentComment( const QString &identifier, const QString &language = QLatin1String( "en_US" ) ) 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. + * If there is no name, it returns the default name of the agent, you can tweak + * the language for that name with the @p language setting. + */ + QString agentInstanceName( const QString &identifier, const QString &language = QLatin1String( "en_US" ) ) 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/akonadi/server/control/agentprocessinstance.cpp b/akonadi/server/control/agentprocessinstance.cpp new file mode 100644 index 00000000..d15fe6b4 --- /dev/null +++ b/akonadi/server/control/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 "libs/xdgbasedirs_p.h" +#include "shared/akdebug.h" + +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, SIGNAL(unableToStart()), SLOT(failedToStart()) ); + + if ( agentInfo.launchMethod == AgentType::Process ) { + QStringList arguments; + arguments << QLatin1String( "--identifier" ) << identifier(); + mController->start( executable, arguments ); + } else { + Q_ASSERT( agentInfo.launchMethod == AgentType::Launcher ); + const QStringList arguments = QStringList() << executable << identifier(); + const QString agentLauncherExec = XdgBaseDirs::findExecutableFile( QLatin1String( "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*/, QLatin1String( "Unable to start." ) ); +} diff --git a/akonadi/server/control/agentprocessinstance.h b/akonadi/server/control/agentprocessinstance.h new file mode 100644 index 00000000..00dae8bd --- /dev/null +++ b/akonadi/server/control/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/akonadi/server/control/agentthreadinstance.cpp b/akonadi/server/control/agentthreadinstance.cpp new file mode 100644 index 00000000..aa75f3e5 --- /dev/null +++ b/akonadi/server/control/agentthreadinstance.cpp @@ -0,0 +1,92 @@ +/* + 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( AkDBus::serviceName( AkDBus::AgentServer ), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration, this ); + connect( watcher, SIGNAL(serviceRegistered(QString)), + this, SLOT(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( AkDBus::serviceName( AkDBus::AgentServer ), + QLatin1String( "/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( AkDBus::serviceName( AkDBus::AgentServer ), + QLatin1String( "/AgentServer" ), QDBusConnection::sessionBus() ); + agentServer.stopAgent( identifier() ); +} + +void AgentThreadInstance::restartWhenIdle() +{ + if ( status() != 1 && !identifier().isEmpty() ) { + org::freedesktop::Akonadi::AgentServer agentServer( AkDBus::serviceName( AkDBus::AgentServer ), + QLatin1String( "/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( AkDBus::serviceName( AkDBus::AgentServer ), + QLatin1String( "/AgentServer" ), QDBusConnection::sessionBus() ); + agentServer.agentInstanceConfigure( identifier(), windowId ); +} diff --git a/akonadi/server/control/agentthreadinstance.h b/akonadi/server/control/agentthreadinstance.h new file mode 100644 index 00000000..54b7df96 --- /dev/null +++ b/akonadi/server/control/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/akonadi/server/control/agenttype.cpp b/akonadi/server/control/agenttype.cpp new file mode 100644 index 00000000..02589ca8 --- /dev/null +++ b/akonadi/server/control/agenttype.cpp @@ -0,0 +1,141 @@ +/* + 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 "libs/xdgbasedirs_p.h" +#include "libs/capabilities_p.h" +#include +#include + +#include + +using namespace Akonadi; + +QLatin1String AgentType::CapabilityUnique = QLatin1String( AKONADI_AGENT_CAPABILITY_UNIQUE ); +QLatin1String AgentType::CapabilityResource = QLatin1String( AKONADI_AGENT_CAPABILITY_RESOURCE ); +QLatin1String AgentType::CapabilityAutostart = QLatin1String( AKONADI_AGENT_CAPABILITY_AUTOSTART ); +QLatin1String AgentType::CapabilityPreprocessor = QLatin1String( AKONADI_AGENT_CAPABILITY_PREPROCESSOR ); +QLatin1String AgentType::CapabilitySearch = QLatin1String( AKONADI_AGENT_CAPABILITY_SEARCH ); + +AgentType::AgentType() + : instanceCounter( 0 ) +{ +} + +QString AgentType::readString( const QSettings &file, const QString &key ) +{ + const QVariant value = file.value( key ); + if ( value.isNull() ) { + return QString(); + } else if ( value.canConvert() ) { + return QString::fromUtf8( value.toByteArray() ); + } else if ( value.canConvert() ) { + // This is a workaround for QSettings interpreting value with a comma as + // a QStringList, which is not compatible with KConfig. KConfig reads everything + // as a QByteArray and splits it to a list when requested. See BKO#330010 + // TODO KF5: If we end up in Tier 2 or above, depend on KConfig for parsing + // .desktop files + const QStringList parts = value.toStringList(); + QStringList utf8Parts; + utf8Parts.reserve( parts.size() ); + Q_FOREACH ( const QString &part, parts ) { + utf8Parts << QString::fromUtf8( part.toLatin1() ); + } + return utf8Parts.join( QLatin1String( ", ") ); + } else { + akError() << "Agent desktop file" << file.fileName() << "contains invalid value for key" << key; + return QString(); + } +} + +bool AgentType::load( const QString &fileName, AgentManager *manager ) +{ + Q_UNUSED( manager ); + + QSettings file( fileName, QSettings::IniFormat ); + file.beginGroup( QLatin1String( "Desktop Entry" ) ); + + Q_FOREACH ( const QString &key, file.allKeys() ) { + if ( key.startsWith( QLatin1String( "Name[" ) ) ) { + QString lang = key.mid( 5, key.length() - 6 ); + name.insert( lang, readString( file, key ) ); + } else if ( key == QLatin1String( "Name" ) ) { + name.insert( QLatin1String( "en_US" ), readString( file, key ) ); + } else if ( key.startsWith( QLatin1String( "Comment[" ) ) ) { + QString lang = key.mid( 8, key.length() - 9 ); + comment.insert( lang, readString( file, key ) ); + } else if ( key == QLatin1String( "Comment" ) ) { + comment.insert( QLatin1String( "en_US" ), readString( file, key ) ); + } else if ( key.startsWith( QLatin1String( "X-Akonadi-Custom-" ) ) ) { + QString customKey = key.mid( 17, key.length() ); + custom[customKey] = file.value( key ); + } + } + icon = file.value( QLatin1String( "Icon" ) ).toString(); + mimeTypes = file.value( QLatin1String( "X-Akonadi-MimeTypes" ) ).toStringList(); + capabilities = file.value( QLatin1String( "X-Akonadi-Capabilities" ) ).toStringList(); + exec = file.value( QLatin1String( "Exec" ) ).toString(); + identifier = file.value( QLatin1String( "X-Akonadi-Identifier" ) ).toString(); + launchMethod = Process; // Save default + + const QString method = file.value( QLatin1String( "X-Akonadi-LaunchMethod" ) ).toString(); + 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"; + } + + file.endGroup(); + + 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( AkStandardDirs::agentConfigFile( XdgBaseDirs::ReadOnly ), QSettings::IniFormat ); + instanceCounter = agentrc.value( QString::fromLatin1( "InstanceCounters/%1/InstanceCounter" ) + .arg( identifier ), 0 ).toInt(); + } + + return true; +} + +void AgentType::save( QSettings *config ) const +{ + Q_ASSERT( config ); + if ( !capabilities.contains( CapabilityUnique ) ) { + config->setValue( QString::fromLatin1( "InstanceCounters/%1/InstanceCounter" ).arg( identifier ), instanceCounter ); + } +} diff --git a/akonadi/server/control/agenttype.h b/akonadi/server/control/agenttype.h new file mode 100644 index 00000000..b9ad8b48 --- /dev/null +++ b/akonadi/server/control/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; + QHash name; + QHash comment; + QString icon; + QStringList mimeTypes; + QStringList capabilities; + QString exec; + QVariantMap custom; + uint instanceCounter; + LaunchMethod launchMethod; + + static QLatin1String CapabilityUnique; + static QLatin1String CapabilityResource; + static QLatin1String CapabilityAutostart; + static QLatin1String CapabilityPreprocessor; + static QLatin1String CapabilitySearch; + + private: + QString readString( const QSettings &file, const QString &key ); +}; + +#endif diff --git a/akonadi/server/control/controlmanager.cpp b/akonadi/server/control/controlmanager.cpp new file mode 100644 index 00000000..483ebe40 --- /dev/null +++ b/akonadi/server/control/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( QLatin1String( "/ControlManager" ), this ); +} + +ControlManager::~ControlManager() +{ +} + +void ControlManager::shutdown() +{ + QTimer::singleShot( 0, QCoreApplication::instance(), SLOT(quit()) ); +} diff --git a/akonadi/server/control/controlmanager.h b/akonadi/server/control/controlmanager.h new file mode 100644 index 00000000..ba050a08 --- /dev/null +++ b/akonadi/server/control/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/akonadi/server/control/main.cpp b/akonadi/server/control/main.cpp new file mode 100644 index 00000000..e7255916 --- /dev/null +++ b/akonadi/server/control/main.cpp @@ -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. * + ***************************************************************************/ + +#include "agentmanager.h" +#include "controlmanager.h" +#include "processcontrol.h" + +#include "akapplication.h" +#include "akcrash.h" +#include "akdebug.h" +#include "akdbus.h" + +#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 ) +{ + AkCoreApplication app( argc, argv ); + app.setDescription( QLatin1String( "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( AkDBus::serviceName( AkDBus::ControlLock ) ) ) { + // We couldn't register. Most likely, it's already running. + const QString lastError = QDBusConnection::sessionBus().lastError().message(); + if ( lastError.isEmpty() ) { + akFatal() << "Unable to register service as" << AkDBus::serviceName( AkDBus::ControlLock ) << "Maybe it's already running?"; + } else { + akFatal() << "Unable to register service as" << AkDBus::serviceName( AkDBus::ControlLock ) << "Error was:" << lastError; + } + } + + // 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( AkDBus::serviceName( AkDBus::Control ) ) ) { + akFatal() << "Another Akonadi control process is already running."; + } + + new ControlManager; + + sAgentManager = new AgentManager; + AkonadiCrash::setEmergencyMethod( crashHandler ); + + int retval = app.exec(); + + delete sAgentManager; + sAgentManager = 0; + + return retval; +} diff --git a/akonadi/server/control/org.freedesktop.Akonadi.Control.service.cmake b/akonadi/server/control/org.freedesktop.Akonadi.Control.service.cmake new file mode 100644 index 00000000..51cdafd5 --- /dev/null +++ b/akonadi/server/control/org.freedesktop.Akonadi.Control.service.cmake @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Akonadi.Control +Exec=${BIN_INSTALL_DIR}/akonadi_control diff --git a/akonadi/server/control/processcontrol.cpp b/akonadi/server/control/processcontrol.cpp new file mode 100644 index 00000000..52f9b4a7 --- /dev/null +++ b/akonadi/server/control/processcontrol.cpp @@ -0,0 +1,241 @@ +/*************************************************************************** + * 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 ) + , 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 ( AkApplication::hasInstanceIdentifier() ) { + QProcessEnvironment env = mProcess.processEnvironment(); + if ( env.isEmpty() ) { + env = QProcessEnvironment::systemEnvironment(); + } + env.insert( QLatin1String( "AKONADI_INSTANCE" ), AkApplication::instanceIdentifier() ); + 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, SLOT(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 ) ); + } + } + } +} + +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/akonadi/server/control/processcontrol.h b/akonadi/server/control/processcontrol.h new file mode 100644 index 00000000..2df98e5b --- /dev/null +++ b/akonadi/server/control/processcontrol.h @@ -0,0 +1,138 @@ +/*************************************************************************** + * 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/akonadi/server/src/CMakeLists.txt b/akonadi/server/src/CMakeLists.txt new file mode 100644 index 00000000..a29c6bf3 --- /dev/null +++ b/akonadi/server/src/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(storage) diff --git a/akonadi/server/src/akonadi.cpp b/akonadi/server/src/akonadi.cpp new file mode 100644 index 00000000..5369320c --- /dev/null +++ b/akonadi/server/src/akonadi.cpp @@ -0,0 +1,403 @@ +/*************************************************************************** + * 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 "connectionthread.h" +#include "serveradaptor.h" +#include +#include +#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/itemretrievalthread.h" +#include "preprocessormanager.h" +#include "search/searchmanager.h" +#include "search/searchtaskmanagerthread.h" +#include "response.h" +#include "collectionreferencemanager.h" + +#include "libs/xdgbasedirs_p.h" +#include "libs/protocol_p.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#ifdef HAVE_UNISTD_H +# include +#endif +#include + +#ifdef Q_WS_WIN +#include +#include +#endif + +using namespace Akonadi; +using namespace Akonadi::Server; + +AkonadiServer* AkonadiServer::s_instance = 0; + +AkonadiServer::AkonadiServer( QObject *parent ) + : QLocalServer( parent ) + , mCacheCleaner( 0 ) + , mIntervalChecker( 0 ) + , mStorageJanitor( 0 ) + , mItemRetrievalThread( 0 ) + , mDatabaseProcess( 0 ) + , mAlreadyShutdown( false ) +{ +} + +bool AkonadiServer::init() +{ + qRegisterMetaType(); + + const QString serverConfigFile = AkStandardDirs::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()->useInternalServer() ) { + startDatabaseProcess(); + } else { + createDatabase(); + } + + DbConfig::configuredDatabase()->setup(); + + s_instance = this; + + const QString connectionSettingsFile = AkStandardDirs::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 ) ) { + akFatal() << "Unable to listen on Named Pipe" << namedPipe; + } + + connectionSettings.setValue( QLatin1String( "Data/Method" ), QLatin1String( "NamedPipe" ) ); + connectionSettings.setValue( QLatin1String( "Data/NamedPipe" ), namedPipe ); +#else + const QString socketDir = Utils::preferredSocketDirectory( AkStandardDirs::saveDir( "data" ) ); + const QString socketFile = socketDir + QLatin1String( "/akonadiserver.socket" ); + unlink( socketFile.toUtf8().constData() ); + if ( !listen( socketFile ) ) { + akFatal() << "Unable to listen on Unix socket" << socketFile; + } + + connectionSettings.setValue( QLatin1String( "Data/Method" ), QLatin1String( "UnixPath" ) ); + connectionSettings.setValue( QLatin1String( "Data/UnixPath" ), socketFile ); +#endif + + // initialize the database + DataStore *db = DataStore::self(); + if ( !db->database().isOpen() ) { + akFatal() << "Unable to open database" << db->database().lastError().text(); + } + if ( !db->init() ) { + akFatal() << "Unable to initialize database."; + } + + NotificationManager::self(); + Tracer::self(); + new DebugInterface( this ); + ResourceManager::self(); + + // Initialize the preprocessor manager + PreprocessorManager::init(); + + // Forcibly disable it if configuration says so + if ( settings.value( QLatin1String( "General/DisablePreprocessing" ), false ).toBool() ) { + PreprocessorManager::instance()->setEnabled( false ); + } + + if ( settings.value( QLatin1String( "Cache/EnableCleaner" ), true ).toBool() ) { + mCacheCleaner = new CacheCleaner( this ); + mCacheCleaner->start( QThread::IdlePriority ); + } + + mIntervalChecker = new IntervalCheck( this ); + mIntervalChecker->start( QThread::IdlePriority ); + + mStorageJanitor = new StorageJanitorThread; + mStorageJanitor->start( QThread::IdlePriority ); + + mItemRetrievalThread = new ItemRetrievalThread( this ); + mItemRetrievalThread->start( QThread::HighPriority ); + + mAgentSearchManagerThread = new SearchTaskManagerThread( this ); + mAgentSearchManagerThread->start(); + + + const QStringList searchManagers = settings.value( QLatin1String( "Search/Manager" ), + QStringList() << QLatin1String( "Nepomuk" ) + << QLatin1String( "Agent" ) ).toStringList(); + mSearchManager = new SearchManagerThread( searchManagers, this ); + mSearchManager->start(); + + new ServerAdaptor( this ); + QDBusConnection::sessionBus().registerObject( QLatin1String( "/Server" ), this ); + + const QByteArray dbusAddress = qgetenv( "DBUS_SESSION_BUS_ADDRESS" ); + if ( !dbusAddress.isEmpty() ) { + connectionSettings.setValue( QLatin1String( "DBUS/Address" ), QLatin1String( dbusAddress ) ); + } + + QDBusServiceWatcher *watcher = new QDBusServiceWatcher( AkDBus::serviceName( AkDBus::Control ), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForOwnerChange, this ); + + connect( watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)), + this, SLOT(serviceOwnerChanged(QString,QString,QString)) ); + + // 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( AkDBus::serviceName( AkDBus::Server ) ) ) { + akFatal() << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message(); + } + + 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"; + quitThread( mCacheCleaner ); + quitThread( mIntervalChecker ); + quitThread( mStorageJanitor ); + quitThread( mItemRetrievalThread ); + mAgentSearchManagerThread->stop(); + quitThread( mAgentSearchManagerThread ); + quitThread( mSearchManager ); + + delete mSearchManager; + mSearchManager = 0; + + akDebug() << "terminating connection threads"; + for ( int i = 0; i < mConnections.count(); ++i ) { + quitThread( mConnections[i] ); + } + mConnections.clear(); + + // Terminate the preprocessor manager before the database but after all connections are gone + PreprocessorManager::done(); + + DataStore::self()->close(); + + akDebug() << "stopping db process"; + stopDatabaseProcess(); + + QSettings settings( AkStandardDirs::serverConfigFile(), QSettings::IniFormat ); + const QString connectionSettingsFile = AkStandardDirs::connectionConfigFile( XdgBaseDirs::WriteOnly ); + +#ifndef Q_OS_WIN + const QString socketDir = Utils::preferredSocketDirectory( AkStandardDirs::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, SLOT(doQuit()) ); + + return true; +} + +void AkonadiServer::doQuit() +{ + QCoreApplication::exit(); +} + +void AkonadiServer::incomingConnection( quintptr socketDescriptor ) +{ + if ( mAlreadyShutdown ) { + return; + } + QPointer thread = new ConnectionThread( socketDescriptor, this ); + connect( thread, SIGNAL(finished()), thread, SLOT(deleteLater()) ); + mConnections.append( thread ); + thread->start(); +} + +AkonadiServer *AkonadiServer::instance() +{ + if ( !s_instance ) { + s_instance = new AkonadiServer(); + } + return s_instance; +} + +void AkonadiServer::startDatabaseProcess() +{ + if ( !DbConfig::configuredDatabase()->useInternalServer() ) { + return; + } + + // create the database directories if they don't exists + AkStandardDirs::saveDir( "data" ); + AkStandardDirs::saveDir( "data", QLatin1String( "file_db_data" ) ); + + DbConfig::configuredDatabase()->startInternalServer(); +} + +void AkonadiServer::createDatabase() +{ + const QLatin1String initCon( "initConnection" ); + QSqlDatabase db = QSqlDatabase::addDatabase( DbConfig::configuredDatabase()->driverName(), initCon ); + DbConfig::configuredDatabase()->apply( db ); + db.setDatabaseName( DbConfig::configuredDatabase()->databaseName() ); + if ( !db.isValid() ) { + akFatal() << "Invalid database object during initial database connection"; + } + + if ( db.open() ) { + db.close(); + } else { + akDebug() << "Failed to use database" << DbConfig::configuredDatabase()->databaseName(); + akDebug() << "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( QString::fromLatin1( "CREATE DATABASE %1" ).arg( DbConfig::configuredDatabase()->databaseName() ) ) ) { + akError() << "Failed to create database"; + akError() << "Query error:" << query.lastError().text(); + akFatal() << "Database error:" << db.lastError().text(); + } + } // make sure query is destroyed before we close the db + db.close(); + } + } + QSqlDatabase::removeDatabase( initCon ); +} + +void AkonadiServer::stopDatabaseProcess() +{ + if ( !DbConfig::configuredDatabase()->useInternalServer() ) { + return; + } + + DbConfig::configuredDatabase()->stopInternalServer(); +} + +void AkonadiServer::serviceOwnerChanged( const QString &, const QString &oldOwner, const QString &newOwner ) +{ + Q_UNUSED( oldOwner ); + if ( newOwner.isEmpty() ) { + akError() << "Control process died, committing suicide!"; + quit(); + } +} + +CacheCleaner* AkonadiServer::cacheCleaner() +{ + return mCacheCleaner; +} + +IntervalCheck* AkonadiServer::intervalChecker() +{ + return mIntervalChecker; +} + diff --git a/akonadi/server/src/akonadi.h b/akonadi/server/src/akonadi.h new file mode 100644 index 00000000..17b4db29 --- /dev/null +++ b/akonadi/server/src/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 ConnectionThread; +class CacheCleaner; +class SearchManagerThread; +class ItemRetrievalThread; +class SearchTaskManagerThread; +class StorageJanitorThread; +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: + void startDatabaseProcess(); + void createDatabase(); + void stopDatabaseProcess(); + + protected: + AkonadiServer( QObject *parent = 0 ); + + CacheCleaner *mCacheCleaner; + IntervalCheck *mIntervalChecker; + StorageJanitorThread *mStorageJanitor; + ItemRetrievalThread *mItemRetrievalThread; + SearchTaskManagerThread *mAgentSearchManagerThread; + QProcess *mDatabaseProcess; + QVector< QPointer > mConnections; + SearchManagerThread *mSearchManager; + bool mAlreadyShutdown; + + static AkonadiServer *s_instance; +}; + +} // namespace Server +} // namespace Akonadi +#endif diff --git a/akonadi/server/src/cachecleaner.cpp b/akonadi/server/src/cachecleaner.cpp new file mode 100644 index 00000000..fbdf9d06 --- /dev/null +++ b/akonadi/server/src/cachecleaner.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 "cachecleaner.h" +#include "akdebug.h" +#include "storage/parthelper.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" +#include "storage/entity.h" +#include "akonadi.h" +#include "libs/protocol_p.h" + +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( parent ) +{ + setMinimumInterval( 5 ); +} + +CacheCleaner::~CacheCleaner() +{ +} + +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; + Q_FOREACH ( QString partName, collection.cachePolicyLocalParts().split( QLatin1String( " " ) ) ) { + 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/akonadi/server/src/cachecleaner.h b/akonadi/server/src/cachecleaner.h new file mode 100644 index 00000000..d3224c0a --- /dev/null +++ b/akonadi/server/src/cachecleaner.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_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: + static QMutex sLock; + static int sInhibitCount; + bool mInhibited; +}; + +/** + Cache cleaner thread. +*/ +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/akonadi/server/src/clientcapabilities.cpp b/akonadi/server/src/clientcapabilities.cpp new file mode 100644 index 00000000..a504b9c3 --- /dev/null +++ b/akonadi/server/src/clientcapabilities.cpp @@ -0,0 +1,90 @@ +/*************************************************************************** + * 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 "clientcapabilities.h" + +using namespace Akonadi::Server; + +ClientCapabilities::ClientCapabilities() + : m_notificationMessageVersion( 0 ) + , m_noPayloadPath( false ) + , m_serverSideSearch( false ) + , m_akAppendStreaming( false ) + , m_directStreaming( false ) +{ +} + +bool ClientCapabilities::isEmpty() +{ + return m_notificationMessageVersion == 0; +} + +int ClientCapabilities::notificationMessageVersion() const +{ + return m_notificationMessageVersion; +} + +void ClientCapabilities::setNotificationMessageVersion( int version ) +{ + if ( version <= 0 ) { + return; // invalid + } + m_notificationMessageVersion = version; +} + +bool ClientCapabilities::noPayloadPath() const +{ + return m_noPayloadPath; +} + +void ClientCapabilities::setNoPayloadPath( bool noPayloadPath ) +{ + m_noPayloadPath = noPayloadPath; +} + +bool ClientCapabilities::serverSideSearch() const +{ + return m_serverSideSearch; +} + +void ClientCapabilities::setServerSideSearch( bool serverSideSearch ) +{ + m_serverSideSearch = serverSideSearch; +} + +bool ClientCapabilities::akAppendStreaming() const +{ + return m_akAppendStreaming; +} + +void ClientCapabilities::setAkAppendStreaming(bool akAppendStreaming) +{ + m_akAppendStreaming = akAppendStreaming; +} + +bool ClientCapabilities::directStreaming() const +{ + return m_directStreaming; +} + +void ClientCapabilities::setDirectStreaming(bool directStreaming) +{ + m_directStreaming = directStreaming; +} + diff --git a/akonadi/server/src/clientcapabilities.h b/akonadi/server/src/clientcapabilities.h new file mode 100644 index 00000000..d761b184 --- /dev/null +++ b/akonadi/server/src/clientcapabilities.h @@ -0,0 +1,68 @@ +/*************************************************************************** + * 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 "capability.h" + +#ifndef AKONADI_CLIENTCAPABILITIES_H +#define AKONADI_CLIENTCAPABILITIES_H + + +namespace Akonadi { +namespace Server { + +/** Describes the capabilities of a specific session. + Filled by the CAPABILITY command. + */ +class ClientCapabilities +{ +public: + ClientCapabilities(); + + /** Returns @c true if no capabilities have been set by the client. + * This is useful for detecting legacy clients. + */ + bool isEmpty(); + + int notificationMessageVersion() const; + void setNotificationMessageVersion( int version ); + + bool noPayloadPath() const; + void setNoPayloadPath( bool noPayloadPath ); + + void setServerSideSearch( bool serverSideSearch ); + bool serverSideSearch() const; + + bool akAppendStreaming() const; + void setAkAppendStreaming( bool akAppendStreaming ); + + bool directStreaming() const; + void setDirectStreaming( bool directStreaming ); + +private: + int m_notificationMessageVersion; + int m_noPayloadPath : 1; + int m_serverSideSearch : 1; + int m_akAppendStreaming : 1; + int m_directStreaming : 1; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // CLIENTCAPABILITIES_H diff --git a/akonadi/server/src/clientcapabilityaggregator.cpp b/akonadi/server/src/clientcapabilityaggregator.cpp new file mode 100644 index 00000000..5dd69cb3 --- /dev/null +++ b/akonadi/server/src/clientcapabilityaggregator.cpp @@ -0,0 +1,77 @@ +/*************************************************************************** + * 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 "clientcapabilityaggregator.h" + +#include +#include + +using namespace Akonadi::Server; + +struct ClientCapabilityAggregatorData +{ + QMutex m_mutex; + QVector m_notifyVersions; +}; + +Q_GLOBAL_STATIC( ClientCapabilityAggregatorData, s_aggregator ) + +void ClientCapabilityAggregator::addSession( const ClientCapabilities &capabilities ) +{ + if ( capabilities.notificationMessageVersion() <= 0 ) { + return; + } + QMutexLocker locker( &s_aggregator()->m_mutex ); + if ( s_aggregator()->m_notifyVersions.size() <= capabilities.notificationMessageVersion() ) { + s_aggregator()->m_notifyVersions.resize( capabilities.notificationMessageVersion() + 1 ); + } + s_aggregator()->m_notifyVersions[capabilities.notificationMessageVersion()]++; +} + +void ClientCapabilityAggregator::removeSession( const ClientCapabilities &capabilities ) +{ + if ( capabilities.notificationMessageVersion() <= 0 ) { + return; + } + QMutexLocker locker( &s_aggregator()->m_mutex ); + s_aggregator()->m_notifyVersions[capabilities.notificationMessageVersion()]--; + Q_ASSERT( s_aggregator()->m_notifyVersions.at( capabilities.notificationMessageVersion() ) >= 0 ); +} + +int ClientCapabilityAggregator::minimumNotificationMessageVersion() +{ + QMutexLocker locker( &s_aggregator()->m_mutex ); + for ( int i = 1; i < s_aggregator()->m_notifyVersions.size(); ++i ) { + if ( s_aggregator()->m_notifyVersions.at( i ) != 0 ) { + return i; + } + } + return 0; +} + +int ClientCapabilityAggregator::maximumNotificationMessageVersion() +{ + QMutexLocker locker( &s_aggregator()->m_mutex ); + for ( int i = s_aggregator()->m_notifyVersions.size() - 1; i >= 1; --i ) { + if ( s_aggregator()->m_notifyVersions.at( i ) != 0 ) { + return i; + } + } + return 0; +} diff --git a/akonadi/server/src/clientcapabilityaggregator.h b/akonadi/server/src/clientcapabilityaggregator.h new file mode 100644 index 00000000..4788a1d3 --- /dev/null +++ b/akonadi/server/src/clientcapabilityaggregator.h @@ -0,0 +1,46 @@ +/*************************************************************************** + * 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 CLIENTCAPABILITYAGGREGATOR_H +#define CLIENTCAPABILITYAGGREGATOR_H + +#include "clientcapabilities.h" + +namespace Akonadi { +namespace Server { + +/** Aggregates client capabilities of all active sessions. */ +namespace ClientCapabilityAggregator +{ + /** Register capabilities of a new session. */ + void addSession( const ClientCapabilities &capabilities ); + /** Unregister capabilities of a new session. */ + void removeSession( const ClientCapabilities &capabilities ); + + /** Minimum required notification message version. */ + int minimumNotificationMessageVersion(); + + /** Maximum required notification message version */ + int maximumNotificationMessageVersion(); +} + +} // namespace Server +} // namespace Akonadi + +#endif // CLIENTCAPABILITYAGGREGATOR_H diff --git a/akonadi/server/src/collectionreferencemanager.cpp b/akonadi/server/src/collectionreferencemanager.cpp new file mode 100644 index 00000000..0160ef3a --- /dev/null +++ b/akonadi/server/src/collectionreferencemanager.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 "collectionreferencemanager.h" + +#include +#include +#include + +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()); + } + } +} + diff --git a/akonadi/server/src/collectionreferencemanager.h b/akonadi/server/src/collectionreferencemanager.h new file mode 100644 index 00000000..b6df0bb4 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/collectionscheduler.cpp b/akonadi/server/src/collectionscheduler.cpp new file mode 100644 index 00000000..8d4cd5c9 --- /dev/null +++ b/akonadi/server/src/collectionscheduler.cpp @@ -0,0 +1,297 @@ +/* + 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 "akdebug.h" + +#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 +{ + 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(); + } + + 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(); + } + + void resume() + { + if ( !isPaused() ) { + akError() << "Cannot resume a timer that is not paused."; + return; + } + + start( interval() - ( mStarted.secsTo( mPaused ) * 1000 ) ); + 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( QObject *parent ) + : QThread( parent ) + , mMinInterval( 5 ) +{ + // make sure we are created from the main thread, ie. before all other threads start to potentially use us + Q_ASSERT( QThread::currentThread() == QCoreApplication::instance()->thread() ); + + mScheduler = new PauseableTimer( this ); + mScheduler->setSingleShot( true ); + connect( mScheduler, SIGNAL(timeout()), + this, SLOT(schedulerTimeout()) ); +} + +CollectionScheduler::~CollectionScheduler() +{ +} + +void CollectionScheduler::run() +{ + DataStore::self(); + + QTimer::singleShot( 0, this, SLOT(initScheduler()) ); + exec(); + + DataStore::self()->close(); +} + +void CollectionScheduler::inhibit( bool inhibit ) +{ + if ( inhibit && mScheduler->isActive() && !mScheduler->isPaused() ) { + mScheduler->pause(); + } else if ( !inhibit && mScheduler->isPaused() ) { + mScheduler->resume(); + } +} + +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 ); + if ( mSchedule.values().contains( collection ) ) { + const uint key = mSchedule.key( collection ); + mSchedule.remove( key, collection ); + } + + 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() ) { + mScheduleLock.unlock(); + startScheduler(); + } +} + +void CollectionScheduler::initScheduler() +{ + const QVector collections = Collection::retrieveAll(); + Q_FOREACH ( /*sic!*/ 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(); +} diff --git a/akonadi/server/src/collectionscheduler.h b/akonadi/server/src/collectionscheduler.h new file mode 100644 index 00000000..a0aea85f --- /dev/null +++ b/akonadi/server/src/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" + +namespace Akonadi { +namespace Server { + +class Collection; +class PauseableTimer; + +class CollectionScheduler: public QThread +{ + Q_OBJECT + + public: + CollectionScheduler( 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 run(); + + 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 initScheduler(); + 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/akonadi/server/src/commandcontext.cpp b/akonadi/server/src/commandcontext.cpp new file mode 100644 index 00000000..f964c0a3 --- /dev/null +++ b/akonadi/server/src/commandcontext.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2014 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) 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 "commandcontext.h" +#include "imapstreamparser.h" +#include "handler.h" +#include "storage/selectquerybuilder.h" + +#include "libs/protocol_p.h" + +using namespace Akonadi::Server; + +CommandContext::CommandContext() + : mTagId( -1 ) +{ +} + +CommandContext::~CommandContext() +{ +} + +void CommandContext::setResource( const Resource &resource ) +{ + mResource = resource; +} + +Resource CommandContext::resource() const +{ + return mResource; +} + +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; +} + + +void CommandContext::parseContext( ImapStreamParser *parser ) +{ + // Context + if ( !parser->hasString() ) { + return; + } + + const QByteArray param = parser->peekString(); + + if ( param == AKONADI_PARAM_COLLECTIONID ) { + parser->readString(); // Read the param + bool ok = false; + const qint64 colId = parser->readNumber( &ok ); + if ( !ok ) { + throw HandlerException( "Invalid FETCH collection ID" ); + } + const Collection col = Collection::retrieveById( colId ); + if ( !col.isValid() ) { + throw HandlerException( "No such collection" ); + } + setCollection( col ); + } else if ( param == AKONADI_PARAM_COLLECTION ) { + if ( !resource().isValid() ) { + throw HandlerException( "Only resources can use REMOTEID" ); + } + parser->readString(); // Read the param + const QByteArray rid = parser->readString(); + SelectQueryBuilder qb; + qb.addValueCondition( Collection::remoteIdColumn(), Query::Equals, QString::fromUtf8( rid ) ); + qb.addValueCondition( Collection::resourceIdColumn(), Query::Equals, resource().id() ); + if ( !qb.exec() ) { + throw HandlerException( "Failed to select collection" ); + } + Collection::List results = qb.result(); + if ( results.count() != 1 ) { + throw HandlerException( QByteArray::number( results.count() ) + " collections found" ); + } + setCollection( results.first() ); + } + + if ( param == AKONADI_PARAM_TAGID ) { + parser->readString(); // Read the param + bool ok = false; + const qint64 tagId = parser->readNumber( &ok ); + if ( !ok ) { + throw HandlerException( "Invalid FETCH tag" ); + } + if ( !Tag::exists( tagId ) ) { + throw HandlerException( "No such tag" ); + } + setTag( tagId ); + } +} diff --git a/akonadi/server/src/commandcontext.h b/akonadi/server/src/commandcontext.h new file mode 100644 index 00000000..9396f268 --- /dev/null +++ b/akonadi/server/src/commandcontext.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 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) 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 COMMANDCONTEXT_H +#define COMMANDCONTEXT_H + +#include "entities.h" + +namespace Akonadi { +namespace Server { + +class ImapStreamParser; + +class CommandContext +{ + public: + CommandContext(); + ~CommandContext(); + + void setResource( const Resource &resource ); + Resource resource() const; + + void setCollection( const Collection &collection ); + qint64 collectionId() const; + Collection collection() const; + + void setTag( qint64 tagId ); + qint64 tagId() const; + Tag tag() const; + + bool isEmpty() const; + + void parseContext( ImapStreamParser *parser ); + + private: + Resource mResource; + Collection mCollection; + qint64 mTagId; +}; + +} + +} + +#endif // COMMANDCONTEXT_H diff --git a/akonadi/server/src/connection.cpp b/akonadi/server/src/connection.cpp new file mode 100644 index 00000000..32f10d5d --- /dev/null +++ b/akonadi/server/src/connection.cpp @@ -0,0 +1,348 @@ +/*************************************************************************** + * 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 +#include +#include +#include + +#include "storage/datastore.h" +#include "handler.h" +#include "response.h" +#include "tracer.h" +#include "clientcapabilityaggregator.h" +#include "collectionreferencemanager.h" + +#include "imapstreamparser.h" +#include "shared/akdebug.h" +#include "shared/akcrash.h" + +#include + +#include + +#define AKONADI_PROTOCOL_VERSION 44 + +using namespace Akonadi::Server; + +Connection::Connection( QObject *parent ) + : QObject( parent ) + , m_socketDescriptor( 0 ) + , m_socket( 0 ) + , m_currentHandler( 0 ) + , m_connectionState( NonAuthenticated ) + , m_backend( 0 ) + , m_streamParser( 0 ) + , m_verifyCacheOnRetrieval( false ) +{ +} + + +Connection::Connection( quintptr socketDescriptor, QObject *parent ) + : Connection( parent ) +{ + m_socketDescriptor = socketDescriptor; + m_identifier.sprintf( "%p", static_cast( this ) ); + ClientCapabilityAggregator::addSession( m_clientCapabilities ); + + const QSettings settings( AkStandardDirs::serverConfigFile(), QSettings::IniFormat ); + m_verifyCacheOnRetrieval = settings.value( QLatin1String( "Cache/VerifyOnRetrieval" ), m_verifyCacheOnRetrieval ).toBool(); + + QLocalSocket *socket = new QLocalSocket(); + + if ( !socket->setSocketDescriptor( m_socketDescriptor ) ) { + qWarning() << "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, SIGNAL(readyRead()), + this, SLOT(slotNewData()) ); + connect( socket, SIGNAL(disconnected()), + this, SIGNAL(disconnected()) ); + + m_streamParser = new ImapStreamParser( m_socket ); + m_streamParser->setTracerIdentifier( m_identifier ); + + Response greeting; + greeting.setUntagged(); + greeting.setString( "OK Akonadi Almost IMAP Server [PROTOCOL " + QByteArray::number(AKONADI_PROTOCOL_VERSION) + "]" ); + // don't send before the event loop is active, since waitForBytesWritten() can cause interesting reentrancy issues + QMetaObject::invokeMethod( this, "slotResponseAvailable", + Qt::QueuedConnection, + Q_ARG( Akonadi::Server::Response, greeting ) ); +} + +int Connection::protocolVersion() +{ + return (int) AKONADI_PROTOCOL_VERSION; +} + + +DataStore *Connection::storageBackend() +{ + if ( !m_backend ) { + m_backend = DataStore::self(); + } + return m_backend; +} + +CollectionReferenceManager *Connection::collectionReferenceManager() +{ + return CollectionReferenceManager::instance(); +} + +Connection::~Connection() +{ + delete m_socket; + m_socket = 0; + delete m_streamParser; + m_streamParser = 0; + + ClientCapabilityAggregator::removeSession( m_clientCapabilities ); + Tracer::self()->endConnection( m_identifier, QString() ); + collectionReferenceManager()->removeSession( m_sessionId ); +} + +void Connection::slotNewData() +{ + // On Windows, calling readLiteralPart() triggers the readyRead() signal recursively and leads to parse errors + if ( m_currentHandler ) { + return; + } + + while ( m_socket->bytesAvailable() > 0 || !m_streamParser->readRemainingData().isEmpty() ) { + try { + const QByteArray tag = m_streamParser->readString(); + // deal with stray newlines + if ( tag.isEmpty() && m_streamParser->atCommandEnd() ) { + continue; + } + const QByteArray command = m_streamParser->readString(); + if ( command.isEmpty() ) { + throw Akonadi::Server::Exception("empty command"); + } + // Tag context is not persistent, unlike Collection + // FIXME: Collection should not be persistent either, but we need to keep backward compatibility + // with SELECT job + context()->setTag( -1 ); + Tracer::self()->connectionInput( m_identifier, ( tag + ' ' + command + ' ' + m_streamParser->readRemainingData() ) ); + m_currentHandler = findHandlerForCommand( command ); + assert( m_currentHandler ); + connect( m_currentHandler, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SLOT(slotResponseAvailable(Akonadi::Server::Response)), Qt::DirectConnection ); + connect( m_currentHandler, SIGNAL(connectionStateChange(ConnectionState)), + this, SLOT(slotConnectionStateChange(ConnectionState)), + Qt::DirectConnection ); + m_currentHandler->setConnection( this ); + m_currentHandler->setTag( tag ); + m_currentHandler->setStreamParser( m_streamParser ); + if ( !m_currentHandler->parseStream() ) { + m_streamParser->skipCurrentCommand(); + } + } catch ( const Akonadi::Server::HandlerException &e ) { + m_currentHandler->failureResponse( e.what() ); + try { + m_streamParser->skipCurrentCommand(); + } catch ( ... ) {} + } catch ( const Akonadi::Server::Exception &e ) { + if ( m_currentHandler ) { + m_currentHandler->failureResponse( QByteArray( e.type() ) + QByteArray( ": " ) + QByteArray( e.what() ) ); + } + try { + m_streamParser->skipCurrentCommand(); + } catch ( ... ) {} + } catch ( ... ) { + akError() << "Unknown exception caught: " << akBacktrace(); + if ( m_currentHandler ) { + m_currentHandler->failureResponse( "Unknown exception caught" ); + } + try { + m_streamParser->skipCurrentCommand(); + } catch ( ... ) {} + } + delete m_currentHandler; + m_currentHandler = 0; + + if ( m_streamParser->readRemainingData().startsWith( '\n' ) || m_streamParser->readRemainingData().startsWith( "\r\n" ) ) { + try { + m_streamParser->readUntilCommandEnd(); //just eat the ending newline + } catch ( ... ) {} + } + } +} + +void Connection::writeOut( const QByteArray &data ) +{ + QByteArray block = data + "\r\n"; + m_socket->write( block ); + m_socket->waitForBytesWritten( 30 * 1000 ); + + Tracer::self()->connectionOutput( m_identifier, block ); +} + +CommandContext *Connection::context() const +{ + return const_cast( &m_context ); +} + +Handler *Connection::findHandlerForCommand( const QByteArray &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, m_streamParser ); + break; + case Selected: + break; + case LoggingOut: + break; + } + // we didn't have a handler for this, let the default one do its thing + if ( !handler ) { + handler = new UnknownCommandHandler( command ); + } + return handler; +} + +void Connection::slotResponseAvailable( const Response &response ) +{ + // FIXME handle reentrancy in the presence of continuation. Something like: + // "if continuation pending, queue responses, once continuation is done, replay them" + writeOut( response.asString() ); +} + +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: + if (dynamic_cast( m_socket ) ) { + dynamic_cast( m_socket )->disconnectFromServer(); + } + break; + } +} + +void Connection::addStatusMessage( const QByteArray &msg ) +{ + m_statusMessageQueue.append( msg ); +} + +void Connection::flushStatusMessageQueue() +{ + for ( int i = 0; i < m_statusMessageQueue.count(); ++i ) { + Response response; + response.setUntagged(); + response.setString( m_statusMessageQueue[i] ); + + slotResponseAvailable( response ); + } + + m_statusMessageQueue.clear(); +} + +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; +} + +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; +} + +const ClientCapabilities &Connection::capabilities() const +{ + return m_clientCapabilities; +} + +void Connection::setCapabilities( const ClientCapabilities &capabilities ) +{ + ClientCapabilityAggregator::removeSession( m_clientCapabilities ); + m_clientCapabilities = capabilities; + ClientCapabilityAggregator::addSession( m_clientCapabilities ); +} + +bool Connection::verifyCacheOnRetrieval() const +{ + return m_verifyCacheOnRetrieval; +} diff --git a/akonadi/server/src/connection.h b/akonadi/server/src/connection.h new file mode 100644 index 00000000..3f1141ee --- /dev/null +++ b/akonadi/server/src/connection.h @@ -0,0 +1,115 @@ +/*************************************************************************** + * 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 "entities.h" +#include "global.h" +#include "clientcapabilities.h" +#include "commandcontext.h" + +namespace Akonadi { +namespace Server { + +class Handler; +class Response; +class DataStore; +class Collection; +class ImapStreamParser; +class CollectionReferenceManager; + +/** + An Connection represents one connection of a client to the server. +*/ +class Connection : public QObject +{ + Q_OBJECT +public: + Connection( quintptr socketDescriptor, QObject *parent = 0 ); + virtual ~Connection(); + + static int protocolVersion(); + + 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 addStatusMessage( const QByteArray &msg ); + void flushStatusMessageQueue(); + + void setSessionId( const QByteArray &id ); + QByteArray sessionId() const; + + const ClientCapabilities &capabilities() const; + void setCapabilities( const ClientCapabilities &capabilities ); + + /** Returns @c true if permanent cache verification is enabled. */ + bool verifyCacheOnRetrieval() const; + +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 ); + + virtual void slotResponseAvailable( const Akonadi::Server::Response &response ); + +protected: + Connection(QObject *parent = 0); // used for testing + + void writeOut( const QByteArray &data ); + virtual Handler *findHandlerForCommand( const QByteArray &command ); + +protected: + quintptr m_socketDescriptor; + QIODevice *m_socket; + QPointer m_currentHandler; + ConnectionState m_connectionState; + mutable DataStore *m_backend; + QList m_statusMessageQueue; + QString m_identifier; + QByteArray m_sessionId; + ImapStreamParser *m_streamParser; + ClientCapabilities m_clientCapabilities; + bool m_verifyCacheOnRetrieval; + CommandContext m_context; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/connectionthread.cpp b/akonadi/server/src/connectionthread.cpp new file mode 100644 index 00000000..35968517 --- /dev/null +++ b/akonadi/server/src/connectionthread.cpp @@ -0,0 +1,54 @@ +/* + * 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 "connectionthread.h" +#include "connection.h" +#include "storage/datastore.h" + + +using namespace Akonadi::Server; + +ConnectionThread::ConnectionThread(quintptr socketDescriptor, QObject* parent) + : QThread(parent) + , mSocketDescriptor(socketDescriptor) +{ +} + +ConnectionThread::~ConnectionThread() +{ +} + +void ConnectionThread::run() +{ + DataStore::self(); + + Connection *connection = new Connection(mSocketDescriptor); + connect(connection, SIGNAL(disconnected()), + this, SLOT(quit())); + + exec(); + + delete connection; + + DataStore::self()->close(); +} + + + + diff --git a/akonadi/server/src/connectionthread.h b/akonadi/server/src/connectionthread.h new file mode 100644 index 00000000..abc73b62 --- /dev/null +++ b/akonadi/server/src/connectionthread.h @@ -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 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_CONNECTIONTHREAD_H +#define AKONADI_SERVER_CONNECTIONTHREAD_H + +#include + +namespace Akonadi +{ +namespace Server +{ + +class ConnectionThread : public QThread +{ + Q_OBJECT + +public: + explicit ConnectionThread(quintptr socketDescriptor, QObject* parent = 0); + virtual ~ConnectionThread(); + + void run(); + +private: + quintptr mSocketDescriptor; +}; + +} +} + +#endif // AKONADI_SERVER_CONNECTIONTHREAD_H diff --git a/akonadi/server/src/dbusconnectionpool.cpp b/akonadi/server/src/dbusconnectionpool.cpp new file mode 100644 index 00000000..9aede4f0 --- /dev/null +++ b/akonadi/server/src/dbusconnectionpool.cpp @@ -0,0 +1,62 @@ +/* + * 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, + QString::fromLatin1("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/akonadi/server/src/dbusconnectionpool.h b/akonadi/server/src/dbusconnectionpool.h new file mode 100644 index 00000000..4f8a93e0 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/dbustracer.cpp b/akonadi/server/src/dbustracer.cpp new file mode 100644 index 00000000..891100e9 --- /dev/null +++ b/akonadi/server/src/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( QLatin1String( "/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/akonadi/server/src/dbustracer.h b/akonadi/server/src/dbustracer.h new file mode 100644 index 00000000..9546935f --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/debuginterface.cpp b/akonadi/server/src/debuginterface.cpp new file mode 100644 index 00000000..2be1fa60 --- /dev/null +++ b/akonadi/server/src/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( QLatin1String( "/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/akonadi/server/src/debuginterface.h b/akonadi/server/src/debuginterface.h new file mode 100644 index 00000000..3542a16d --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/exception.h b/akonadi/server/src/exception.h new file mode 100644 index 00000000..04c4a1c8 --- /dev/null +++ b/akonadi/server/src/exception.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 Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see 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/akonadi/server/src/filetracer.cpp b/akonadi/server/src/filetracer.cpp new file mode 100644 index 00000000..5a89478d --- /dev/null +++ b/akonadi/server/src/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, QString::fromLatin1( "begin_connection: %1" ).arg( msg ) ); +} + +void FileTracer::endConnection( const QString &identifier, const QString &msg ) +{ + output( identifier, QString::fromLatin1( "end_connection: %1" ).arg( msg ) ); +} + +void FileTracer::connectionInput( const QString &identifier, const QByteArray &msg ) +{ + output( identifier, QString::fromLatin1( "input: %1" ).arg( QString::fromUtf8( msg ) ) ); +} + +void FileTracer::connectionOutput( const QString &identifier, const QByteArray &msg ) +{ + output( identifier, QString::fromLatin1( "output: %1" ).arg( QString::fromUtf8( msg ) ) ); +} + +void FileTracer::signal( const QString &signalName, const QString &msg ) +{ + output( QLatin1String( "signal" ), QString::fromLatin1( "<%1> %2" ).arg( signalName, msg ) ); +} + +void FileTracer::warning( const QString &componentName, const QString &msg ) +{ + output( QLatin1String( "warning" ), QString::fromLatin1( "<%1> %2" ).arg( componentName, msg ) ); +} + +void FileTracer::error( const QString &componentName, const QString &msg ) +{ + output( QLatin1String( "error" ), QString::fromLatin1( "<%1> %2" ).arg( componentName, msg ) ); +} + +void FileTracer::output( const QString &id, const QString &msg ) +{ + QString output = QString::fromLatin1( "%1: %2: %3\r\n" ).arg( QTime::currentTime().toString( QLatin1String( "HH:mm:ss.zzz" ) ), id, msg.left( msg.indexOf( QLatin1String( "\n" ) ) ) ); + m_file->write( output.toUtf8() ); +} diff --git a/akonadi/server/src/filetracer.h b/akonadi/server/src/filetracer.h new file mode 100644 index 00000000..dc840a62 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/global.h b/akonadi/server/src/global.h new file mode 100644 index 00000000..7f6c7f77 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/handler.cpp b/akonadi/server/src/handler.cpp new file mode 100644 index 00000000..7e950677 --- /dev/null +++ b/akonadi/server/src/handler.cpp @@ -0,0 +1,287 @@ +/*************************************************************************** + * 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 "libs/imapset_p.h" +#include "libs/protocol_p.h" + +#include "connection.h" +#include "response.h" +#include "scope.h" +#include "handler/akappend.h" +#include "handler/append.h" +#include "handler/capability.h" +#include "handler/copy.h" +#include "handler/colcopy.h" +#include "handler/colmove.h" +#include "handler/create.h" +#include "handler/delete.h" +#include "handler/expunge.h" +#include "handler/fetch.h" +#include "handler/link.h" +#include "handler/list.h" +#include "handler/login.h" +#include "handler/logout.h" +#include "handler/merge.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/select.h" +#include "handler/subscribe.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 "storage/querybuilder.h" +#include "imapstreamparser.h" + +using namespace Akonadi::Server; + +Handler::Handler() + : QObject() + , m_connection( 0 ) + , m_streamParser( 0 ) +{ +} + +Handler::~Handler() +{ +} + +Handler *Handler::findHandlerForCommandNonAuthenticated( const QByteArray &command ) +{ + // allowed are LOGIN + if ( command == AKONADI_CMD_LOGIN ) { + return new Login(); + } + + return 0; +} + +Handler *Handler::findHandlerForCommandAlwaysAllowed( const QByteArray &command ) +{ + // allowed commands CAPABILITY and LOGOUT + if ( command == AKONADI_CMD_LOGOUT ) { + return new Logout(); + } + if ( command == AKONADI_CMD_CAPABILITY ) { + return new Capability(); + } + return 0; +} + +void Handler::setTag( const QByteArray &tag ) +{ + m_tag = tag; +} + +QByteArray Handler::tag() const +{ + return m_tag; +} + +Handler *Handler::findHandlerForCommandAuthenticated( const QByteArray &_command, ImapStreamParser *streamParser ) +{ + QByteArray command( _command ); + // deal with command prefixes + Scope::SelectionScope scope = Scope::selectionScopeFromByteArray( command ); + if ( scope != Scope::None ) { + command = streamParser->readString(); + } + + // allowed commands are listed below ;-). + if ( command == AKONADI_CMD_APPEND ) { + return new Append(); + } + if ( command == AKONADI_CMD_COLLECTIONCREATE ) { + return new Create( scope ); + } + if ( command == AKONADI_CMD_LIST || command == AKONADI_CMD_X_AKLIST ) { //TODO: remove X-AKLIST support in Akonadi 2.0 + return new List( scope, false ); + } + if ( command == AKONADI_CMD_LSUB || command == AKONADI_CMD_X_AKLSUB ) { //TODO: remove X-AKLSUB support in Akonadi 2.0 + return new List( scope, true ); + } + if ( command == AKONADI_CMD_SELECT ) { + return new Select( scope ); + } + if ( command == AKONADI_CMD_SEARCH_STORE ) { + return new SearchPersistent(); + } + if ( command == AKONADI_CMD_SEARCH ) { + return new Search(); + } + if ( command == AKONADI_CMD_SEARCH_RESULT ) { + return new SearchResult( scope ); + } + if ( command == AKONADI_CMD_ITEMFETCH ) { + return new Fetch( scope ); + } + if ( command == AKONADI_CMD_EXPUNGE ) { //TODO: remove EXPUNGE support in Akonadi 2.0 + return new Expunge(); + } + if ( command == AKONADI_CMD_ITEMMODIFY ) { + return new Store( scope ); + } + if ( command == AKONADI_CMD_STATUS ) { + return new Status(); + } + if ( command == AKONADI_CMD_COLLECTIONDELETE ) { + return new Delete( scope ); + } + if ( command == AKONADI_CMD_COLLECTIONMODIFY ) { + return new Modify( scope ); + } + if ( command == AKONADI_CMD_BEGIN ) { + return new TransactionHandler( TransactionHandler::Begin ); + } + if ( command == AKONADI_CMD_ROLLBACK ) { + return new TransactionHandler( TransactionHandler::Rollback ); + } + if ( command == AKONADI_CMD_COMMIT ) { + return new TransactionHandler( TransactionHandler::Commit ); + } + if ( command == AKONADI_CMD_ITEMCREATE ) { + return new AkAppend(); + } + if ( command == AKONADI_CMD_SUBSCRIBE ) { + return new Subscribe( true ); + } + if ( command == AKONADI_CMD_UNSUBSCRIBE ) { + return new Subscribe( false ); + } + if ( command == AKONADI_CMD_ITEMCOPY ) { + return new Copy(); + } + if ( command == AKONADI_CMD_COLLECTIONCOPY ) { + return new ColCopy(); + } + if ( command == AKONADI_CMD_ITEMLINK ) { + return new Link( scope, true ); + } + if ( command == AKONADI_CMD_ITEMUNLINK ) { + return new Link( scope, false ); + } + if ( command == AKONADI_CMD_RESOURCESELECT ) { + return new ResourceSelect(); + } + if ( command == AKONADI_CMD_ITEMDELETE ) { + return new Remove( scope ); + } + if ( command == AKONADI_CMD_ITEMMOVE ) { + return new Move( scope ); + } + if ( command == AKONADI_CMD_COLLECTIONMOVE ) { + return new ColMove( scope ); + } + if ( command == AKONADI_CMD_TAGAPPEND ) { + return new TagAppend(); + } + if ( command == AKONADI_CMD_TAGFETCH ) { + return new TagFetch( scope ); + } + if ( command == AKONADI_CMD_TAGREMOVE ) { + return new TagRemove( scope ); + } + if ( command == AKONADI_CMD_TAGSTORE ) { + return new TagStore(); + } + if ( command == AKONADI_CMD_MERGE ) { + return new Merge(); + } + + return 0; +} + +void Handler::setConnection( Connection *connection ) +{ + m_connection = connection; +} + +Connection *Handler::connection() const +{ + return m_connection; +} + +bool Handler::failureResponse( const QByteArray &failureMessage ) +{ + Response response; + response.setTag( tag() ); + response.setFailure(); + response.setString( failureMessage ); + Q_EMIT responseAvailable( response ); + return false; +} + +bool Handler::failureResponse( const char *failureMessage ) +{ + return failureResponse( QByteArray( failureMessage ) ); +} + +bool Handler::successResponse( const QByteArray &successMessage ) +{ + Response response; + response.setTag( tag() ); + response.setSuccess(); + response.setString( successMessage ); + Q_EMIT responseAvailable( response ); + return true; +} + +bool Handler::successResponse( const char *successMessage ) +{ + return successResponse( QByteArray( successMessage ) ); +} + +void Handler::setStreamParser( ImapStreamParser *parser ) +{ + m_streamParser = parser; +} + +UnknownCommandHandler::UnknownCommandHandler( const QByteArray &command ) + : mCommand( command ) +{ +} + +bool UnknownCommandHandler::parseStream() +{ + Response response; + response.setError(); + response.setTag( tag() ); + if ( mCommand.isEmpty() ) { + response.setString( "No command specified" ); + } else { + response.setString( "Unrecognized command: " + mCommand ); + } + m_streamParser->readUntilCommandEnd(); + + Q_EMIT responseAvailable( response ); + return true; +} diff --git a/akonadi/server/src/handler.h b/akonadi/server/src/handler.h new file mode 100644 index 00000000..1c70c1eb --- /dev/null +++ b/akonadi/server/src/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 +#include + +#include "global.h" +#include "exception.h" + +namespace Akonadi { + +class ImapSet; + +namespace Server { + +class Response; +class Connection; +class QueryBuilder; +class ImapStreamParser; + +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( const QByteArray &tag ); + + /** + * The tag of the command associated with this handler. + */ + QByteArray tag() 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( const QByteArray &line ); + + /** + * 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( const QByteArray &line ); + + /** + * 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( const QByteArray &line, ImapStreamParser *streamParser ); + + void setConnection( Connection *connection ); + Connection *connection() const; + + /** Send a failure response with the given message. */ + bool failureResponse( const QByteArray &failureMessage ); + /** + Convenience method to compile with QT_NO_CAST_FROM_ASCII without + typing too much ;-) + */ + bool failureResponse( const char *failureMessage ); + + /** Send a success response with the given message. */ + bool successResponse( const QByteArray &successMessage ); + /** Send a success response with the given message. */ + bool successResponse( const char *successMessage ); + + /** + * Assigns the streaming IMAP parser to the handler. Useful only if supportsStreamParser() returns true. + * @param parser the imap parser object + */ + void setStreamParser( ImapStreamParser *parser ); + + /** + * 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; + +Q_SIGNALS: + + /** + * Emitted whenever the handler has a response ready for output. There can + * be several responses per command. + * @param response The response to be sent to the client. + */ + void responseAvailable( const Akonadi::Server::Response &response ); + + /** + * 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: + QByteArray m_tag; + Connection *m_connection; + +protected: + ImapStreamParser *m_streamParser; +}; + +class UnknownCommandHandler : public Handler +{ + Q_OBJECT + public: + UnknownCommandHandler( const QByteArray &command ); + bool parseStream(); + + private: + QByteArray mCommand; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/akappend.cpp b/akonadi/server/src/handler/akappend.cpp new file mode 100644 index 00000000..43f03ba9 --- /dev/null +++ b/akonadi/server/src/handler/akappend.cpp @@ -0,0 +1,398 @@ +/*************************************************************************** + * 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 "libs/imapparser_p.h" +#include "imapstreamparser.h" + +#include "append.h" +#include "response.h" +#include "handlerhelper.h" + +#include "akonadi.h" +#include "connection.h" +#include "preprocessormanager.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/transaction.h" +#include "storage/parttypehelper.h" +#include "storage/dbconfig.h" +#include "storage/partstreamer.h" +#include "storage/parthelper.h" +#include "libs/protocol_p.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +AkAppend::AkAppend() + : Handler() +{ +} + +AkAppend::~AkAppend() +{ +} + +QByteArray AkAppend::parseFlag( const QByteArray &flag ) const +{ + const int pos1 = flag.indexOf( '[' ); + const int pos2 = flag.lastIndexOf( ']' ); + return flag.mid( pos1 + 1, pos2 - pos1 - 1 ); +} + +bool AkAppend::buildPimItem( PimItem &item, Collection &col, + ChangedAttributes &itemFlags, + ChangedAttributes &itemTagsRID, + ChangedAttributes &itemTagsGID ) +{ + // Arguments: mailbox name + // OPTIONAL flag parenthesized list + // OPTIONAL date/time string + // (partname literal)+ + // + // Syntax: + // x-akappend = "X-AKAPPEND" SP mailbox SP size [SP flag-list] [SP date-time] SP (partname SP literal)+ + const QByteArray mailbox = m_streamParser->readString(); + + const qint64 size = m_streamParser->readNumber(); + // parse optional flag parenthesized list + // Syntax: + // flag-list = "(" [flag *(SP flag)] ")" + // flag = "\ANSWERED" / "\FLAGGED" / "\DELETED" / "\SEEN" / + // "\DRAFT" / flag-keyword / flag-extension + // ; Does not include "\Recent" + // flag-extension = "\" atom + // flag-keyword = atom + QList flags; + if ( m_streamParser->hasList() ) { + flags = m_streamParser->readParenthesizedList(); + } + + // parse optional date/time string + QDateTime dateTime; + if ( m_streamParser->hasDateTime() ) { + dateTime = m_streamParser->readDateTime().toUTC(); + // FIXME Should we return an error if m_dateTime is invalid? + } else { + // if date/time is not given then it will be set to the current date/time + // converted to UTC. + dateTime = QDateTime::currentDateTime().toUTC(); + } + + Response response; + + col = HandlerHelper::collectionFromIdOrName( mailbox ); + if ( !col.isValid() ) { + throw HandlerException( QByteArray( "Unknown collection for '" ) + mailbox + QByteArray( "'." ) ); + } + if ( col.isVirtual() ) { + throw HandlerException( "Cannot append item into virtual collection" ); + } + + QByteArray mt; + QString remote_id; + QString remote_revision; + QString gid; + Q_FOREACH ( const QByteArray &flag, flags ) { + if ( flag.startsWith( AKONADI_FLAG_MIMETYPE ) ) { + mt = parseFlag( flag ); + } else if ( flag.startsWith( AKONADI_FLAG_REMOTEID ) ) { + remote_id = QString::fromUtf8( parseFlag( flag ) ); + } else if ( flag.startsWith( AKONADI_FLAG_REMOTEREVISION ) ) { + remote_revision = QString::fromUtf8( parseFlag( flag ) ); + } else if ( flag.startsWith( AKONADI_FLAG_GID ) ) { + gid = QString::fromUtf8( parseFlag( flag ) ); + } else if ( flag.startsWith( "+" AKONADI_FLAG_TAG ) ) { + itemTagsGID.incremental = true; + itemTagsGID.added.append( parseFlag( flag ) ); + } else if ( flag.startsWith( "-" AKONADI_FLAG_TAG ) ) { + itemTagsGID.incremental = true; + itemTagsGID.removed.append( parseFlag( flag ) ); + } else if ( flag.startsWith( AKONADI_FLAG_TAG ) ) { + itemTagsGID.incremental = false; + itemTagsGID.added.append( parseFlag( flag ) ); + } else if ( flag.startsWith( "+" AKONADI_FLAG_RTAG ) ) { + itemTagsRID.incremental = true; + itemTagsRID.added.append( parseFlag( flag ) ); + } else if ( flag.startsWith( "-" AKONADI_FLAG_RTAG ) ) { + itemTagsRID.incremental = true; + itemTagsRID.removed.append( parseFlag( flag ) ); + } else if ( flag.startsWith( AKONADI_FLAG_RTAG ) ) { + itemTagsRID.incremental = false; + itemTagsRID.added.append( parseFlag( flag ) ); + } else if ( flag.startsWith( '+' ) ) { + itemFlags.incremental = true; + itemFlags.added.append( flag.mid( 1 ) ); + } else if ( flag.startsWith( '-' ) ) { + itemFlags.incremental = true; + itemFlags.removed.append( flag.mid( 1 ) ); + } else { + itemFlags.incremental = false; + itemFlags.added.append( flag ); + } + } + // standard imap does not know this attribute, so that's mail + if ( mt.isEmpty() ) { + mt = "message/rfc822"; + } + MimeType mimeType = MimeType::retrieveByName( QString::fromLatin1( mt ) ); + if ( !mimeType.isValid() ) { + MimeType m( QString::fromLatin1( mt ) ); + if ( !m.insert() ) { + return failureResponse( QByteArray( "Unable to create mimetype '" ) + mt + QByteArray( "'." ) ); + } + mimeType = m; + } + + item.setRev( 0 ); + item.setSize( size ); + item.setMimeTypeId( mimeType.id() ); + item.setCollectionId( col.id() ); + if ( dateTime.isValid() ) { + item.setDatetime( dateTime ); + } + if ( remote_id.isEmpty() ) { + // from application + item.setDirty( true ); + } else { + // from resource + item.setRemoteId( remote_id ); + item.setDirty( false ); + } + item.setRemoteRevision( remote_revision ); + item.setGid( gid ); + item.setAtime( QDateTime::currentDateTime() ); + + return true; +} + +// This is used for clients that don't support item streaming +bool AkAppend::readParts( PimItem &pimItem ) +{ + + // parse part specification + QVector > > partSpecs; + QByteArray partName = ""; + qint64 partSize = -1; + qint64 partSizes = 0; + bool ok = false; + + qint64 realSize = pimItem.size(); + + const QList list = m_streamParser->readParenthesizedList(); + Q_FOREACH ( const QByteArray &item, list ) { + if ( partName.isEmpty() && partSize == -1 ) { + partName = item; + continue; + } + if ( item.startsWith( ':' ) ) { + int pos = 1; + ImapParser::parseNumber( item, partSize, &ok, pos ); + if ( !ok ) { + partSize = 0; + } + + int version = 0; + QByteArray plainPartName; + ImapParser::splitVersionedKey( partName, plainPartName, version ); + + partSpecs.append( qMakePair( plainPartName, qMakePair( partSize, version ) ) ); + partName = ""; + partSizes += partSize; + partSize = -1; + } + } + + realSize = qMax( partSizes, realSize ); + + const QByteArray allParts = m_streamParser->readString(); + + // chop up literal data in parts + int pos = 0; // traverse through part data now + QPair > partSpec; + Q_FOREACH ( partSpec, partSpecs ) { + // wrap data into a part + Part part; + part.setPimItemId( pimItem.id() ); + part.setPartType( PartTypeHelper::fromFqName( partSpec.first ) ); + part.setData( allParts.mid( pos, partSpec.second.first ) ); + if ( partSpec.second.second != 0 ) { + part.setVersion( partSpec.second.second ); + } + part.setDatasize( partSpec.second.first ); + + if ( !PartHelper::insert( &part ) ) { + return failureResponse( "Unable to append item part" ); + } + + pos += partSpec.second.first; + } + + if ( realSize != pimItem.size() ) { + pimItem.setSize( realSize ); + pimItem.update(); + } + + return true; +} + +bool AkAppend::insertItem( PimItem &item, const Collection &parentCol, + const QVector &itemFlags, + const QVector &itemTagsRID, + const QVector &itemTagsGID ) +{ + if ( !item.insert() ) { + return failureResponse( "Failed to append item" ); + } + + // set message flags + // This will hit an entry in cache inserted there in buildPimItem() + const Flag::List flagList = HandlerHelper::resolveFlags( itemFlags ); + bool flagsChanged = false; + if ( !DataStore::self()->appendItemsFlags( PimItem::List() << item, flagList, &flagsChanged, false, parentCol, true ) ) { + return failureResponse( "Unable to append item flags." ); + } + + Tag::List tagList; + if ( !itemTagsGID.isEmpty() ) { + tagList << HandlerHelper::resolveTagsByGID( itemTagsGID ); + } + if ( !itemTagsRID.isEmpty() ) { + tagList << HandlerHelper::resolveTagsByRID( itemTagsRID, connection()->context() ); + } + bool tagsChanged; + 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; + if ( connection()->capabilities().akAppendStreaming() ) { + QByteArray partName /* unused */; + qint64 partSize; + m_streamParser->beginList(); + PartStreamer streamer(connection(), m_streamParser, item, this); + connect( &streamer, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SIGNAL(responseAvailable(Akonadi::Server::Response)) ); + while ( !m_streamParser->atListEnd() ) { + QByteArray command = m_streamParser->readString(); + if ( command.isEmpty() ) { + throw HandlerException( "Syntax error" ); + } + + if ( !streamer.stream( command, false, partName, partSize ) ) { + throw HandlerException( streamer.error() ); + } + + partSizes += partSize; + } + + // TODO: Try to avoid this addition query + if ( partSizes > item.size() ) { + item.setSize( partSizes ); + item.update(); + } + + } else { + if ( !readParts( item ) ) { + return false; + } + } + + // Preprocessing + if ( PreprocessorManager::instance()->isActive() ) { + Part hiddenAttribute; + hiddenAttribute.setPimItemId( item.id() ); + hiddenAttribute.setPartType( PartTypeHelper::fromFqName( QString::fromLatin1( 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 ); + } + + 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::sendResponse( const QByteArray &responseStr, const PimItem &item ) +{ + // Date time is always stored in UTC time zone by the server. + const QString datetime = QLocale::c().toString( item.datetime(), QLatin1String( "dd-MMM-yyyy hh:mm:ss +0000" ) ); + + Response response; + response.setTag( tag() ); + response.setUserDefined(); + response.setString( "[UIDNEXT " + QByteArray::number( item.id() ) + " DATETIME " + ImapParser::quote( datetime.toUtf8() ) + ']' ); + Q_EMIT responseAvailable( response ); + + response.setSuccess(); + response.setString( responseStr ); + Q_EMIT responseAvailable( response ); + return true; +} + + +bool AkAppend::parseStream() +{ + // 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 ); + + ChangedAttributes itemFlags, itemTagsRID, itemTagsGID; + Collection parentCol; + PimItem item; + if ( !buildPimItem( item, parentCol, itemFlags, itemTagsRID, itemTagsGID ) ) { + return false; + } + + if ( itemFlags.incremental ) { + throw HandlerException( "Incremental flags changes are not allowed in AK-APPEND" ); + } + if ( itemTagsRID.incremental || itemTagsRID.incremental ) { + throw HandlerException( "Incremental tags changes are not allowed in AK-APPEND" ); + } + + if ( !insertItem( item, parentCol, itemFlags.added, itemTagsRID.added, itemTagsGID.added ) ) { + return false; + } + + // All SQL is done, let's commit! + if ( !transaction.commit() ) { + return failureResponse( "Failed to commit transaction" ); + } + + notify( item, parentCol ); + return sendResponse( "Append completed", item ); +} diff --git a/akonadi/server/src/handler/akappend.h b/akonadi/server/src/handler/akappend.h new file mode 100644 index 00000000..eb5597a4 --- /dev/null +++ b/akonadi/server/src/handler/akappend.h @@ -0,0 +1,91 @@ +/*************************************************************************** + * 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 +#include + +#include +#include + +class QTemporaryFile; +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: + AkAppend(); + + virtual ~AkAppend(); + + virtual bool parseStream(); + +protected: + class ChangedAttributes + { + public: + ChangedAttributes() + : incremental(false) + { + } + + bool incremental; + QVector added; + QVector removed; + }; + + bool buildPimItem( PimItem &item, + Collection &parentCollection, + ChangedAttributes &flags, + ChangedAttributes &tagsRID, + ChangedAttributes &tagsGID ); + + + bool insertItem( PimItem &item, + const Collection &parentCollection, + const QVector &itemFlags, + const QVector &itemTagsRID, + const QVector &itemTagsGID ); + + bool readParts( PimItem &item ); + + virtual bool notify( const PimItem &item, const Collection &collection ); + virtual bool sendResponse( const QByteArray &response, const PimItem &item ); + + +private: + QByteArray parseFlag( const QByteArray &flag ) const; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/append.cpp b/akonadi/server/src/handler/append.cpp new file mode 100644 index 00000000..c503216e --- /dev/null +++ b/akonadi/server/src/handler/append.cpp @@ -0,0 +1,252 @@ +/*************************************************************************** + * Copyright (C) 2006 by Till Adam * + * 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 "append.h" +#include "response.h" +#include "handlerhelper.h" +#include "libs/imapparser_p.h" +#include "imapstreamparser.h" + +#include "akonadi.h" +#include "connection.h" +#include "preprocessormanager.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/transaction.h" +#include "storage/parthelper.h" +#include "storage/dbconfig.h" +#include "storage/parttypehelper.h" + +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Append::Append() + : Handler() +{ +} + +Append::~Append() +{ +} + +bool Append::commit() +{ + bool storeInFile = false; + qint64 dataSize = 0; + QTemporaryFile tmpFile; + + //need to read out the data before the below code to avoid hangs, probably at some place the eventloop is reentered + //and more data is read into the stream and that causes the problems. + if ( m_streamParser->hasLiteral() ) { + dataSize = m_streamParser->remainingLiteralSize(); + m_size = qMax( m_size, dataSize ); + storeInFile = dataSize > DbConfig::configuredDatabase()->sizeThreshold(); + if ( storeInFile ) { + try { + PartHelper::streamToFile( m_streamParser, tmpFile ); + } catch ( const PartHelperException &e ) { + return failureResponse( e.what() ); + } + } else { + while ( !m_streamParser->atLiteralEnd() ) { + m_data += m_streamParser->readLiteralPart(); + } + } + } else { + m_data = m_streamParser->readString(); + } + + Response response; + + DataStore *db = connection()->storageBackend(); + Transaction transaction( db ); + + Collection col = HandlerHelper::collectionFromIdOrName( m_mailbox ); + if ( !col.isValid() ) { + return failureResponse( QByteArray( "Unknown collection for '" ) + m_mailbox + QByteArray( "'." ) ); + } + + QByteArray mt; + QString remote_id; + QString remote_revision; + QString gid; + QVector flags; + Q_FOREACH ( const QByteArray &flag, m_flags ) { + if ( flag.startsWith( "\\MimeType" ) ) { + int pos1 = flag.indexOf( '[' ); + int pos2 = flag.indexOf( ']', pos1 ); + mt = flag.mid( pos1 + 1, pos2 - pos1 - 1 ); + } else if ( flag.startsWith( "\\RemoteId" ) ) { + int pos1 = flag.indexOf( '[' ); + int pos2 = flag.lastIndexOf( ']' ); + remote_id = QString::fromUtf8( flag.mid( pos1 + 1, pos2 - pos1 - 1 ) ); + } else if ( flag.startsWith( "\\RemoteRevision" ) ) { + int pos1 = flag.indexOf( '[' ); + int pos2 = flag.lastIndexOf( ']' ); + remote_revision = QString::fromUtf8( flag.mid( pos1 + 1, pos2 - pos1 - 1 ) ); + } else if ( flag.startsWith( "\\Gid" ) ) { + int pos1 = flag.indexOf( '[' ); + int pos2 = flag.lastIndexOf( ']' ); + gid = QString::fromUtf8( flag.mid( pos1 + 1, pos2 - pos1 - 1 ) ); + } else { + flags << flag; + } + } + // standard imap does not know this attribute, so that's mail + if ( mt.isEmpty() ) { + mt = "message/rfc822"; + } + MimeType mimeType = MimeType::retrieveByName( QString::fromLatin1( mt ) ); + if ( !mimeType.isValid() ) { + MimeType m( QString::fromLatin1( mt ) ); + if ( !m.insert() ) { + return failureResponse( QByteArray( "Unable to create mimetype '" ) + mt + QByteArray( "'." ) ); + } + mimeType = m; + } + + PimItem item; + item.setRev( 0 ); + item.setSize( m_size ); + item.setDatetime( m_dateTime ); + + // wrap data into a part + Part part; + part.setPartType( PartTypeHelper::fromName( "PLD", "RFC822" ) ); + part.setData( m_data ); + part.setPimItemId( item.id() ); + part.setDatasize( dataSize ); + + QVector parts; + parts.append( part ); + + // If we have active preprocessors then we also set the hidden attribute + // for the UI and we enqueue the item for preprocessing. + bool doPreprocessing = PreprocessorManager::instance()->isActive(); + //akDebug() << "Append handler: doPreprocessing is" << doPreprocessing; + if ( doPreprocessing ) { + Part hiddenAttribute; + hiddenAttribute.setPartType( PartTypeHelper::fromName( "ATR", "HIDDEN" ) ); + hiddenAttribute.setData( QByteArray() ); + hiddenAttribute.setPimItemId( item.id() ); + hiddenAttribute.setDatasize( 0 ); + parts.append( hiddenAttribute ); + } + + bool ok = db->appendPimItem( parts, mimeType, col, m_dateTime, remote_id, remote_revision, gid, item ); + response.setTag( tag() ); + if ( !ok ) { + return failureResponse( "Append failed" ); + } + + // set message flags + const Flag::List flagList = HandlerHelper::resolveFlags( flags ); + bool flagsChanged = false; + if ( !db->appendItemsFlags( PimItem::List() << item, flagList, &flagsChanged, false, col ) ) { + return failureResponse( "Unable to append item flags." ); + } + + if ( storeInFile ) { + const QString fileName = PartHelper::resolveAbsolutePath( parts[0].data() ); + + //the new item was just created and the transaction is not yet committed, so delete + overwrite should be safe, as no + //client knows about the item yet + PartHelper::removeFile( fileName ); + + if ( !tmpFile.copy( fileName ) ) { + return failureResponse( "Unable to copy item part data from the temporary file" ); + } + } + + // TODO if the mailbox is currently selected, the normal new message + // actions SHOULD occur. Specifically, the server SHOULD notify the + // client immediately via an untagged EXISTS response. + + if ( !transaction.commit() ) { + return failureResponse( "Unable to commit transaction." ); + } + + if ( doPreprocessing ) { + // enqueue the item for preprocessing + PreprocessorManager::instance()->beginHandleItem( item, db ); + } + + // Date time is always stored in UTC time zone by the server. + QString datetime = QLocale::c().toString( item.datetime(), QLatin1String( "dd-MMM-yyyy hh:mm:ss +0000" ) ); + + QByteArray res( "[UIDNEXT " + QByteArray::number( item.id() ) + ' ' ); + res.append( "DATETIME " + ImapParser::quote( datetime.toUtf8() ) ); + res.append( ']' ); + + response.setTag( tag() ); + response.setUserDefined(); + response.setString( res ); + Q_EMIT responseAvailable( response ); + + response.setSuccess(); + response.setString( "Append completed" ); + Q_EMIT responseAvailable( response ); + return true; +} + +bool Append::parseStream() +{ + // Arguments: mailbox name + // OPTIONAL flag parenthesized list + // OPTIONAL date/time string + // message literal + // + // Syntax: + // append = "APPEND" SP mailbox SP size [SP flag-list] [SP date-time] SP literal + + m_mailbox = m_streamParser->readString(); + + m_size = m_streamParser->readNumber(); + + // parse optional flag parenthesized list + // Syntax: + // flag-list = "(" [flag *(SP flag)] ")" + // flag = "\ANSWERED" / "\FLAGGED" / "\DELETED" / "\SEEN" / + // "\DRAFT" / flag-keyword / flag-extension + // ; Does not include "\Recent" + // flag-extension = "\" atom + // flag-keyword = atom + if ( m_streamParser->hasList() ) { + m_flags = m_streamParser->readParenthesizedList(); + } + + // parse optional date/time string + if ( m_streamParser->hasDateTime() ) { + m_dateTime = m_streamParser->readDateTime(); + m_dateTime = m_dateTime.toUTC(); + // FIXME Should we return an error if m_dateTime is invalid? + } else { + // if date/time is not given then it will be set to the current date/time + // converted to UTC. + m_dateTime = QDateTime::currentDateTime().toUTC(); + } + + return commit(); +} diff --git a/akonadi/server/src/handler/append.h b/akonadi/server/src/handler/append.h new file mode 100644 index 00000000..6d65771e --- /dev/null +++ b/akonadi/server/src/handler/append.h @@ -0,0 +1,59 @@ +/*************************************************************************** + * 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 AKONADIAPPEND_H +#define AKONADIAPPEND_H + +#include + +#include + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the Append command. + */ +class Append : public Handler +{ + Q_OBJECT +public: + Append(); + + ~Append(); + + bool parseStream(); + +protected: + bool commit(); + +private: + qint64 m_size; + QByteArray m_data; + QByteArray m_mailbox; + QDateTime m_dateTime; + QList m_flags; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/capability.cpp b/akonadi/server/src/handler/capability.cpp new file mode 100644 index 00000000..d6a3a9f9 --- /dev/null +++ b/akonadi/server/src/handler/capability.cpp @@ -0,0 +1,75 @@ +/*************************************************************************** + * 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 "capability.h" +#include "response.h" +#include "imapstreamparser.h" +#include "clientcapabilities.h" +#include "connection.h" + +#include + +#include + +using namespace Akonadi; +using namespace Server; + +Capability::Capability(): Handler() +{ +} + +Capability::~Capability() +{ +} + +bool Capability::parseStream() +{ + ClientCapabilities capabilities; + + m_streamParser->beginList(); + while ( !m_streamParser->atListEnd() ) { + const QByteArray capability = m_streamParser->readString(); + if ( capability.isEmpty() ) { + break; // shouldn't happen + } + if ( capability == AKONADI_PARAM_CAPABILITY_NOTIFY ) { + capabilities.setNotificationMessageVersion( m_streamParser->readNumber() ); + } else if ( capability == AKONADI_PARAM_CAPABILITY_NOPAYLOADPATH ) { + capabilities.setNoPayloadPath( true ); + } else if ( capability == AKONADI_PARAM_CAPABILITY_SERVERSEARCH ) { + capabilities.setServerSideSearch( true ); + } else if ( capability == AKONADI_PARAM_CAPABILITY_AKAPPENDSTREAMING ) { + capabilities.setAkAppendStreaming( true ); + } else if ( capability == AKONADI_PARAM_CAPABILITY_DIRECTSTREAMING ) { + capabilities.setDirectStreaming( true ); + } else { + qDebug() << Q_FUNC_INFO << "Unknown client capability:" << capability; + } + } + + connection()->setCapabilities( capabilities ); + + Response response; + response.setSuccess(); + response.setTag( tag() ); + response.setString( "CAPABILITY completed" ); + Q_EMIT responseAvailable( response ); + return true; +} diff --git a/akonadi/server/src/handler/capability.h b/akonadi/server/src/handler/capability.h new file mode 100644 index 00000000..b7733a5c --- /dev/null +++ b/akonadi/server/src/handler/capability.h @@ -0,0 +1,67 @@ +/*************************************************************************** + * 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 AKONADICAPABILITY_H +#define AKONADICAPABILITY_H + +#include + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the capability command. + +

Syntax

+ Request: + @verbatim + CAPABILITY + @endverbatim + + Response: + @verbatim + [ ] + OK + @endverbatim + +

Client Capabilities

+ - @c NOTIFY version - version of the notification message format + - @c NOPAYLOADPATH - only filename of external payload file is expected + +

Server Capabilities

+ None defined yet. + + @since ASAP 32, Akonadi 1.10 + */ +class Capability : public Handler +{ + Q_OBJECT +public: + Capability(); + ~Capability(); + + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/colcopy.cpp b/akonadi/server/src/handler/colcopy.cpp new file mode 100644 index 00000000..c3385bc7 --- /dev/null +++ b/akonadi/server/src/handler/colcopy.cpp @@ -0,0 +1,124 @@ +/* + 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" +#include "imapstreamparser.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() +{ + QByteArray tmp = m_streamParser->readString(); + const Collection source = HandlerHelper::collectionFromIdOrName( tmp ); + if ( !source.isValid() ) { + return failureResponse( "No valid source specified" ); + } + + tmp = m_streamParser->readString(); + const Collection target = HandlerHelper::collectionFromIdOrName( tmp ); + 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( "COLCOPY complete" ); +} diff --git a/akonadi/server/src/handler/colcopy.h b/akonadi/server/src/handler/colcopy.h new file mode 100644 index 00000000..1793694e --- /dev/null +++ b/akonadi/server/src/handler/colcopy.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_COLCOPY_H +#define AKONADI_COLCOPY_H + +#include +#include + +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 + +

Syntax

+ + Request: + @verbatim + request = tag " COLCOPY " collection-id " " collection-id + @endverbatim + + There is only the usual status response indicating success or failure of the + COLCOPY command + */ +class ColCopy : public Copy +{ + Q_OBJECT + public: + bool parseStream(); + + private: + bool copyCollection( const Collection &source, const Collection &target ); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/colmove.cpp b/akonadi/server/src/handler/colmove.cpp new file mode 100644 index 00000000..3fe0bcb0 --- /dev/null +++ b/akonadi/server/src/handler/colmove.cpp @@ -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 Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +ColMove::ColMove( Scope::SelectionScope scope ) + : m_scope( scope ) +{ +} + +bool ColMove::parseStream() +{ + m_scope.parseScope( m_streamParser ); + SelectQueryBuilder qb; + CollectionQueryHelper::scopeToQuery( m_scope, connection(), qb ); + if ( !qb.exec() ) { + throw HandlerException( "Unable to execute collection query" ); + } + const Collection::List sources = qb.result(); + if ( sources.isEmpty() ) { + throw HandlerException( "No source collection specified" ); + } else if ( sources.size() > 1 ) { // TODO + throw HandlerException( "Moving multiple collections is not yet implemented" ); + } + Collection source = sources.first(); + + Scope destScope( m_scope.scope() ); + destScope.parseScope( m_streamParser ); + akDebug() << destScope.uidSet().toImapSequenceSet(); + const Collection target = CollectionQueryHelper::singleCollectionFromScope( destScope, connection() ); + + if ( source.parentId() == target.id() ) { + return successResponse( "COLMOVE complete - nothing to do" ); + } + + 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( "COLMOVE complete" ); +} diff --git a/akonadi/server/src/handler/colmove.h b/akonadi/server/src/handler/colmove.h new file mode 100644 index 00000000..ab37eb7e --- /dev/null +++ b/akonadi/server/src/handler/colmove.h @@ -0,0 +1,66 @@ +/* + 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 +#include "scope.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. + +

Syntax

+ + Request: + @verbatim + request = tag [" RID"] " COLMOVE " collection-ids " " collection-id + @endverbatim + + @c collection-ids is the set of collections that should be moved, either as UID-set + or as a list of RIDs (in case the @c RID prefix is given). + + @c collection-id is a single collection UID and describes the target collection. + + There is only the usual status response indicating success or failure of the + COLMOVE command +*/ +class ColMove : public Handler +{ + Q_OBJECT + public: + ColMove( Scope::SelectionScope scope ); + virtual bool parseStream(); + + private: + Scope m_scope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/copy.cpp b/akonadi/server/src/handler/copy.cpp new file mode 100644 index 00000000..6148ae48 --- /dev/null +++ b/akonadi/server/src/handler/copy.cpp @@ -0,0 +1,118 @@ +/* + 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 "libs/imapset_p.h" +#include "imapstreamparser.h" + +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; + 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() +{ + ImapSet set = m_streamParser->readSequenceSet(); + if ( set.isEmpty() ) { + return failureResponse( "No items specified" ); + } + + CacheCleanerInhibitor inhibitor; + + ItemRetriever retriever( connection() ); + retriever.setItemSet( set ); + retriever.setRetrieveFullPayload( true ); + if ( !retriever.exec() ) { + return failureResponse( retriever.lastError() ); + } + + const QByteArray tmp = m_streamParser->readString(); + const Collection targetCollection = HandlerHelper::collectionFromIdOrName( tmp ); + 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( set, 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( "COPY complete" ); +} diff --git a/akonadi/server/src/handler/copy.h b/akonadi/server/src/handler/copy.h new file mode 100644 index 00000000..5309d5c4 --- /dev/null +++ b/akonadi/server/src/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 +#include + +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 " seqeunce-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/akonadi/server/src/handler/create.cpp b/akonadi/server/src/handler/create.cpp new file mode 100644 index 00000000..82c13592 --- /dev/null +++ b/akonadi/server/src/handler/create.cpp @@ -0,0 +1,236 @@ +/*************************************************************************** + * 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 +#include + +#include "connection.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/transaction.h" +#include "handlerhelper.h" +#include "storage/selectquerybuilder.h" +#include "libs/protocol_p.h" + +#include "response.h" +#include "libs/imapparser_p.h" +#include "imapstreamparser.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +Create::Create( Scope::SelectionScope scope ) + : Handler() + , m_scope( scope ) +{ +} + +static Tristate getTristateValue( const QByteArray &value ) +{ + if ( value == "TRUE" ){ + return Tristate::True; + } else if ( value == "FALSE" ){ + return Tristate::False; + } + return Tristate::Undefined; +} + +bool Create::parseStream() +{ + QString name = m_streamParser->readUtf8String(); + if ( name.isEmpty() ) { + return failureResponse( "Invalid collection name" ); + } + + bool ok = false; + Collection parent; + + if ( m_scope == Scope::Uid || m_scope == Scope::None ) { + qint64 parentId = m_streamParser->readNumber( &ok ); + if ( !ok ) { // RFC 3501 compat + QString parentPath; + int index = name.lastIndexOf( QLatin1Char( '/' ) ); + if ( index > 0 ) { + parentPath = name.left( index ); + name = name.mid( index + 1 ); + parent = HandlerHelper::collectionFromIdOrName( parentPath.toUtf8() ); + } else { + parentId = 0; + } + } else { + if ( parentId > 0 ) { + parent = Collection::retrieveById( parentId ); + } + } + + if ( parentId != 0 && !parent.isValid() ) { + return failureResponse( "Parent collection not found" ); + } + } else if ( m_scope == Scope::Rid ) { + const QString rid = m_streamParser->readUtf8String(); + if ( rid.isEmpty() ) { + throw HandlerException( "Empty parent remote identifier" ); + } + if ( !connection()->context()->resource().isValid() ) { + throw HandlerException( "Invalid resource context" ); + } + SelectQueryBuilder qb; + qb.addValueCondition( Collection::remoteIdColumn(), Query::Equals, rid ); + qb.addValueCondition( Collection::resourceIdColumn(), Query::Equals, connection()->context()->resource().id() ); + if ( !qb.exec() ) { + throw HandlerException( "Unable to execute collection query" ); + } + const Collection::List cols = qb.result(); + if ( cols.size() == 0 ) { + throw HandlerException( "Parent collection not found" ); + } else if ( cols.size() > 1 ) { + throw HandlerException( "Parent collection is not unique" ); + } + parent = cols.first(); + } + + qint64 resourceId = 0; + bool forceVirtual = false; + MimeType::List parentContentTypes; + if ( parent.isValid() ) { + // 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; + if ( foundVirtual ) { + break; + } + } else if ( mt.name() == QLatin1String( "application/x-vnd.akonadi.collection.virtual" ) ) { + foundVirtual = true; + if ( found ) { + 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 { + // deduce owning resource from current session id + 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( name ); + collection.setResourceId( resourceId ); + + // attributes + QList attributes; + QList mimeTypes; + QVector< QPair > userDefAttrs; + bool mimeTypesSet = false; + attributes = m_streamParser->readParenthesizedList(); + for ( int i = 0; i < attributes.count() - 1; i += 2 ) { + const QByteArray key = attributes.at( i ); + const QByteArray value = attributes.at( i + 1 ); + if ( key == AKONADI_PARAM_REMOTEID ) { + collection.setRemoteId( QString::fromUtf8( value ) ); + } else if ( key == AKONADI_PARAM_REMOTEREVISION ) { + collection.setRemoteRevision( QString::fromUtf8( value ) ); + } else if ( key == AKONADI_PARAM_MIMETYPE ) { + ImapParser::parseParenthesizedList( value, mimeTypes ); + mimeTypesSet = true; + } else if ( key == AKONADI_PARAM_CACHEPOLICY ) { + HandlerHelper::parseCachePolicy( value, collection ); + } else if ( key == AKONADI_PARAM_VIRTUAL ) { + collection.setIsVirtual( value.toUInt() != 0 ); + } else if ( key == AKONADI_PARAM_ENABLED ) { + collection.setEnabled( getTristateValue( value ) == Server::True ); + } else if ( key == AKONADI_PARAM_SYNC ) { + collection.setSyncPref( getTristateValue( value ) ); + } else if ( key == AKONADI_PARAM_DISPLAY ) { + collection.setDisplayPref( getTristateValue( value ) ); + } else if ( key == AKONADI_PARAM_INDEX ) { + collection.setIndexPref( getTristateValue( value ) ); + } else { + userDefAttrs << qMakePair( key, value ); + } + } + + if ( forceVirtual ) { + collection.setIsVirtual( true ); + } + + DataStore *db = connection()->storageBackend(); + Transaction transaction( db ); + + if ( !db->appendCollection( collection ) ) { + return failureResponse( "Could not create collection " + name.toLocal8Bit() + " resourceId: " + QByteArray::number( resourceId ) ); + } + + QStringList effectiveMimeTypes; + if ( mimeTypesSet ) { + Q_FOREACH ( const QByteArray &b, mimeTypes ) + effectiveMimeTypes << QString::fromUtf8( b ); + } else { + Q_FOREACH ( const MimeType &mt, parentContentTypes ) + effectiveMimeTypes << mt.name(); + } + if ( !db->appendMimeTypeForCollection( collection.id(), effectiveMimeTypes ) ) { + return failureResponse( "Unable to append mimetype for collection " + name.toLocal8Bit() + " resourceId: " + QByteArray::number( resourceId ) ); + } + + // store user defined attributes + typedef QPair QByteArrayPair; + Q_FOREACH ( const QByteArrayPair &attr, userDefAttrs ) { + if ( !db->addCollectionAttribute( collection, attr.first, attr.second ) ) { + return failureResponse( "Unable to add collection attribute." ); + } + } + + Response response; + response.setUntagged(); + + // write out collection details + db->activeCachePolicy( collection ); + const QByteArray b = HandlerHelper::collectionToByteArray( collection ); + response.setString( b ); + Q_EMIT responseAvailable( response ); + + if ( !transaction.commit() ) { + return failureResponse( "Unable to commit transaction." ); + } + + return successResponse( "CREATE completed" ); +} diff --git a/akonadi/server/src/handler/create.h b/akonadi/server/src/handler/create.h new file mode 100644 index 00000000..ed921229 --- /dev/null +++ b/akonadi/server/src/handler/create.h @@ -0,0 +1,63 @@ +/*************************************************************************** + * 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 +#include "scope.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. + +

Syntax

+ + Request: + @verbatim + tag [" RID"] " CREATE " collection-name " " parent-collection " (" attribute-list ")" + @endverbatim + + @c attribute-list is the same as defined in AkList. + @c parent-collection is either a collection UID or a collection RID, depending on the command prefix + + Response: + A untagged response identical to AkList is sent for every created collection. + */ +class Create : public Handler +{ + Q_OBJECT +public: + Create( Scope::SelectionScope scope ); + + bool parseStream(); + + private: + Scope::SelectionScope m_scope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/delete.cpp b/akonadi/server/src/handler/delete.cpp new file mode 100644 index 00000000..de3ac4ef --- /dev/null +++ b/akonadi/server/src/handler/delete.cpp @@ -0,0 +1,105 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include "imapstreamparser.h" +#include +#include +#include "libs/protocol_p.h" +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Delete::Delete( Scope scope ) + : Handler() + , m_scope( scope ) +{ +} + +bool Delete::deleteRecursive( Collection &col ) +{ + Collection::List children = col.children(); + Q_FOREACH ( Collection child, children ) { + if ( !deleteRecursive( child ) ) { + return false; + } + } + DataStore *db = connection()->storageBackend(); + return db->cleanupCollection( col ); +} + +bool Delete::parseStream() +{ + m_scope.parseScope( m_streamParser ); + connection()->context()->parseContext( m_streamParser ); + + SelectQueryBuilder qb; + CollectionQueryHelper::scopeToQuery( m_scope, connection(), qb ); + if ( !qb.exec() ) { + throw HandlerException( "Unable to execute collection query" ); + } + const Collection::List collections = qb.result(); + if ( collections.isEmpty() ) { + throw HandlerException( "No collection selected" ); + } else if ( collections.size() > 1 ) { + throw HandlerException( "Deleting multiple collections is not yet implemented" ); + } + + // check if collection exists + DataStore *db = connection()->storageBackend(); + Transaction transaction( db ); + + Collection collection = collections.first(); + 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" ); + } + } + + if ( !deleteRecursive( collection ) ) { + return failureResponse( "Unable to delete collection" ); + } + + if ( !transaction.commit() ) { + return failureResponse( "Unable to commit transaction." ); + } + + Response response; + response.setTag( tag() ); + response.setString( "DELETE completed" ); + Q_EMIT responseAvailable( response ); + return true; +} diff --git a/akonadi/server/src/handler/delete.h b/akonadi/server/src/handler/delete.h new file mode 100644 index 00000000..48274c91 --- /dev/null +++ b/akonadi/server/src/handler/delete.h @@ -0,0 +1,67 @@ +/* + 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 +#include +#include + +namespace Akonadi { +namespace Server { + +/** + @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. + +

Syntax

+ + Request: + @verbatim + request = tag [" RID"] " DELETE " collection-ids + @endverbatim + + @c collection-ids is the set of collections that should be deleted, either as UID-set + or as a list of RIDs (in case the @c RID prefix is given). + + There is only the usual status response indicating success or failure of the + DELETE command +*/ +class Delete : public Handler +{ + Q_OBJECT + public: + Delete( Scope scope ); + bool parseStream(); + + private: + bool deleteRecursive( Collection &col ); + Scope m_scope; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/expunge.cpp b/akonadi/server/src/handler/expunge.cpp new file mode 100644 index 00000000..1a0f00ed --- /dev/null +++ b/akonadi/server/src/handler/expunge.cpp @@ -0,0 +1,96 @@ +/*************************************************************************** + * 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 "expunge.h" + +#include "akonadi.h" +#include "connection.h" +#include "response.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" +#include "imapstreamparser.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +Expunge::Expunge() + : Handler() +{ +} + +Expunge::~Expunge() +{ +} + +bool Expunge::parseStream() +{ + Response response; + + DataStore *store = connection()->storageBackend(); + Transaction transaction( store ); + + Flag flag = Flag::retrieveByName( QLatin1String( "\\DELETED" ) ); + if ( !flag.isValid() ) { + response.setError(); + response.setString( "\\DELETED flag unknown" ); + + Q_EMIT responseAvailable( response ); + return true; + } + + SelectQueryBuilder qb; + qb.addJoin( QueryBuilder::InnerJoin, PimItemFlagRelation::tableName(), + PimItemFlagRelation::leftFullColumnName(), PimItem::idFullColumnName() ); + qb.addValueCondition( PimItemFlagRelation::rightFullColumnName(), Query::Equals, flag.id() ); + + if ( qb.exec() ) { + const QVector items = qb.result(); + if ( store->cleanupPimItems( items ) ) { + // FIXME: Change the protocol to EXPUNGE + list of removed ids + Q_FOREACH ( const PimItem &item, items ) { + response.setUntagged(); + // IMAP protocol violation: should actually be the sequence number + response.setString( QByteArray::number( item.id() ) + " EXPUNGE" ); + + Q_EMIT responseAvailable( response ); + } + } else { + response.setTag( tag() ); + response.setError(); + response.setString( "internal error" ); + + Q_EMIT responseAvailable( response ); + return true; + } + } else { + throw HandlerException( "Unable to execute query." ); + } + + if ( !transaction.commit() ) { + return failureResponse( "Unable to commit transaction." ); + } + + response.setTag( tag() ); + response.setSuccess(); + response.setString( "EXPUNGE completed" ); + + Q_EMIT responseAvailable( response ); + return true; +} diff --git a/akonadi/server/src/handler/expunge.h b/akonadi/server/src/handler/expunge.h new file mode 100644 index 00000000..d2d0f84e --- /dev/null +++ b/akonadi/server/src/handler/expunge.h @@ -0,0 +1,47 @@ +/*************************************************************************** + * 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 AKONADIEXPUNGE_H +#define AKONADIEXPUNGE_H + +#include + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the expunge command. + */ +class Expunge : public Handler +{ + Q_OBJECT + public: + Expunge(); + + ~Expunge(); + + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/fetch.cpp b/akonadi/server/src/handler/fetch.cpp new file mode 100644 index 00000000..069c355c --- /dev/null +++ b/akonadi/server/src/handler/fetch.cpp @@ -0,0 +1,73 @@ +/*************************************************************************** + * 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 "akonadi.h" +#include "connection.h" +#include "fetchhelper.h" +#include "response.h" +#include "storage/selectquerybuilder.h" +#include "imapstreamparser.h" +#include "cachecleaner.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Fetch::Fetch( Scope::SelectionScope scope ) + : mScope( scope ) +{ +} + +bool Fetch::parseStream() +{ + // sequence set + mScope.parseScope( m_streamParser ); + + // context + connection()->context()->parseContext( m_streamParser ); + // We require context in case we do RID fetch + if ( connection()->context()->isEmpty() && mScope.scope() == Scope::Rid ) { + throw HandlerException( "No FETCH context specified" ); + } + + CacheCleanerInhibitor inhibitor; + + FetchHelper fetchHelper( connection(), mScope, FetchScope( m_streamParser ) ); + connect( &fetchHelper, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SIGNAL(responseAvailable(Akonadi::Server::Response)) ); + + if ( !fetchHelper.fetchItems( AKONADI_CMD_ITEMFETCH ) ) { + return false; + } + + if ( mScope.scope() == Scope::Uid ) { + successResponse( "UID FETCH completed" ); + } else if ( mScope.scope() == Scope::Rid ) { + successResponse( "RID FETCH completed" ); + } else if ( mScope.scope() == Scope::Gid ) { + successResponse( "GID FETCH completed" ); + } else { + successResponse( "FETCH completed" ); + } + + return true; +} diff --git a/akonadi/server/src/handler/fetch.h b/akonadi/server/src/handler/fetch.h new file mode 100644 index 00000000..d8dba96b --- /dev/null +++ b/akonadi/server/src/handler/fetch.h @@ -0,0 +1,65 @@ +/*************************************************************************** + * 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 + +#include "scope.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the fetch command. + + Request syntax: + @verbatim + fetch-request = tag " " [scope-selector " "] "FETCH " scope " " fetch-parameters " " part-list + scope-selector = [ "UID" / "RID" ] + fetch-parameters = [ "FULLPAYLOAD" / "CACHEONLY" / "CACHEONLY" / "EXTERNALPAYLOAD" / "ANCESTORS " depth ] + part-list = "(" *(part-id) ")" + depth = "0" / "1" / "INF" + @endverbatim + + Semantics: + - @c FULLPAYLOAD: Retrieve the full payload + - @c CACHEONLY: Restrict retrieval to parts already in the cache, even if more parts have been requested. + - @c EXTERNALPAYLOAD: Indicate the capability to retrieve parts via the filesystem instead over the socket + - @c ANCESTORS: Indicate the desired ancestor collection depth (0 is the default) + */ +class Fetch : public Handler +{ + Q_OBJECT + public: + Fetch( Scope::SelectionScope scope ); + + bool parseStream(); + + private: + Scope mScope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/fetchhelper.cpp b/akonadi/server/src/handler/fetchhelper.cpp new file mode 100644 index 00000000..c0123625 --- /dev/null +++ b/akonadi/server/src/handler/fetchhelper.cpp @@ -0,0 +1,667 @@ +/*************************************************************************** + * 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 "akdebug.h" +#include "akdbus.h" +#include "akonadi.h" +#include "connection.h" +#include "handler.h" +#include "handlerhelper.h" +#include "imapstreamparser.h" +#include "libs/imapparser_p.h" +#include "libs/protocol_p.h" +#include "response.h" +#include "storage/selectquerybuilder.h" +#include "storage/itemqueryhelper.h" +#include "storage/itemretrievalmanager.h" +#include "storage/itemretrievalrequest.h" +#include "storage/parthelper.h" +#include +#include "storage/transaction.h" +#include "utils.h" +#include "intervalcheck.h" +#include "agentmanagerinterface.h" +#include "dbusconnectionpool.h" +#include "tagfetchhelper.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +FetchHelper::FetchHelper( Connection *connection, const Scope &scope, const FetchScope &fetchScope ) + : mStreamParser( 0 ) + , mConnection( connection ) + , mScope( scope ) + , mFetchScope( fetchScope ) +{ + std::fill( mItemQueryColumnMap, mItemQueryColumnMap + ItemQueryColumnCount, -1 ); +} + +enum PartQueryColumns { + PartQueryPimIdColumn, + PartQueryTypeNamespaceColumn, + PartQueryTypeNameColumn, + PartQueryDataColumn, + PartQueryExternalColumn, + PartQueryVersionColumn +}; + +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.addJoin( QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName() ); + partQuery.addColumn( PimItem::idFullColumnName() ); + partQuery.addColumn( PartType::nsFullColumnName() ); + partQuery.addColumn( PartType::nameFullColumnName() ); + partQuery.addColumn( Part::dataFullColumnName() ); + partQuery.addColumn( Part::externalFullColumnName() ); + + partQuery.addColumn( Part::versionFullColumnName() ); + + partQuery.addSortColumn( PimItem::idFullColumnName(), Query::Descending ); + + Query::Condition cond( Query::Or ); + if ( !partList.isEmpty() ) { + QStringList partNameList; + Q_FOREACH ( const QByteArray &b, partList ) { + if ( b.startsWith( "PLD" ) || b.startsWith( "ATR" ) ) { + partNameList.push_back( QString::fromLatin1( b ) ); + } + } + if ( !partNameList.isEmpty() ) { + cond.addCondition( PartTypeHelper::conditionFromFqNames( partNameList ) ); + } + } + + if ( allPayload ) { + cond.addValueCondition( PartType::nsFullColumnName(), Query::Equals, QLatin1String( "PLD" ) ); + } + if ( allAttrs ) { + cond.addValueCondition( PartType::nsFullColumnName(), Query::Equals, QLatin1String( "ATR" ) ); + } + + if ( !cond.isEmpty() ) { + 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() ); + + itemQuery.addJoin( QueryBuilder::InnerJoin, MimeType::tableName(), + PimItem::mimeTypeIdFullColumnName(), MimeType::idFullColumnName() ); + + int column = 0; + #define ADD_COLUMN(colName, colId) { itemQuery.addColumn( colName ); mItemQueryColumnMap[colId] = column++; } + ADD_COLUMN( PimItem::idFullColumnName(), ItemQueryPimItemIdColumn ); + if ( mFetchScope.remoteIdRequested() ) { + ADD_COLUMN( PimItem::remoteIdFullColumnName(), ItemQueryPimItemRidColumn ) + } + ADD_COLUMN( MimeType::nameFullColumnName(), ItemQueryMimeTypeColumn ) + ADD_COLUMN( PimItem::revFullColumnName(), ItemQueryRevColumn ) + if ( mFetchScope.remoteRevisionRequested() ) { + ADD_COLUMN( PimItem::remoteRevisionFullColumnName(), ItemQueryRemoteRevisionColumn ) + } + if ( mFetchScope.sizeRequested() ) { + ADD_COLUMN( PimItem::sizeFullColumnName(), ItemQuerySizeColumn ) + } + if ( mFetchScope.mTimeRequested() ) { + ADD_COLUMN( PimItem::datetimeFullColumnName(), ItemQueryDatetimeColumn ) + } + ADD_COLUMN( PimItem::collectionIdFullColumnName(), ItemQueryCollectionIdColumn ) + if ( mFetchScope.gidRequested() ) { + ADD_COLUMN( PimItem::gidFullColumnName(), ItemQueryPimItemGidColumn ) + } + #undef ADD_COLUMN + + itemQuery.addSortColumn( PimItem::idFullColumnName(), Query::Descending ); + + if ( mScope.scope() != Scope::Invalid ) { + 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 { + FlagQueryIdColumn, + FlagQueryNameColumn +}; + +QSqlQuery FetchHelper::buildFlagQuery() +{ + QueryBuilder flagQuery( PimItem::tableName() ); + flagQuery.addJoin( QueryBuilder::InnerJoin, PimItemFlagRelation::tableName(), + PimItem::idFullColumnName(), PimItemFlagRelation::leftFullColumnName() ); + flagQuery.addJoin( QueryBuilder::InnerJoin, Flag::tableName(), + Flag::idFullColumnName(), PimItemFlagRelation::rightFullColumnName() ); + flagQuery.addColumn( PimItem::idFullColumnName() ); + flagQuery.addColumn( Flag::nameFullColumnName() ); + 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_baloo_indexer" ) ) { + 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( AkDBus::serviceName( AkDBus::Control ), + QLatin1String( "/AgentManager" ), + DBusConnectionPool::threadConnection() ); + const QString typeIdentifier = manager.agentInstanceType( resourceName ); + const QVariantMap properties = manager.agentCustomProperties( typeIdentifier ); + return properties.value( QLatin1String( "HasLocalStorage" ), false ).toBool(); +} + +QByteArray FetchHelper::tagsToByteArray( const Tag::List &tags ) +{ + QByteArray b; + QList attributes; + b += "("; + Q_FOREACH ( const Tag &tag, tags ) { + b += "(" + TagFetchHelper::tagToByteArray( tag.id(), + tag.gid().toLatin1(), + tag.parentId(), + tag.tagType().name().toLatin1(), + QByteArray(), + TagFetchHelper::fetchTagAttributes( tag.id() ) ) + ") "; + } + b += ")"; + return b; +} + +bool FetchHelper::fetchItems( const QByteArray &responseIdentifier ) +{ + // 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. + if ( !mFetchScope.cacheOnly() || isScopeLocal( mScope ) ) { + // 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( QString::fromLatin1( "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( QString::fromLatin1( "Unable to fetch item from backend (collection %1) : %2" ) + .arg( mConnection->context()->collectionId() ) + .arg( QString::fromLatin1( retriever.lastError() ) ) ); + } + } + } + + QSqlQuery itemQuery = buildItemQuery(); + + // 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 + QSqlQuery partQuery; + if ( !mFetchScope.requestedParts().isEmpty() || mFetchScope.fullPayload() || mFetchScope.allAttributes() ) { + partQuery = buildPartQuery( mFetchScope.requestedParts(), mFetchScope.fullPayload(), mFetchScope.allAttributes() ); + } + + // build flag query if needed + QSqlQuery flagQuery; + if ( mFetchScope.flagsRequested() ) { + flagQuery = buildFlagQuery(); + } + + // build tag query if needed + QSqlQuery tagQuery; + if ( mFetchScope.tagsRequested() ) { + tagQuery = buildTagQuery(); + } + + QSqlQuery vRefQuery; + if ( mFetchScope.virtualReferencesRequested() ) { + vRefQuery = buildVRefQuery(); + } + + // build responses + Response response; + response.setUntagged(); + while ( itemQuery.isValid() ) { + const qint64 pimItemId = extractQueryResult( itemQuery, ItemQueryPimItemIdColumn ).toLongLong(); + const int pimItemRev = extractQueryResult( itemQuery, ItemQueryRevColumn ).toInt(); + + QList attributes; + attributes.append( AKONADI_PARAM_UID " " + QByteArray::number( pimItemId ) ); + attributes.append( AKONADI_PARAM_REVISION " " + QByteArray::number( pimItemRev ) ); + if ( mFetchScope.remoteIdRequested() ) { + attributes.append( AKONADI_PARAM_REMOTEID " " + ImapParser::quote( Utils::variantToByteArray( extractQueryResult( itemQuery, ItemQueryPimItemRidColumn ) ) ) ); + } + attributes.append( AKONADI_PARAM_MIMETYPE " " + ImapParser::quote( Utils::variantToByteArray( extractQueryResult( itemQuery, ItemQueryMimeTypeColumn ) ) ) ); + Collection::Id parentCollectionId = extractQueryResult( itemQuery, ItemQueryCollectionIdColumn ).toLongLong(); + attributes.append( AKONADI_PARAM_COLLECTIONID " " + QByteArray::number( parentCollectionId ) ); + + if ( mFetchScope.sizeRequested() ) { + const qint64 pimItemSize = extractQueryResult( itemQuery, ItemQuerySizeColumn ).toLongLong(); + attributes.append( AKONADI_PARAM_SIZE " " + QByteArray::number( pimItemSize ) ); + } + if ( mFetchScope.mTimeRequested() ) { + const QDateTime pimItemDatetime = extractQueryResult( itemQuery, ItemQueryDatetimeColumn ).toDateTime(); + // Date time is always stored in UTC time zone by the server. + QString datetime = QLocale::c().toString( pimItemDatetime, QLatin1String( "dd-MMM-yyyy hh:mm:ss +0000" ) ); + attributes.append( AKONADI_PARAM_MTIME " " + ImapParser::quote( datetime.toUtf8() ) ); + } + if ( mFetchScope.remoteRevisionRequested() ) { + const QByteArray rrev = Utils::variantToByteArray( extractQueryResult( itemQuery, ItemQueryRemoteRevisionColumn ) ); + if ( !rrev.isEmpty() ) { + attributes.append( AKONADI_PARAM_REMOTEREVISION " " + ImapParser::quote( rrev ) ); + } + } + if ( mFetchScope.gidRequested() ) { + const QByteArray gid = Utils::variantToByteArray( extractQueryResult( itemQuery, ItemQueryPimItemGidColumn ) ); + if ( !gid.isEmpty() ) { + attributes.append( AKONADI_PARAM_GID " " + ImapParser::quote( gid ) ); + } + } + + if ( mFetchScope.flagsRequested() ) { + QList flags; + while ( flagQuery.isValid() ) { + const qint64 id = flagQuery.value( FlagQueryIdColumn ).toLongLong(); + if ( id > pimItemId ) { + flagQuery.next(); + continue; + } else if ( id < pimItemId ) { + break; + } + flags << Utils::variantToByteArray( flagQuery.value( FlagQueryNameColumn ) ); + flagQuery.next(); + } + attributes.append( AKONADI_PARAM_FLAGS " (" + ImapParser::join( flags, " " ) + ')' ); + } + + if ( mFetchScope.tagsRequested() ) { + ImapSet tags; + QVector tagIds; + //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() ) { + const qint64 id = tagQuery.value( TagQueryItemIdColumn ).toLongLong(); + if ( id > pimItemId ) { + tagQuery.next(); + continue; + } else if ( id < pimItemId ) { + break; + } + const qint64 tagId = tagQuery.value( TagQueryTagIdColumn ).toLongLong(); + tags.add( QVector() << tagId ); + + tagIds << tagId; + tagQuery.next(); + } + if ( !fullTagsRequested ) { + if ( !tags.isEmpty() ) { + attributes.append( AKONADI_PARAM_TAGS " " + tags.toImapSequenceSet() ); + } + } else { + Tag::List tagList; + Q_FOREACH ( qint64 t, tagIds ) { + tagList << Tag::retrieveById( t ); + } + attributes.append( AKONADI_PARAM_TAGS " " + tagsToByteArray( tagList ) ); + } + } + + if ( mFetchScope.virtualReferencesRequested() ) { + ImapSet cols; + while ( vRefQuery.isValid() ) { + const qint64 id = vRefQuery.value( VRefQueryItemIdColumn ).toLongLong(); + if ( id > pimItemId ) { + vRefQuery.next(); + continue; + } else if ( id < pimItemId ) { + break; + } + const qint64 collectionId = vRefQuery.value( VRefQueryCollectionIdColumn ).toLongLong(); + cols.add( QVector() << collectionId ); + vRefQuery.next(); + } + if ( !cols.isEmpty() ) { + attributes.append( AKONADI_PARAM_VIRTREF " " + cols.toImapSequenceSet() ); + } + } + + if ( mFetchScope.ancestorDepth() > 0 ) { + attributes.append( HandlerHelper::ancestorsToByteArray( mFetchScope.ancestorDepth(), ancestorsForItem( parentCollectionId ) ) ); + } + + bool skipItem = false; + + QList cachedParts; + + while ( partQuery.isValid() ) { + const qint64 id = partQuery.value( PartQueryPimIdColumn ).toLongLong(); + if ( id > pimItemId ) { + partQuery.next(); + continue; + } else if ( id < pimItemId ) { + break; + } + const QByteArray partName = Utils::variantToByteArray( partQuery.value( PartQueryTypeNamespaceColumn ) ) + ':' + + Utils::variantToByteArray( partQuery.value( PartQueryTypeNameColumn ) ); + QByteArray part = partName; + QByteArray data = Utils::variantToByteArray( partQuery.value( PartQueryDataColumn ) ); + + if ( mFetchScope.checkCachedPayloadPartsOnly() ) { + if ( !data.isEmpty() ) { + cachedParts << part; + } + 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; + } + const bool partIsExternal = partQuery.value( PartQueryExternalColumn ).toBool(); + if ( !mFetchScope.externalPayloadSupported() && partIsExternal ) { //external payload not supported by the client, translate the data + data = PartHelper::translateData( data, partIsExternal ); + } + int version = partQuery.value( PartQueryVersionColumn ).toInt(); + if ( version != 0 ) { // '0' is the default, so don't send it + part += '[' + QByteArray::number( version ) + ']'; + } + if ( mFetchScope.externalPayloadSupported() && partIsExternal ) { // external data and this is supported by the client + part += " [FILE] "; + } + if ( data.isNull() ) { + part += " NIL"; + } else if ( data.isEmpty() ) { + part += " \"\""; + } else { + if ( partIsExternal ) { + if ( !mConnection->capabilities().noPayloadPath() ) { + data = PartHelper::resolveAbsolutePath( data ).toLocal8Bit(); + } + } + + part += " {" + QByteArray::number( data.length() ) + "}\r\n"; + part += data; + } + + if ( mFetchScope.requestedParts().contains( partName ) || mFetchScope.fullPayload() || mFetchScope.allAttributes() ) { + attributes << part; + } + + partQuery.next(); + } + } + + if ( skipItem ) { + itemQuery.next(); + continue; + } + + if ( mFetchScope.checkCachedPayloadPartsOnly() ) { + attributes.append( AKONADI_PARAM_CACHEDPARTS " (" + ImapParser::join( cachedParts, " " ) + ')' ); + } + + // IMAP protocol violation: should actually be the sequence number + QByteArray attr = QByteArray::number( pimItemId ) + ' ' + responseIdentifier + " ("; + attr += ImapParser::join( attributes, " " ) + ')'; + response.setUntagged(); + response.setString( attr ); + Q_EMIT responseAvailable( response ); + + itemQuery.next(); + } + + // update atime (only if the payload was actually requested, otherwise a simple resource sync prevents cache clearing) + if ( needsAccessTimeUpdate( mFetchScope.requestedParts() ) || mFetchScope.fullPayload() ) { + updateItemAccessTime(); + } + + 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::currentDateTime() ); + ItemQueryHelper::scopeToQuery( mScope, mConnection->context(), qb ); + + if ( !qb.exec() ) { + qWarning() << "Unable to update item access time"; + } else { + transaction.commit(); + } +} + +void FetchHelper::triggerOnDemandFetch() +{ + if ( mScope.scope() != Scope::None || 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 ); + } +} + +QStack FetchHelper::ancestorsForItem( Collection::Id parentColId ) +{ + if ( mFetchScope.ancestorDepth() <= 0 || parentColId == 0 ) { + return QStack(); + } + if ( mAncestorCache.contains( parentColId ) ) { + return mAncestorCache.value( parentColId ); + } + + QStack ancestors; + Collection col = Collection::retrieveById( parentColId ); + for ( int i = 0; i < mFetchScope.ancestorDepth(); ++i ) { + if ( !col.isValid() ) { + break; + } + ancestors.prepend( col ); + 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/akonadi/server/src/handler/fetchhelper.h b/akonadi/server/src/handler/fetchhelper.h new file mode 100644 index 00000000..684d7e04 --- /dev/null +++ b/akonadi/server/src/handler/fetchhelper.h @@ -0,0 +1,96 @@ +/*************************************************************************** + * 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 "fetchscope.h" +#include "libs/imapset_p.h" +#include "storage/countquerybuilder.h" +#include "storage/datastore.h" +#include "storage/itemretriever.h" + +class FetchHelperTest; + +namespace Akonadi { + +class ImapSet; + +namespace Server { + +class Connection; +class Response; + +class FetchHelper : public QObject +{ + Q_OBJECT + + public: + FetchHelper( Connection *connection, const Scope &scope, const FetchScope &fetchScope ); + + bool fetchItems( const QByteArray &responseIdentifier ); + + Q_SIGNALS: + void responseAvailable( const Akonadi::Server::Response &response ); + + private: + enum ItemQueryColumns { + ItemQueryPimItemIdColumn, + ItemQueryPimItemRidColumn, + ItemQueryMimeTypeColumn, + 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(); + QStack 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); + + private: + ImapStreamParser *mStreamParser; + + Connection *mConnection; + QHash > mAncestorCache; + Scope mScope; + FetchScope mFetchScope; + int mItemQueryColumnMap[ItemQueryColumnCount]; + + friend class ::FetchHelperTest; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/fetchscope.cpp b/akonadi/server/src/handler/fetchscope.cpp new file mode 100644 index 00000000..2d443ec7 --- /dev/null +++ b/akonadi/server/src/handler/fetchscope.cpp @@ -0,0 +1,415 @@ +/* + 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 "fetchscope.h" +#include "imapstreamparser.h" +#include "protocol_p.h" +#include "handlerhelper.h" +#include "handler.h" + +using namespace Akonadi::Server; + +class FetchScope::Private : public QSharedData +{ + public: + Private(); + Private( const Private &other ); + + void parseCommandStream(); + void parsePartList(); + + ImapStreamParser *mStreamParser; + + QVector mRequestedParts; + QStringList mRequestedPayloads; + QDateTime mChangedSince; + + int mAncestorDepth; + uint mCacheOnly : 1; + uint mCheckCachedPayloadPartsOnly : 1; + uint mFullPayload : 1; + uint mAllAttrs : 1; + uint mSizeRequested : 1; + uint mMTimeRequested : 1; + uint mExternalPayloadSupported : 1; + uint mRemoteRevisionRequested : 1; + uint mIgnoreErrors : 1; + uint mFlagsRequested : 1; + uint mRemoteIdRequested : 1; + uint mGidRequested : 1; + uint mTagsRequested : 1; + uint mVirtRefRequested: 1; + QVector mTagFetchScope; +}; + +FetchScope::Private::Private() + : QSharedData() + , mStreamParser( 0 ) + , mAncestorDepth( 0 ) + , mCacheOnly( false ) + , mCheckCachedPayloadPartsOnly( false ) + , mFullPayload( false ) + , mAllAttrs( false ) + , mSizeRequested( false ) + , mMTimeRequested( false ) + , mExternalPayloadSupported( false ) + , mRemoteRevisionRequested( false ) + , mIgnoreErrors( false ) + , mFlagsRequested( false ) + , mRemoteIdRequested( false ) + , mGidRequested( false ) + , mTagsRequested( false ) + , mVirtRefRequested( false ) +{ +} + +FetchScope::Private::Private( const Private &other ) + : QSharedData( other ) + , mStreamParser( other.mStreamParser ) + , mRequestedParts( other.mRequestedParts ) + , mRequestedPayloads( other.mRequestedPayloads ) + , mChangedSince( other.mChangedSince ) + , mAncestorDepth( other.mAncestorDepth ) + , mCacheOnly( other.mCacheOnly ) + , mCheckCachedPayloadPartsOnly( other.mCheckCachedPayloadPartsOnly ) + , mFullPayload( other.mFullPayload ) + , mAllAttrs( other.mAllAttrs ) + , mSizeRequested( other.mSizeRequested ) + , mMTimeRequested( other.mMTimeRequested ) + , mExternalPayloadSupported( other.mExternalPayloadSupported ) + , mRemoteRevisionRequested( other.mRemoteRevisionRequested ) + , mIgnoreErrors( other.mIgnoreErrors ) + , mFlagsRequested( other.mFlagsRequested ) + , mRemoteIdRequested( other.mRemoteIdRequested ) + , mGidRequested( other.mGidRequested ) + , mTagsRequested( other.mTagsRequested ) + , mVirtRefRequested( other.mVirtRefRequested ) + , mTagFetchScope( other.mTagFetchScope ) +{ +} + + +void FetchScope::Private::parseCommandStream() +{ + Q_ASSERT( mStreamParser ); + + // macro vs. attribute list + Q_FOREVER { + if ( mStreamParser->atCommandEnd() ) { + break; + } + if ( mStreamParser->hasList() ) { + parsePartList(); + break; + } else { + const QByteArray buffer = mStreamParser->readString(); + if ( buffer == AKONADI_PARAM_CACHEONLY ) { + mCacheOnly = true; + } else if ( buffer == AKONADI_PARAM_CHECKCACHEDPARTSONLY ) { + mCheckCachedPayloadPartsOnly = true; + } else if ( buffer == AKONADI_PARAM_ALLATTRIBUTES ) { + mAllAttrs = true; + } else if ( buffer == AKONADI_PARAM_EXTERNALPAYLOAD ) { + mExternalPayloadSupported = true; + } else if ( buffer == AKONADI_PARAM_FULLPAYLOAD ) { + mRequestedParts << AKONADI_PARAM_PLD_RFC822; // HACK: temporary workaround until we have support for detecting the availability of the full payload in the server + mFullPayload = true; + } else if ( buffer == AKONADI_PARAM_ANCESTORS ) { + mAncestorDepth = HandlerHelper::parseDepth( mStreamParser->readString() ); + } else if ( buffer == AKONADI_PARAM_IGNOREERRORS ) { + mIgnoreErrors = true; + } else if ( buffer == AKONADI_PARAM_CHANGEDSINCE ) { + bool ok = false; + mChangedSince = QDateTime::fromTime_t( mStreamParser->readNumber( &ok ) ); + if ( !ok ) { + throw HandlerException( "Invalid CHANGEDSINCE timestamp" ); + } + } else { + throw HandlerException( "Invalid command argument" ); + } + } + } +} + +void FetchScope::Private::parsePartList() +{ + mStreamParser->beginList(); + while ( !mStreamParser->atListEnd() ) { + const QByteArray b = mStreamParser->readString(); + // filter out non-part attributes we send all the time + if ( b == AKONADI_PARAM_REVISION || b == AKONADI_PARAM_UID ) { + continue; + } else if ( b == AKONADI_PARAM_REMOTEID ) { + mRemoteIdRequested = true; + } else if ( b == AKONADI_PARAM_FLAGS ) { + mFlagsRequested = true; + } else if ( b == AKONADI_PARAM_SIZE ) { + mSizeRequested = true; + } else if ( b == AKONADI_PARAM_MTIME ) { + mMTimeRequested = true; + } else if ( b == AKONADI_PARAM_REMOTEREVISION ) { + mRemoteRevisionRequested = true; + } else if ( b == AKONADI_PARAM_GID ) { + mGidRequested = true; + } else if ( b == AKONADI_PARAM_TAGS ) { + mTagsRequested = true; + if (mStreamParser->hasList()) { + mStreamParser->beginList(); + while (!mStreamParser->atListEnd()) { + mTagFetchScope << mStreamParser->readString(); + } + } + } else if ( b == AKONADI_PARAM_COLLECTIONID ) { + // we always return collection IDs anyway + } else if ( b == AKONADI_PARAM_VIRTREF ) { + mVirtRefRequested = true; + } else { + mRequestedParts.push_back( b ); + if ( b.startsWith( AKONADI_PARAM_PLD ) ) { + mRequestedPayloads.push_back( QString::fromLatin1( b ) ); + } + } + } +} + +FetchScope::FetchScope() + : d( new Private ) +{ +} + +FetchScope::FetchScope( ImapStreamParser *streamParser ) + : d( new Private ) +{ + d->mStreamParser = streamParser; + d->parseCommandStream(); +} + +FetchScope::FetchScope( const FetchScope &other ) + : d( other.d ) +{ +} + +FetchScope::~FetchScope() +{ +} + +FetchScope &FetchScope::operator=( const FetchScope &other ) +{ + if ( this != &other ) { + d = other.d; + } + + return *this; +} + +bool FetchScope::isValid() const +{ + return d->mStreamParser != 0; +} + +ImapStreamParser *FetchScope::streamParser() const +{ + return d->mStreamParser; +} + + +void FetchScope::setRequestedParts( const QVector &requestedParts ) +{ + d->mRequestedParts = requestedParts; +} + +QVector FetchScope::requestedParts() const +{ + return d->mRequestedParts; +} + +void FetchScope::setRequestedPayloads( const QStringList &payloads ) +{ + d->mRequestedPayloads = payloads; +} + +QStringList FetchScope::requestedPayloads() const +{ + return d->mRequestedPayloads; +} + +void FetchScope::setChangedSince( const QDateTime &dt ) +{ + d->mChangedSince = dt; +} + +QDateTime FetchScope::changedSince() const +{ + return d->mChangedSince; +} + +void FetchScope::setAncestorDepth( int depth ) +{ + d->mAncestorDepth = depth; +} + +int FetchScope::ancestorDepth() const +{ + return d->mAncestorDepth; +} + +void FetchScope::setCacheOnly( bool cacheOnly ) +{ + d->mCacheOnly = cacheOnly; +} + +bool FetchScope::cacheOnly() const +{ + return d->mCacheOnly; +} + +void FetchScope::setCheckCachedPayloadPartsOnly( bool checkCachedPayloadPartsOnly ) +{ + d->mCheckCachedPayloadPartsOnly = checkCachedPayloadPartsOnly; +} + +bool FetchScope::checkCachedPayloadPartsOnly() const +{ + return d->mCheckCachedPayloadPartsOnly; +} + +void FetchScope::setFullPayload( bool fullPayload ) +{ + d->mFullPayload = fullPayload; +} + +bool FetchScope::fullPayload() const +{ + return d->mFullPayload; +} + +void FetchScope::setAllAttributes( bool allAttributes ) +{ + d->mAllAttrs = allAttributes; +} + +bool FetchScope::allAttributes() const +{ + return d->mAllAttrs; +} + +void FetchScope::setSizeRequested( bool sizeRequested ) +{ + d->mSizeRequested = sizeRequested; +} + +bool FetchScope::sizeRequested() const +{ + return d->mSizeRequested; +} + +void FetchScope::setMTimeRequested( bool mTimeRequested ) +{ + d->mMTimeRequested = mTimeRequested; +} + +bool FetchScope::mTimeRequested() const +{ + return d->mMTimeRequested; +} + +void FetchScope::setExternalPayloadSupported( bool externalPayloadSupported ) +{ + d->mExternalPayloadSupported = externalPayloadSupported; +} + +bool FetchScope::externalPayloadSupported() const +{ + return d->mExternalPayloadSupported; +} + +void FetchScope::setRemoteRevisionRequested( bool remoteRevisionRequested ) +{ + d->mRemoteRevisionRequested = remoteRevisionRequested; +} + +bool FetchScope::remoteRevisionRequested() const +{ + return d->mRemoteRevisionRequested; +} + +void FetchScope::setIgnoreErrors( bool ignoreErrors ) +{ + d->mIgnoreErrors = ignoreErrors; +} + +bool FetchScope::ignoreErrors() const +{ + return d->mIgnoreErrors; +} + +void FetchScope::setFlagsRequested( bool flagsRequested ) +{ + d->mFlagsRequested = flagsRequested; +} + +bool FetchScope::flagsRequested() const +{ + return d->mFlagsRequested; +} + +void FetchScope::setRemoteIdRequested( bool remoteIdRequested ) +{ + d->mRemoteIdRequested = remoteIdRequested; +} + +bool FetchScope::remoteIdRequested() const +{ + return d->mRemoteIdRequested; +} + +void FetchScope::setGidRequested( bool gidRequested ) +{ + d->mGidRequested = gidRequested; +} + +bool FetchScope::gidRequested() const +{ + return d->mGidRequested; +} + +void FetchScope::setTagsRequested( bool tagsRequested ) +{ + d->mTagsRequested = tagsRequested; +} + +bool FetchScope::tagsRequested() const +{ + return d->mTagsRequested; +} + +QVector FetchScope::tagFetchScope() const +{ + return d->mTagFetchScope; +} + +void FetchScope::setVirtualReferencesRequested( bool vRefRequested ) +{ + d->mVirtRefRequested = vRefRequested; +} + +bool FetchScope::virtualReferencesRequested() const +{ + return d->mVirtRefRequested; +} diff --git a/akonadi/server/src/handler/fetchscope.h b/akonadi/server/src/handler/fetchscope.h new file mode 100644 index 00000000..5b323d4c --- /dev/null +++ b/akonadi/server/src/handler/fetchscope.h @@ -0,0 +1,94 @@ +/* + 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_FETCHSCOPE_H +#define AKONADI_FETCHSCOPE_H + +#include +#include +#include +#include + +namespace Akonadi { +namespace Server { + +class ImapStreamParser; + +class FetchScope +{ + public: + FetchScope(); + FetchScope( ImapStreamParser *streamParser ); + FetchScope( const FetchScope &other ); + ~FetchScope(); + + FetchScope &operator=( const FetchScope &other ); + + bool isValid() const; + + ImapStreamParser *streamParser() const; + + void setRequestedParts( const QVector &requestedParts ); + QVector requestedParts() const; + void setRequestedPayloads( const QStringList &payloads ); + QStringList requestedPayloads() const; + void setChangedSince( const QDateTime &dt ); + QDateTime changedSince() const; + void setAncestorDepth( int depth ); + int ancestorDepth() const; + void setCacheOnly( bool cacheOnly ); + bool cacheOnly() const; + void setCheckCachedPayloadPartsOnly( bool checkCachedPayloadPartsOnly ); + bool checkCachedPayloadPartsOnly() const; + void setFullPayload( bool fullPayload ); + bool fullPayload() const; + void setAllAttributes( bool allAttributes ); + bool allAttributes() const; + void setSizeRequested( bool sizeRequested ); + bool sizeRequested() const; + void setMTimeRequested( bool mTimeRequested ); + bool mTimeRequested() const; + void setExternalPayloadSupported( bool externalPayloadSupported ); + bool externalPayloadSupported() const; + void setRemoteRevisionRequested( bool remoteRevisionRequested ); + bool remoteRevisionRequested() const; + void setIgnoreErrors( bool ignoreErrors ); + bool ignoreErrors() const; + void setFlagsRequested( bool flagsRequested ); + bool flagsRequested() const; + void setRemoteIdRequested( bool remoteIdRequested ); + bool remoteIdRequested() const; + void setGidRequested( bool gidRequested ); + bool gidRequested() const; + void setTagsRequested( bool tagsRequested ); + bool tagsRequested() const; + QVector tagFetchScope() const; + void setVirtualReferencesRequested( bool vRefRequested ); + bool virtualReferencesRequested() const; + + private: + class Private; + QSharedDataPointer d; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif //AKONADI_FETCHSCOPE_H diff --git a/akonadi/server/src/handler/link.cpp b/akonadi/server/src/handler/link.cpp new file mode 100644 index 00000000..ce18e47a --- /dev/null +++ b/akonadi/server/src/handler/link.cpp @@ -0,0 +1,110 @@ +/* + 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 "entities.h" + +#include "imapstreamparser.h" +#include + +using namespace Akonadi::Server; + +Link::Link( Scope::SelectionScope scope, bool create ) + : Handler() + , mDestinationScope( scope ) + , mCreateLinks( create ) +{ +} + +bool Link::parseStream() +{ + mDestinationScope.parseScope( m_streamParser ); + const Collection collection = CollectionQueryHelper::singleCollectionFromScope( mDestinationScope, connection() ); + + if ( !collection.isVirtual() ) { + return failureResponse( "Can't link items to non-virtual collections" ); + } + + 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( itemScope, connection()->context(), qb ); + + // Restore resource context if necessary + 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; + Q_FOREACH ( const PimItem &item, items ) { + const bool alreadyLinked = collection.relatesToPimItem( item ); + bool result = true; + if ( mCreateLinks && !alreadyLinked ) { + result = collection.addPimItem( item ); + toLink << item; + } else if ( !mCreateLinks && 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( "LINK complete" ); +} diff --git a/akonadi/server/src/handler/link.h b/akonadi/server/src/handler/link.h new file mode 100644 index 00000000..a4f74cce --- /dev/null +++ b/akonadi/server/src/handler/link.h @@ -0,0 +1,65 @@ +/* + 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" +#include "scope.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. + * + *

Syntax

+ * + * Request: + * @verbatim + * request = tag [" " selection-scope] " " [" LINK "|" UNLINK "] collection-id " " [selection-scope " "] item-set + * @endverbatim + * + * There is only the usual status response indicating success or failure of the + * LINK and UNLINK commands. + */ +class Link : public Handler +{ + Q_OBJECT + public: + /** + * @param create @c true adds references, @c false removes them + */ + Link( Scope::SelectionScope scope, bool create ); + bool parseStream(); + + private: + Scope mDestinationScope; + bool mCreateLinks; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/list.cpp b/akonadi/server/src/handler/list.cpp new file mode 100644 index 00000000..b891d10d --- /dev/null +++ b/akonadi/server/src/handler/list.cpp @@ -0,0 +1,332 @@ +/* + 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 + +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/selectquerybuilder.h" + +#include "connection.h" +#include "response.h" +#include "handlerhelper.h" +#include "imapstreamparser.h" +#include "collectionreferencemanager.h" + +#include +#include + +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( Scope::SelectionScope scope, bool onlySubscribed ): + Handler(), + mScope( scope ), + mAncestorDepth( 0 ), + mOnlySubscribed( onlySubscribed ), + 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; + } + parent = parent.parent(); + ancestors.prepend( parent ); + } + return ancestors; +} + +bool List::listCollection( const Collection &root, int depth, const QStack &ancestors ) +{ + // recursive listing of child collections + bool childrenFound = false; + if ( depth > 0 ) { + const Collection::List children = retrieveChildren( root.id() ); + QStack ancestorsAndMe( ancestors ); + ancestorsAndMe.push( root ); + Q_FOREACH ( const Collection &col, children ) { + if ( listCollection( col, depth - 1, ancestorsAndMe ) ) { + childrenFound = true; + } + } + } + + const bool isReferencedFromSession = connection()->collectionReferenceManager()->isReferenced( root.id(), connection()->sessionId() ); + //The collection is referenced, but not from this session. + if ( root.referenced() && !isReferencedFromSession ) { + //Don't include the collection when only looking for enabled collections + if ( mEnabledCollections && !root.enabled() ) { + return false; + } + //Don't include the collection when only looking for collections to display/index/sync + if ( mCollectionsToDisplay && + ( ( ( root.displayPref() == Tristate::Undefined ) && !root.enabled() ) || + ( root.displayPref() == Tristate::False ) ) ) { + return false; + } + if ( mCollectionsToIndex && + ( ( ( root.indexPref() == Tristate::Undefined ) && !root.enabled() ) || + ( root.indexPref() == Tristate::False ) ) ) { + return false; + } + //Single collection sync will still work since that is using a base fetch + if ( mCollectionsToSynchronize && + ( ( ( root.syncPref() == Tristate::Undefined ) && !root.enabled() ) || + ( root.syncPref() == Tristate::False ) ) ) { + return false; + } + } + + // filter if this node isn't needed by it's children + const bool hidden = ( mResource.isValid() && root.resourceId() != mResource.id() ) + || ( !mMimeTypes.isEmpty() && !intersect( mMimeTypes, root.mimeTypes() ) ); + + if ( !childrenFound && hidden ) { + return false; + } + + // write out collection details + Collection dummy = root; + DataStore *db = connection()->storageBackend(); + db->activeCachePolicy( dummy ); + const QByteArray b = HandlerHelper::collectionToByteArray( dummy, hidden, mIncludeStatistics, mAncestorDepth, ancestors, isReferencedFromSession ); + + Response response; + response.setUntagged(); + response.setString( b ); + Q_EMIT responseAvailable( response ); + + return true; +} + +static Query::Condition filterCondition( const QString &column ) +{ + Query::Condition orCondition( Query::Or ); + orCondition.addValueCondition( column, Query::Equals, Akonadi::Server::Tristate::True ); + Query::Condition andCondition( Query::And ); + andCondition.addValueCondition( column, Query::Equals, Akonadi::Server::Tristate::Undefined ); + andCondition.addValueCondition( Collection::enabledFullColumnName(), Query::Equals, true ); + orCondition.addCondition( andCondition ); + orCondition.addValueCondition( Collection::referencedFullColumnName(), Query::Equals, true ); + return orCondition; +} + +Collection::List List::retrieveChildren( const QVariant &value ) +{ + SelectQueryBuilder qb; + if ( value.isNull() ) { + qb.addValueCondition( Collection::parentIdColumn(), Query::Is, QVariant() ); + } else { + qb.addValueCondition( Collection::parentIdColumn(), Query::Equals, value ); + } + + if ( mEnabledCollections ) { + Query::Condition orCondition( Query::Or ); + orCondition.addValueCondition( Collection::enabledFullColumnName(), Query::Equals, true ); + orCondition.addValueCondition( Collection::referencedFullColumnName(), Query::Equals, true ); + qb.addCondition(orCondition); + } else if ( mCollectionsToSynchronize ) { + qb.addCondition( filterCondition( Collection::syncPrefFullColumnName() ) ); + } else if ( mCollectionsToDisplay ) { + qb.addCondition( filterCondition( Collection::displayPrefFullColumnName() ) ); + } else if ( mCollectionsToIndex ) { + qb.addCondition( filterCondition( Collection::indexPrefFullColumnName() ) ); + } + if ( !qb.exec() ) { + throw HandlerException( "Unable to retrieve collection for listing" ); + } + return qb.result(); +} + +bool List::parseStream() +{ + qint64 baseCollection = -1; + QString rid; + if ( mScope.scope() == Scope::None || mScope.scope() == Scope::Uid ) { + bool ok = false; + baseCollection = m_streamParser->readNumber( &ok ); + if ( !ok ) { + return failureResponse( "Invalid base collection" ); + } + } else if ( mScope.scope() == Scope::Rid ) { + rid = m_streamParser->readUtf8String(); + if ( rid.isEmpty() ) { + throw HandlerException( "No remote identifier specified" ); + } + } else if ( mScope.scope() == Scope::HierarchicalRid ) { + mScope.parseScope( m_streamParser ); + } else { + throw HandlerException( "WTF" ); + } + + int depth = HandlerHelper::parseDepth( m_streamParser->readString() ); + + m_streamParser->beginList(); + while ( !m_streamParser->atListEnd() ) { + const QByteArray filter = m_streamParser->readString(); + if ( filter == AKONADI_PARAM_RESOURCE ) { + mResource = Resource::retrieveByName( m_streamParser->readUtf8String() ); + if ( !mResource.isValid() ) { + return failureResponse( "Unknown resource" ); + } + } else if ( filter == AKONADI_PARAM_MIMETYPE ) { + m_streamParser->beginList(); + while ( !m_streamParser->atListEnd() ) { + const QString mtName = m_streamParser->readUtf8String(); + const MimeType mt = MimeType::retrieveByName( mtName ); + if ( mt.isValid() ) { + mMimeTypes.append( mt.id() ); + } else { + MimeType mt ( mtName ); + if ( !mt.insert() ) { + throw HandlerException( "Failed to create mimetype record" ); + } + mMimeTypes.append( mt.id() ); + } + } + } else if ( filter == AKONADI_PARAM_ENABLED ) { + mEnabledCollections = true; + } else if ( filter == AKONADI_PARAM_SYNC ) { + mCollectionsToSynchronize = true; + } else if ( filter == AKONADI_PARAM_DISPLAY ) { + mCollectionsToDisplay = true; + } else if ( filter == AKONADI_PARAM_INDEX ) { + mCollectionsToIndex = true; + } + } + + //For backwards compatibilty with the subscription mechanism + mEnabledCollections = mEnabledCollections || mOnlySubscribed; + + if ( m_streamParser->hasList() ) { // We got extra options + m_streamParser->beginList(); + while ( !m_streamParser->atListEnd() ) { + const QByteArray option = m_streamParser->readString(); + if ( option == AKONADI_PARAM_STATISTICS ) { + if ( m_streamParser->readString() == "true" ) { + mIncludeStatistics = true; + } + } + if ( option == AKONADI_PARAM_ANCESTORS ) { + const QByteArray argument = m_streamParser->readString(); + mAncestorDepth = HandlerHelper::parseDepth( argument ); + } + } + } + + Collection::List collections; + QStack ancestors; + + if ( baseCollection != 0 ) { // not root + Collection col; + if ( mScope.scope() == Scope::None || mScope.scope() == Scope::Uid ) { + col = Collection::retrieveById( baseCollection ); + } else if ( mScope.scope() == Scope::Rid ) { + SelectQueryBuilder qb; + qb.addValueCondition( Collection::remoteIdFullColumnName(), Query::Equals, 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 { + throw HandlerException( "Cannot retrieve collection based on remote identifier without a resource context" ); + } + if ( !qb.exec() ) { + throw HandlerException( "Unable to retrieve collection for listing" ); + } + Collection::List results = qb.result(); + if ( results.count() != 1 ) { + throw HandlerException( QByteArray::number( results.count() ) + " collections found" ); + } + col = results.first(); + } else if ( mScope.scope() == Scope::HierarchicalRid ) { + if ( !connection()->context()->resource().isValid() ) { + throw HandlerException( "Cannot retrieve collection based on hierarchical remote identifier without a resource context" ); + } + col = CollectionQueryHelper::resolveHierarchicalRID( mScope.ridChain(), connection()->context()->resource().id() ); + } else { + throw HandlerException( "WTF" ); + } + + if ( !col.isValid() ) { + return failureResponse( "Collection " + QByteArray::number( baseCollection ) + " does not exist" ); + } + + if ( depth == 0 ) { //Base listing + collections << col; + ancestors = ancestorsForCollection( col ); + } else { //First level or recursive listing + collections << retrieveChildren( col.id() ); + --depth; + if ( !collections.isEmpty() ) { + ancestors = ancestorsForCollection( collections.first() ); + } + } + } else { //Root folder listing + if ( depth != 0 ) { + Collection::List list = retrieveChildren( QVariant() ); + collections << list; + } + --depth; + } + + Q_FOREACH ( const Collection &col, collections ) { + listCollection( col, depth, ancestors ); + } + + Response response; + response.setSuccess(); + response.setTag( tag() ); + response.setString( "List completed" ); + Q_EMIT responseAvailable( response ); + return true; +} diff --git a/akonadi/server/src/handler/list.h b/akonadi/server/src/handler/list.h new file mode 100644 index 00000000..56401b31 --- /dev/null +++ b/akonadi/server/src/handler/list.h @@ -0,0 +1,116 @@ +/* + 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 +#include +#include + +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( Scope::SelectionScope scope, bool onlySubscribed ); + + bool parseStream(); + + private: + bool listCollection( const Collection &root, int depth, const QStack &ancestors ); + QStack ancestorsForCollection( const Collection &col ); + Collection::List retrieveChildren(const QVariant &value); + + private: + Resource mResource; + QVector mMimeTypes; + Scope mScope; + int mAncestorDepth; + bool mOnlySubscribed; + bool mIncludeStatistics; + bool mEnabledCollections; + bool mCollectionsToDisplay; + bool mCollectionsToSynchronize; + bool mCollectionsToIndex; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/login.cpp b/akonadi/server/src/handler/login.cpp new file mode 100644 index 00000000..2371694c --- /dev/null +++ b/akonadi/server/src/handler/login.cpp @@ -0,0 +1,45 @@ +/*************************************************************************** + * 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 "response.h" +#include "connection.h" +#include "imapstreamparser.h" + +using namespace Akonadi::Server; + +Login::Login(): Handler() +{ +} + +bool Login::parseStream() +{ + const QByteArray sessionId = m_streamParser->readString(); + if ( sessionId.isEmpty() ) { + return failureResponse( "Missing session identifier." ); + } + connection()->setSessionId( sessionId ); + + // ignore anything that follows, for Roundcube compatibility + m_streamParser->readUntilCommandEnd(); + + successResponse( "User logged in" ); + Q_EMIT connectionStateChange( Authenticated ); + return true; +} diff --git a/akonadi/server/src/handler/login.h b/akonadi/server/src/handler/login.h new file mode 100644 index 00000000..b3553e64 --- /dev/null +++ b/akonadi/server/src/handler/login.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 AKONADILOGIN_H +#define AKONADILOGIN_H + +#include + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the login command. +*/ +class Login : public Handler +{ + Q_OBJECT + public: + Login(); + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/logout.cpp b/akonadi/server/src/handler/logout.cpp new file mode 100644 index 00000000..daf5c1bd --- /dev/null +++ b/akonadi/server/src/handler/logout.cpp @@ -0,0 +1,49 @@ +/*************************************************************************** + * 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" + +#include + +#include "response.h" + +using namespace Akonadi::Server; + +Logout::Logout(): Handler() +{ +} + +Logout::~Logout() +{ +} + +bool Logout::parseStream() +{ + Response response; + response.setBye(); + response.setString( "Akonadi server logging out" ); + response.setUntagged(); + Q_EMIT responseAvailable( response ); + + response.setSuccess(); + response.setTag( tag() ); + response.setString( "Logout completed" ); + Q_EMIT responseAvailable( response ); + Q_EMIT connectionStateChange( LoggingOut ); + return true; +} diff --git a/akonadi/server/src/handler/logout.h b/akonadi/server/src/handler/logout.h new file mode 100644 index 00000000..ef806142 --- /dev/null +++ b/akonadi/server/src/handler/logout.h @@ -0,0 +1,47 @@ +/*************************************************************************** + * 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 + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the logout command. + */ +class Logout : public Handler +{ + Q_OBJECT + public: + Logout(); + + ~Logout(); + + bool parseStream(); + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/merge.cpp b/akonadi/server/src/handler/merge.cpp new file mode 100644 index 00000000..fffe1009 --- /dev/null +++ b/akonadi/server/src/handler/merge.cpp @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2014 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) 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 "merge.h" +#include "fetchhelper.h" +#include "imapstreamparser.h" +#include "storage/selectquerybuilder.h" +#include "storage/datastore.h" +#include "storage/transaction.h" +#include "storage/parthelper.h" +#include "storage/parttypehelper.h" +#include "storage/itemretriever.h" +#include "storage/partstreamer.h" +#include "connection.h" +#include "handlerhelper.h" +#include + +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Merge::Merge() + : AkAppend() +{ +} + +Merge::~Merge() +{ +} + +bool Merge::mergeItem( PimItem &newItem, PimItem ¤tItem, + const ChangedAttributes &itemFlags, + const ChangedAttributes &itemTagsRID, + const ChangedAttributes &itemTagsGID ) +{ + if ( newItem.rev() > 0 ) { + currentItem.setRev( newItem.rev() ); + } + if ( !newItem.remoteId().isEmpty() && currentItem.remoteId() != newItem.remoteId() ) { + currentItem.setRemoteId( newItem.remoteId() ); + mChangedParts << AKONADI_PARAM_REMOTEID; + } + if ( !newItem.remoteRevision().isEmpty() && currentItem.remoteRevision() != newItem.remoteRevision() ) { + currentItem.setRemoteRevision( newItem.remoteRevision() ); + mChangedParts << AKONADI_PARAM_REMOTEREVISION; + } + if ( !newItem.gid().isEmpty() && currentItem.gid() != newItem.gid() ) { + currentItem.setGid( newItem.gid() ); + mChangedParts << AKONADI_PARAM_GID; + } + if ( newItem.datetime().isValid() ) { + currentItem.setDatetime( newItem.datetime() ); + } + currentItem.setAtime( QDateTime::currentDateTime() ); + + // Only mark dirty when merged from application + currentItem.setDirty( !connection()->context()->resource().isValid() ); + + const Collection col = Collection::retrieveById( newItem.collectionId() ); + if ( itemFlags.incremental ) { + bool flagsAdded = false, flagsRemoved = false; + if ( !itemFlags.added.isEmpty() ) { + const Flag::List addedFlags = HandlerHelper::resolveFlags( itemFlags.added ); + DataStore::self()->appendItemsFlags( PimItem::List() << currentItem, addedFlags, + &flagsAdded, true, col, true ); + } + + if ( !itemFlags.removed.isEmpty() ) { + const Flag::List removedFlags = HandlerHelper::resolveFlags( itemFlags.removed ); + DataStore::self()->removeItemsFlags( PimItem::List() << currentItem, removedFlags, + &flagsRemoved, true ); + } + + if ( flagsAdded || flagsRemoved ) { + mChangedParts << AKONADI_PARAM_FLAGS; + } + } else if ( !itemFlags.added.isEmpty() ) { + bool flagsChanged = false; + const Flag::List flags = HandlerHelper::resolveFlags( itemFlags.added ); + DataStore::self()->setItemsFlags( PimItem::List() << currentItem, flags, + &flagsChanged, true ); + if ( flagsChanged ) { + mChangedParts << AKONADI_PARAM_FLAGS; + } + } + + if ( itemTagsRID.incremental ) { + bool tagsAdded = false, tagsRemoved = false; + if ( !itemTagsRID.added.isEmpty() ) { + const Tag::List addedTags = HandlerHelper::resolveTagsByRID( itemTagsRID.added, connection()->context() ); + DataStore::self()->appendItemsTags( PimItem::List() << currentItem, addedTags, + &tagsAdded, true, col, true ); + } + + if ( !itemTagsRID.removed.isEmpty() ) { + const Tag::List removedTags = HandlerHelper::resolveTagsByRID( itemTagsRID.removed, connection()->context() ); + DataStore::self()->removeItemsTags( PimItem::List() << currentItem, removedTags, + &tagsRemoved, true ); + } + + if ( tagsAdded || tagsRemoved ) { + mChangedParts << AKONADI_PARAM_TAGS; + } + } else if ( !itemTagsRID.added.isEmpty() ) { + bool tagsChanged = false; + const Tag::List tags = HandlerHelper::resolveTagsByRID( itemTagsRID.added, connection()->context() ); + DataStore::self()->setItemsTags( PimItem::List() << currentItem, tags, + &tagsChanged, true ); + if ( tagsChanged ) { + mChangedParts << AKONADI_PARAM_TAGS; + } + } + + if ( itemTagsGID.incremental ) { + bool tagsAdded = false, tagsRemoved = false; + if ( !itemTagsGID.added.isEmpty() ) { + const Tag::List addedTags = HandlerHelper::resolveTagsByGID( itemTagsGID.added ); + DataStore::self()->appendItemsTags( PimItem::List() << currentItem, addedTags, + &tagsAdded, true, col, true ); + } + + if ( !itemTagsGID.removed.isEmpty() ) { + const Tag::List removedTags = HandlerHelper::resolveTagsByGID( itemTagsGID.removed ); + DataStore::self()->removeItemsTags( PimItem::List() << currentItem, removedTags, + &tagsRemoved, true ); + } + + if ( tagsAdded || tagsRemoved ) { + mChangedParts << AKONADI_PARAM_TAGS; + } + } else if ( !itemTagsGID.added.isEmpty() ) { + bool tagsChanged = false; + const Tag::List tags = HandlerHelper::resolveTagsByGID( itemTagsGID.added ); + DataStore::self()->setItemsTags( PimItem::List() << currentItem, tags, + &tagsChanged, true ); + if ( tagsChanged ) { + mChangedParts << AKONADI_PARAM_TAGS; + } + } + + + 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(), m_streamParser, currentItem); + connect( &streamer, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SIGNAL(responseAvailable(Akonadi::Server::Response)) ); + m_streamParser->beginList(); + while ( !m_streamParser->atListEnd() ) { + QByteArray partName; + qint64 partSize; + QByteArray command = m_streamParser->readString(); + bool changed = false; + if ( command.isEmpty() ) { + throw HandlerException( "Syntax error" ); + } + + if ( command.startsWith( '-' ) ) { + command = command.mid( 1 ); + Q_FOREACH ( const Part &part, existingParts ) { + if ( part.partType().name() == QString::fromUtf8( command ) ) { + DataStore::self()->removeItemParts( currentItem, QList() << command ); + mChangedParts << command; + partsSizes.remove( command ); + break; + } + } + break; + } else if ( command.startsWith( '+' ) ) { + command = command.mid( 1 ); + } + + if ( !streamer.stream( command, true, partName, partSize, &changed ) ) { + return failureResponse( streamer.error() ); + } + if ( changed ) { + mChangedParts << partName; + partsSizes.insert( partName, partSize ); + } + } + + qint64 size = std::accumulate( partsSizes.begin(), partsSizes.end(), 0); + currentItem.setSize( size ); + + // Store all changes + if ( !currentItem.update() ) { + return failureResponse( "Failed to store merged item" ); + } + + return true; +} + +bool Merge::notify( const PimItem &item, const Collection &collection ) +{ + if ( !mChangedParts.isEmpty() ) { + DataStore::self()->notificationCollector()->itemChanged( item, mChangedParts, collection ); + } + + return true; +} + +bool Merge::sendResponse( const QByteArray &responseStr, const PimItem &item ) +{ + ImapSet set; + set.add( QVector() << item.id() ); + Scope scope( Scope::Uid ); + scope.setUidSet( set ); + + FetchScope fetchScope; + // FetchHelper requires collection context + fetchScope.setAllAttributes( true ); + fetchScope.setFullPayload( true ); + fetchScope.setAncestorDepth( 1 ); + fetchScope.setCacheOnly( true ); + fetchScope.setExternalPayloadSupported( true ); + fetchScope.setFlagsRequested( true ); + fetchScope.setGidRequested( true ); + fetchScope.setMTimeRequested( true ); + fetchScope.setRemoteIdRequested( true ); + fetchScope.setRemoteRevisionRequested( true ); + fetchScope.setSizeRequested( true ); + fetchScope.setTagsRequested( true ); + + FetchHelper fetch( connection(), scope, fetchScope ); + connect( &fetch, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SIGNAL(responseAvailable(Akonadi::Server::Response)) ); + if ( !fetch.fetchItems( AKONADI_CMD_ITEMFETCH ) ) { + return failureResponse( "Failed to retrieve merged item" ); + } + + Response response; + response.setTag( tag() ); + response.setSuccess(); + response.setString( responseStr ); + Q_EMIT responseAvailable( response ); + return true; +} + +bool Merge::parseStream() +{ + const QList mergeParts = m_streamParser->readParenthesizedList(); + + DataStore *db = DataStore::self(); + Transaction transaction( db ); + + Collection parentCol; + ChangedAttributes itemFlags, itemTagsRID, itemTagsGID; + PimItem item; + // Parse the rest of the command, assuming X-AKAPPEND syntax + if ( !buildPimItem( item, parentCol, itemFlags, itemTagsRID, itemTagsGID ) ) { + return false; + } + + bool silent = false; + + // Merging is always restricted to the same collection and mimetype + SelectQueryBuilder qb; + qb.addValueCondition( PimItem::collectionIdColumn(), Query::Equals, parentCol.id() ); + qb.addValueCondition( PimItem::mimeTypeIdColumn(), Query::Equals, item.mimeTypeId() ); + Q_FOREACH ( const QByteArray &part, mergeParts ) { + if ( part == AKONADI_PARAM_GID ) { + qb.addValueCondition( PimItem::gidColumn(), Query::Equals, item.gid() ); + } else if ( part == AKONADI_PARAM_REMOTEID ) { + qb.addValueCondition( PimItem::remoteIdColumn(), Query::Equals, item.remoteId() ); + } else if ( part == AKONADI_PARAM_SILENT ) { + silent = true; + } else { + throw HandlerException( "Only merging by RID or GID is allowed" ); + } + } + + if ( !qb.exec() ) { + return failureResponse( "Failed to query database for item" ); + } + + 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( item, parentCol, itemFlags.added, itemTagsRID.added, itemTagsGID.added ) ) { + return false; + } + if ( !transaction.commit() ) { + return failureResponse( "Failed to commit transaction" ); + } + AkAppend::notify( item, parentCol ); + return AkAppend::sendResponse( "Append completed", item ); + + } else if ( result.count() == 1 ) { + // Item with matching GID/RID combination exists, so merge this item into it + // and send itemChanged() + PimItem existingItem = result.first(); + if ( !mergeItem( item, existingItem, itemFlags, itemTagsRID, itemTagsGID ) ) { + return false; + } + if ( !transaction.commit() ) { + return failureResponse( "Failed to commit transaction" ); + } + + notify( existingItem, parentCol ); + if ( silent ) { + return AkAppend::sendResponse( "Merge completed", existingItem ); + } else { + return sendResponse( "Merge completed", existingItem ); + } + + } else { + Q_FOREACH (const PimItem &item, result) { + qDebug() << item.id() << item.remoteId() << 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" ); + } +} + diff --git a/akonadi/server/src/handler/merge.h b/akonadi/server/src/handler/merge.h new file mode 100644 index 00000000..100782f4 --- /dev/null +++ b/akonadi/server/src/handler/merge.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 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) 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 AKONADI_SERVER_MERGE_H +#define AKONADI_SERVER_MERGE_H + +#include "handler/akappend.h" + +namespace Akonadi { +namespace Server { + +class Merge : public AkAppend +{ + Q_OBJECT + +public: + Merge(); + + ~Merge(); + + bool parseStream(); + +protected: + bool mergeItem( PimItem &newItem, PimItem ¤tItem, + const ChangedAttributes &itemFlags, + const ChangedAttributes &itemTagsRID, + const ChangedAttributes &itemTagsGID ); + + bool notify( const PimItem &item, const Collection &collection ); + bool sendResponse( const QByteArray &response, const PimItem &item ); + +private: + QSet mChangedParts; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SERVER_MERGE_H diff --git a/akonadi/server/src/handler/modify.cpp b/akonadi/server/src/handler/modify.cpp new file mode 100644 index 00000000..9671fb94 --- /dev/null +++ b/akonadi/server/src/handler/modify.cpp @@ -0,0 +1,342 @@ +/* + 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 +#include +#include +#include +#include "libs/imapparser_p.h" +#include "imapstreamparser.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Modify::Modify( Scope::SelectionScope scope ) + : m_scope( scope ) +{ +} + +static Tristate getTristateValue( const QByteArray &line, int &pos ) +{ + QByteArray tmp; + pos = ImapParser::parseString( line, tmp, pos ); + if ( tmp == "TRUE" ){ + return Tristate::True; + } else if ( tmp == "FALSE" ){ + return Tristate::False; + } + return Tristate::Undefined; +} + +bool Modify::parseStream() +{ + m_scope.parseScope( m_streamParser ); + SelectQueryBuilder qb; + CollectionQueryHelper::scopeToQuery( m_scope, connection(), qb ); + if ( !qb.exec() ) { + throw HandlerException( "Unable to execute collection query" ); + } + const Collection::List collections = qb.result(); + if ( collections.isEmpty() ) { + throw HandlerException( "No such collection" ); + } + if ( collections.size() > 1 ) { // TODO + throw HandlerException( "Mass-modifying collections is not yet implemented" ); + } + Collection collection = collections.first(); + + //TODO: do it cleanly with the streaming parser, which doesn't have look-ahead at this moment + QByteArray line = m_streamParser->readUntilCommandEnd(); + m_streamParser->insertData( "\n" ); + + CacheCleanerInhibitor inhibitor( false ); + + int p = 0; + if ( ( p = line.indexOf( AKONADI_PARAM_PARENT ) ) > 0 ) { + QByteArray tmp; + ImapParser::parseString( line, tmp, p + 6 ); + const Collection newParent = HandlerHelper::collectionFromIdOrName( tmp ); + 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; + + int pos = 0; + while ( pos < line.length() ) { + QByteArray type; + pos = ImapParser::parseString( line, type, pos ); + if ( type == AKONADI_PARAM_MIMETYPE ) { + QList mimeTypes; + pos = ImapParser::parseParenthesizedList( line, mimeTypes, pos ); + QStringList mts; + Q_FOREACH ( const QByteArray &ba, mimeTypes ) { + mts << QString::fromLatin1( ba ); + } + 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 ) ) { + throw HandlerException( "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 ); + } + } else if ( type == AKONADI_PARAM_CACHEPOLICY ) { + bool changed = false; + pos = HandlerHelper::parseCachePolicy( line, collection, pos, &changed ); + if ( changed ) { + changes.append( AKONADI_PARAM_CACHEPOLICY ); + } + } else if ( type == AKONADI_PARAM_NAME ) { + QString newName; + pos = ImapParser::parseString( line, newName, pos ); + if ( collection.name() == newName ) { + continue; + } + if ( !CollectionQueryHelper::hasAllowedName( collection, newName, collection.parentId() ) ) { + throw HandlerException( "Collection with the same name exists already" ); + } + collection.setName( newName ); + changes.append( AKONADI_PARAM_NAME ); + } else if ( type == AKONADI_PARAM_PARENT ) { + qint64 newParent; + bool ok = false; + pos = ImapParser::parseNumber( line, newParent, &ok, pos ); + if ( !ok ) { + return failureResponse( "Invalid syntax: " + line); + } + if ( collection.parentId() == newParent ) { + continue; + } + if ( !db->moveCollection( collection, Collection::retrieveById( newParent ) ) ) { + return failureResponse( "Unable to reparent collection" ); + } + changes.append( AKONADI_PARAM_PARENT ); + } else if ( type == AKONADI_PARAM_VIRTUAL ) { + QString newValue; + pos = ImapParser::parseString( line, newValue, pos ); + if ( newValue.toInt() != collection.isVirtual() ) { + return failureResponse( "Can't modify VIRTUAL collection flag" ); + } + } else if ( type == AKONADI_PARAM_REMOTEID ) { + QString rid; + pos = ImapParser::parseString( line, rid, pos ); + if ( rid == collection.remoteId() ) { + continue; + } + if ( !connection()->isOwnerResource( collection ) ) { + throw HandlerException( "Only resources can modify remote identifiers" ); + } + collection.setRemoteId( rid ); + changes.append( AKONADI_PARAM_REMOTEID ); + } else if ( type == AKONADI_PARAM_REMOTEREVISION ) { + QString remoteRevision; + pos = ImapParser::parseString( line, remoteRevision, pos ); + if ( remoteRevision == collection.remoteRevision() ) { + continue; + } + collection.setRemoteRevision( remoteRevision ); + changes.append( AKONADI_PARAM_REMOTEREVISION ); + } else if ( type == AKONADI_PARAM_PERSISTENTSEARCH ) { + QByteArray tmp; + QList queryArgs; + pos = ImapParser::parseString( line, tmp, pos ); + ImapParser::parseParenthesizedList( tmp, queryArgs ); + QString queryString, queryCollections, queryAttributes; + QStringList attrs; + for ( int i = 0; i < queryArgs.size(); ++i ) { + const QByteArray key = queryArgs.at( i ); + if ( key == AKONADI_PARAM_PERSISTENTSEARCH_QUERYSTRING ) { + queryString = QString::fromUtf8( queryArgs.at( i + 1 ) ); + ++i; + } else if ( key == AKONADI_PARAM_PERSISTENTSEARCH_QUERYCOLLECTIONS ) { + QList cols; + ImapParser::parseParenthesizedList( queryArgs.at( i + 1), cols ); + queryCollections = QString::fromLatin1( ImapParser::join( cols, " " ) ); + ++i; + } else if ( key == AKONADI_PARAM_PERSISTENTSEARCH_QUERYLANG ) { + // Ignore query lang + ++i; + } else if ( key == AKONADI_PARAM_REMOTE ) { + attrs << QString::fromLatin1( key ); + } else if ( key == AKONADI_PARAM_RECURSIVE ) { + attrs << QString::fromLatin1( key ); + } + } + + queryAttributes = attrs.join( QLatin1String( " " ) ); + + qDebug() << collection.queryAttributes() << queryAttributes; + qDebug() << collection.queryCollections() << queryCollections; + qDebug() << collection.queryString() << queryString; + + if ( collection.queryAttributes() != queryAttributes + || collection.queryCollections() != queryCollections + || collection.queryString() != queryString + || changes.contains( AKONADI_PARAM_MIMETYPE ) ) { + collection.setQueryString( queryString ); + collection.setQueryCollections( queryCollections ); + collection.setQueryAttributes( queryAttributes ); + + SearchManager::instance()->updateSearch( collection ); + + changes.append( AKONADI_PARAM_PERSISTENTSEARCH ); + } + } else if ( type == AKONADI_PARAM_ENABLED ) { + //Not actually a tristate + const bool enabled = ( getTristateValue( line, pos ) == Tristate::True ); + if ( enabled != collection.enabled() ) { + collection.setEnabled( enabled ); + changes.append( AKONADI_PARAM_ENABLED ); + } + } else if ( type == AKONADI_PARAM_SYNC ) { + const Tristate tristate = getTristateValue( line, pos ); + if ( tristate != collection.syncPref() ) { + collection.setSyncPref ( tristate ); + changes.append( AKONADI_PARAM_SYNC ); + } + } else if ( type == AKONADI_PARAM_DISPLAY ) { + const Tristate tristate = getTristateValue( line, pos ); + if ( tristate != collection.displayPref() ) { + collection.setDisplayPref ( tristate ); + changes.append( AKONADI_PARAM_DISPLAY ); + } + } else if ( type == AKONADI_PARAM_INDEX ) { + const Tristate tristate = getTristateValue( line, pos ); + if ( tristate != collection.indexPref() ) { + collection.setIndexPref ( tristate ); + changes.append( AKONADI_PARAM_INDEX ); + } + } else if ( type == AKONADI_PARAM_REFERENCED ) { + //Not actually a tristate + const bool reference = ( getTristateValue( line, pos ) == Tristate::True ); + connection()->collectionReferenceManager()->referenceCollection(connection()->sessionId(), collection, reference); + const bool referenced = connection()->collectionReferenceManager()->isReferenced(collection.id()); + if ( referenced != collection.referenced() ) { + collection.setReferenced( referenced ); + if ( AkonadiServer::instance()->intervalChecker() && referenced ) { + AkonadiServer::instance()->intervalChecker()->requestCollectionSync( collection ); + } + changes.append( AKONADI_PARAM_REFERENCED ); + } + } else if ( type.isEmpty() ) { + break; // input end + } else { + // custom attribute + if ( type.startsWith( '-' ) ) { + type = type.mid( 1 ); + if ( db->removeCollectionAttribute( collection, type ) ) { + changes.append( type ); + } + } else { + QByteArray value; + pos = ImapParser::parseString( line, value, pos ); + + SelectQueryBuilder qb; + qb.addValueCondition( CollectionAttribute::collectionIdColumn(), Query::Equals, collection.id() ); + qb.addValueCondition( CollectionAttribute::typeColumn(), Query::Equals, type ); + if ( !qb.exec() ) { + throw HandlerException( "Unable to retrieve collection attribute" ); + } + + const CollectionAttribute::List attrs = qb.result(); + if ( attrs.isEmpty() ) { + CollectionAttribute attr; + attr.setCollectionId( collection.id() ); + attr.setType( type ); + attr.setValue( value ); + if ( !attr.insert() ) { + throw HandlerException( "Unable to add collection attribute" ); + } + changes.append( type ); + } else if ( attrs.size() == 1 ) { + CollectionAttribute attr = attrs.first(); + if ( attr.value() == value ) { + continue; + } + attr.setValue( value ); + if ( !attr.update() ) { + throw HandlerException( "Unable to update collection attribute" ); + } + changes.append( type ); + } else { + throw HandlerException( "WTF: more than one attribute with the same name" ); + } + } + } + } + + if ( !changes.isEmpty() ) { + if ( collection.hasPendingChanges() && !collection.update() ) { + return failureResponse( "Unable to update 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" ); + } + } + + Response response; + response.setTag( tag() ); + response.setString( "MODIFY done" ); + Q_EMIT responseAvailable( response ); + return true; +} diff --git a/akonadi/server/src/handler/modify.h b/akonadi/server/src/handler/modify.h new file mode 100644 index 00000000..b91ec977 --- /dev/null +++ b/akonadi/server/src/handler/modify.h @@ -0,0 +1,64 @@ +/* + 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 +#include + +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. + +

Syntax

+ + Request: + @verbatim + request = tag " [ "RID " ] MODIFY " collection-ids " " attribute-list + attribute-list = *([-]attribute-name [" " attribute-value]) + attribute-name = "NAME" | "MIMETYPE" | "REMOTEID" | "REMOTEREVISION" | "CACHEPOLICY" | "PARENT" | "ENABLED" | "SYNC" | "DISPLAY" | "INDEX" | "REFERENCED" | [-]custom-attr-name + @endverbatim + + @c collection-ids is either a UID set or a RID list, depending on the command prefix. + + Attributes marked with a leading '-' will be deleted, they don't have any attribute value. +*/ +class Modify : public Handler +{ + Q_OBJECT + public: + Modify( Scope::SelectionScope scope ); + bool parseStream(); + + private: + Scope m_scope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/move.cpp b/akonadi/server/src/handler/move.cpp new file mode 100644 index 00000000..0a6c3bf3 --- /dev/null +++ b/akonadi/server/src/handler/move.cpp @@ -0,0 +1,148 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi::Server; + +Move::Move( Scope::SelectionScope scope ) + : mScope( scope ) +{ +} + +bool Move::parseStream() +{ + mScope.parseScope( m_streamParser ); + + Scope destScope( mScope.scope() ); + destScope.parseScope( m_streamParser ); + const Collection destination = CollectionQueryHelper::singleCollectionFromScope( destScope, connection() ); + const Resource destResource = destination.resource(); + + if ( destination.isVirtual() ) { + return failureResponse( "Moving items into virtual collection is not allowed" ); + } + + Collection source; + if ( !m_streamParser->atCommandEnd() ) { + Scope sourceScope( mScope.scope() ); + sourceScope.parseScope( m_streamParser ); + source = CollectionQueryHelper::singleCollectionFromScope( sourceScope, connection() ); + } + + if ( mScope.scope() == Scope::Rid && !source.isValid() ) { + throw HandlerException( "RID move requires valid source collection" ); + } + connection()->context()->setCollection( source ); + + CacheCleanerInhibitor inhibitor; + + // make sure all the items we want to move are in the cache + ItemRetriever retriever( connection() ); + retriever.setScope( mScope ); + retriever.setRetrieveFullPayload( true ); + if ( !retriever.exec() ) { + return failureResponse( retriever.lastError() ); + } + + DataStore *store = connection()->storageBackend(); + Transaction transaction( store ); + + SelectQueryBuilder qb; + ItemQueryHelper::scopeToQuery( mScope, 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() ) { + throw HandlerException( "No items found" ); + } + + // Split the list by source collection + QMap toMove; + QMap sources; + Q_FOREACH ( /*sic!*/ PimItem item, items ) { + const Collection source = items.first().collection(); + if ( !source.isValid() ) { + throw HandlerException( "Item without collection found!?" ); + } + if ( !sources.contains( source.id() ) ) { + sources.insert( source.id(), source ); + } + + if ( !item.isValid() ) { + throw HandlerException( "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() != destResource.id() ) { + item.setDirty( true ); + } + + toMove.insertMulti( source.id(), item ); + } + + // Emit notification for each source collection separately + Q_FOREACH ( const Entity::Id &sourceId, toMove.uniqueKeys() ) { + const PimItem::List &itemsToMove = toMove.values( sourceId ).toVector(); + const Collection &source = sources.value( sourceId ); + store->notificationCollector()->itemsMoved( itemsToMove, source, destination ); + + Q_FOREACH ( PimItem moved, toMove.values( sourceId ) ) { + // 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 = moved.collection().resource().id() != source.resource().id(); + if ( isInterResourceMove ) { + moved.setRemoteId( QString() ); + } + + // FIXME Could we aggregate the changes to a single SQL query? + if ( !moved.update() ) { + throw HandlerException( "Unable to update item" ); + } + } + } + } else { + throw HandlerException( "Unable to execute query" ); + } + + if ( !transaction.commit() ) { + return failureResponse( "Unable to commit transaction." ); + } + + return successResponse( "MOVE complete" ); +} diff --git a/akonadi/server/src/handler/move.h b/akonadi/server/src/handler/move.h new file mode 100644 index 00000000..b9dd070d --- /dev/null +++ b/akonadi/server/src/handler/move.h @@ -0,0 +1,64 @@ +/* + 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" +#include "scope.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the item move command. + +

Syntax

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

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: + Move( Scope::SelectionScope scope ); + bool parseStream(); + + private: + Scope mScope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/remove.cpp b/akonadi/server/src/handler/remove.cpp new file mode 100644 index 00000000..090531f9 --- /dev/null +++ b/akonadi/server/src/handler/remove.cpp @@ -0,0 +1,70 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Remove::Remove( Scope::SelectionScope scope ) + : mScope( scope ) +{ +} + +bool Remove::parseStream() +{ + mScope.parseScope( m_streamParser ); + connection()->context()->parseContext( m_streamParser ); + qDebug() << "Tag context:" << connection()->context()->tagId(); + qDebug() << "Collection context: " << connection()->context()->collectionId(); + + + SelectQueryBuilder qb; + ItemQueryHelper::scopeToQuery( mScope, connection()->context(), qb ); + + DataStore *store = connection()->storageBackend(); + Transaction transaction( store ); + + if ( qb.exec() ) { + const QVector items = qb.result(); + if ( items.isEmpty() ) { + throw HandlerException( "No items found" ); + } + if ( !store->cleanupPimItems( items ) ) { + throw HandlerException( "Deletion failed" ); + } + } else { + throw HandlerException( "Unable to execute query" ); + } + + if ( !transaction.commit() ) { + return failureResponse( "Unable to commit transaction." ); + } + + return successResponse( "REMOVE complete" ); +} diff --git a/akonadi/server/src/handler/remove.h b/akonadi/server/src/handler/remove.h new file mode 100644 index 00000000..823c2430 --- /dev/null +++ b/akonadi/server/src/handler/remove.h @@ -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. +*/ + +#ifndef AKONADI_REMOVE_H +#define AKONADI_REMOVE_H + +#include "handler.h" +#include "scope.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: + Remove( Scope::SelectionScope scope ); + bool parseStream(); + + private: + Scope mScope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/resourceselect.cpp b/akonadi/server/src/handler/resourceselect.cpp new file mode 100644 index 00000000..d4515c91 --- /dev/null +++ b/akonadi/server/src/handler/resourceselect.cpp @@ -0,0 +1,49 @@ +/* + 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 +#include +#include + +using namespace Akonadi::Server; + +ResourceSelect::ResourceSelect() + : Handler() +{ +} + +bool ResourceSelect::parseStream() +{ + const QString resourceName = m_streamParser->readUtf8String(); + if ( resourceName.isEmpty() ) { + connection()->context()->setResource( Resource() ); + return successResponse( "Resource deselected" ); + } + + const Resource res = Resource::retrieveByName( resourceName ); + if ( !res.isValid() ) { + throw HandlerException( resourceName.toUtf8() + " is not a valid resource identifier" ); + } + + connection()->context()->setResource( res ); + + return successResponse( resourceName.toUtf8() + " selected" ); +} diff --git a/akonadi/server/src/handler/resourceselect.h b/akonadi/server/src/handler/resourceselect.h new file mode 100644 index 00000000..14a08cd0 --- /dev/null +++ b/akonadi/server/src/handler/resourceselect.h @@ -0,0 +1,54 @@ +/* + 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 + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the resource selection command. + +

Syntax

+ @verbatim + RESSELECT + @endverbatim + +

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: + ResourceSelect(); + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/scope.cpp b/akonadi/server/src/handler/scope.cpp new file mode 100644 index 00000000..669b11cc --- /dev/null +++ b/akonadi/server/src/handler/scope.cpp @@ -0,0 +1,133 @@ +/* + 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 "scope.h" +#include "imapstreamparser.h" +#include "handler.h" +#include "libs/protocol_p.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Scope::Scope() + : mScope( Scope::Invalid ) +{ +} + +Scope::Scope( SelectionScope scope ) + : mScope( scope ) +{ +} + +void Scope::parseScope( ImapStreamParser *parser ) +{ + if ( mScope == None || mScope == Uid ) { + mUidSet = parser->readSequenceSet(); + if ( mUidSet.isEmpty() ) { + throw HandlerException( "Empty uid set specified" ); + } + } else if ( mScope == Rid ) { + if ( parser->hasList() ) { + parser->beginList(); + while ( !parser->atListEnd() ) { + mRidSet << parser->readUtf8String(); + } + } else { + mRidSet << parser->readUtf8String(); + } + if ( mRidSet.isEmpty() ) { + throw HandlerException( "Empty remote identifier set specified" ); + } + } else if ( mScope == HierarchicalRid ) { + parser->beginList(); + while ( !parser->atListEnd() ) { + parser->beginList(); + parser->readString(); // uid, invalid here + mRidChain.append( parser->readUtf8String() ); + if ( !parser->atListEnd() ) { + throw HandlerException( "Invalid hierarchical RID chain format" ); + } + } + } else if ( mScope == Gid ) { + if ( parser->hasList() ) { + parser->beginList(); + while ( !parser->atListEnd() ) { + mGidSet << parser->readUtf8String(); + } + } else { + mGidSet << parser->readUtf8String(); + } + if ( mGidSet.isEmpty() ) { + throw HandlerException( "Empty gid set specified" ); + } + } else { + throw HandlerException( "WTF?!?" ); + } +} + +Scope::SelectionScope Scope::selectionScopeFromByteArray( const QByteArray &input ) +{ + if ( input == AKONADI_CMD_UID ) { + return Scope::Uid; + } else if ( input == AKONADI_CMD_RID ) { + return Scope::Rid; + } else if ( input == AKONADI_CMD_HRID ) { + return Scope::HierarchicalRid; + } else if ( input == AKONADI_CMD_GID ) { + return Scope::Gid; + } + return Scope::None; +} + +Scope::SelectionScope Scope::scope() const +{ + return mScope; +} + +void Scope::setScope( SelectionScope scope ) +{ + mScope = scope; +} + +ImapSet Scope::uidSet() const +{ + return mUidSet; +} + +void Scope::setUidSet( const ImapSet &uidSet ) +{ + mUidSet = uidSet; +} + +QStringList Scope::ridSet() const +{ + return mRidSet; +} + +QStringList Scope::ridChain() const +{ + return mRidChain; +} + +QStringList Scope::gidSet() const +{ + return mGidSet; +} diff --git a/akonadi/server/src/handler/scope.h b/akonadi/server/src/handler/scope.h new file mode 100644 index 00000000..3887e668 --- /dev/null +++ b/akonadi/server/src/handler/scope.h @@ -0,0 +1,85 @@ +/* + 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_SCOPE_H +#define AKONADI_SCOPE_H + +#include "libs/imapset_p.h" +#include + +namespace Akonadi { +namespace Server { + +class ImapStreamParser; + +/** + Represents a set of Akonadi objects (eg. items or collections) selected for an operations. +*/ +class Scope +{ + public: + enum SelectionScope { + Invalid, + None, + Uid, + Rid, + HierarchicalRid, + Gid + }; + + /** + * Constructs an invalid scope. + * + * This exists only so that we can register Scope as a metatype + */ + Scope(); + + Scope( SelectionScope scope ); + /** + Parse the object set dependent on the set selection scope. + The set has to be non-empty. If not a HandlerException is thrown. + */ + void parseScope( ImapStreamParser *parser ); + + /** + Parse the selection scope identifier (UID, RID, HRID, etc.). + Returns None if @p input is not a selection scope. + */ + static SelectionScope selectionScopeFromByteArray( const QByteArray &input ); + + SelectionScope scope() const; + void setScope( SelectionScope scope ); + ImapSet uidSet() const; + void setUidSet( const ImapSet &uidSet ); + QStringList ridSet() const; + QStringList ridChain() const; + QStringList gidSet() const; + + private: + SelectionScope mScope; + ImapSet mUidSet; + QStringList mRidSet; + QStringList mRidChain; + QStringList mGidSet; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/search.cpp b/akonadi/server/src/handler/search.cpp new file mode 100644 index 00000000..06d172f0 --- /dev/null +++ b/akonadi/server/src/handler/search.cpp @@ -0,0 +1,187 @@ +/*************************************************************************** + * 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 "akonadi.h" +#include "config-akonadi.h" +#include "connection.h" +#include "fetchhelper.h" +#include "handlerhelper.h" +#include "searchhelper.h" +#include "imapstreamparser.h" +#include "nepomuksearch.h" +#include "response.h" +#include "search/searchrequest.h" +#include "search/searchmanager.h" + +#include "libs/protocol_p.h" + +#include + +using namespace Akonadi::Server; + +Search::Search() + : Handler() +{ +} + +Search::~Search() +{ +} + +bool Search::parseStream() +{ + QStringList mimeTypes; + QVector collectionIds; + bool recursive = false, remote = false; + QString queryString; + + // Backward compatibility + if ( !connection()->capabilities().serverSideSearch() ) { + searchNepomuk(); + } else { + while (m_streamParser->hasString()) { + const QByteArray param = m_streamParser->readString(); + if ( param == AKONADI_PARAM_MIMETYPE ) { + const QList mt = m_streamParser->readParenthesizedList(); + mimeTypes.reserve( mt.size() ); + Q_FOREACH ( const QByteArray &ba, mt ) { + mimeTypes.append( QString::fromLatin1( ba ) ); + } + } else if ( param == AKONADI_PARAM_COLLECTIONS ) { + QList list = m_streamParser->readParenthesizedList(); + Q_FOREACH ( const QByteArray &col, list ) { + collectionIds << col.toLongLong(); + } + } else if ( param == AKONADI_PARAM_RECURSIVE ) { + recursive = true; + } else if ( param == AKONADI_PARAM_REMOTE ) { + remote = true; + } else if ( param == AKONADI_PARAM_QUERY ) { + queryString = m_streamParser->readUtf8String(); + // TODO: This is an ugly hack, but we assume QUERY is the last parameter, + // followed only by fetch scope, which we parse separately below + break; + } else { + return failureResponse( "Invalid parameter" ); + } + } + + if ( queryString.isEmpty() ) { + return failureResponse( "No query specified" ); + } + + QVector collections; + if ( collectionIds.isEmpty() ) { + collectionIds << 0; + recursive = true; + } + + if ( recursive ) { + Q_FOREACH ( qint64 collection, collectionIds ) { + collections << SearchHelper::listCollectionsRecursive( QVector() << collection, mimeTypes ); + } + } else { + collections = collectionIds; + } + + akDebug() << "SEARCH:"; + akDebug() << "\tQuery:" << queryString; + akDebug() << "\tMimeTypes:" << mimeTypes; + akDebug() << "\tCollections:" << collections; + akDebug() << "\tRemote:" << remote; + akDebug() << "\tRecursive" << recursive; + + if ( collections.isEmpty() ) { + m_streamParser->readUntilCommandEnd(); + return successResponse( "Search done" ); + } + + // Read the fetch scope + mFetchScope = FetchScope( m_streamParser ); + // Read any newlines + m_streamParser->readUntilCommandEnd(); + + SearchRequest request( connection()->sessionId() ); + request.setCollections( collections ); + request.setMimeTypes( mimeTypes ); + request.setQuery( queryString ); + request.setRemoteSearch( remote ); + connect( &request, SIGNAL(resultsAvailable(QSet)), + this, SLOT(slotResultsAvailable(QSet)) ); + request.exec(); + + } + + //akDebug() << "\tResult:" << uids; + akDebug() << "\tResult:" << mAllResults.count() << "matches"; + + return successResponse( "Search done" ); +} + +void Search::searchNepomuk() +{ + const QString queryString = m_streamParser->readUtf8String(); + mFetchScope = FetchScope( m_streamParser ); + + QStringList uids; +#ifdef HAVE_SOPRANO + NepomukSearch *service = new NepomukSearch; + uids = service->search( queryString ); + delete service; +#else + akError() << "Akonadi has been built without Nepomuk support!"; + return; +#endif + + if ( uids.isEmpty() ) { + return; + } + + QSet results; + Q_FOREACH ( const QString &uid, uids ) { + results.insert( uid.toLongLong() ); + } + + slotResultsAvailable( results ); +} + +void Search::slotResultsAvailable( const QSet &results ) +{ + QSet newResults = results; + newResults.subtract( mAllResults ); + mAllResults.unite( newResults ); + + if ( newResults.isEmpty() ) { + return; + } + + // create imap query + ImapSet itemSet; + itemSet.add( newResults ); + Scope scope( Scope::Uid ); + scope.setUidSet( itemSet ); + + FetchHelper fetchHelper( connection(), scope, mFetchScope ); + connect( &fetchHelper, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SIGNAL(responseAvailable(Akonadi::Server::Response)) ); + + fetchHelper.fetchItems( AKONADI_CMD_SEARCH ); +} diff --git a/akonadi/server/src/handler/search.h b/akonadi/server/src/handler/search.h new file mode 100644 index 00000000..6d460774 --- /dev/null +++ b/akonadi/server/src/handler/search.h @@ -0,0 +1,66 @@ +/*************************************************************************** + * 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 +#include "fetchscope.h" + +#include +#include + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the search commands. + + A search has the following forms: + @verbatim + SEARCH [MIMETYPE (mimetype-list)] [COLLECTIONS (collections-list) [RECURSIVE]] QUERY + @endverbatim +*/ +class Search : public Handler +{ + Q_OBJECT + + public: + Search(); + + ~Search(); + + bool parseStream(); + + private Q_SLOTS: + void slotResultsAvailable( const QSet &results ); + + private: + void searchNepomuk(); + + FetchScope mFetchScope; + QSet mAllResults; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/searchhelper.cpp b/akonadi/server/src/handler/searchhelper.cpp new file mode 100644 index 00000000..aa6694d5 --- /dev/null +++ b/akonadi/server/src/handler/searchhelper.cpp @@ -0,0 +1,143 @@ +/*************************************************************************** + * 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 "storage/countquerybuilder.h" +#include "entities.h" + +#include + +using namespace Akonadi::Server; + +QList SearchHelper::splitLine( const QByteArray &line ) +{ + QList retval; + + int i, start = 0; + bool escaped = false; + for ( i = 0; i < line.count(); ++i ) { + if ( line[i] == ' ' ) { + if ( !escaped ) { + retval.append( line.mid( start, i - start ) ); + start = i + 1; + } + } else if ( line[i] == '"' ) { + if ( escaped ) { + escaped = false; + } else { + escaped = true; + } + } + } + + retval.append( line.mid( start, i - start ) ); + + return retval; +} + +QString SearchHelper::extractMimetype( const QList &junks, int start ) +{ + QString mimeType; + + if ( junks.count() <= start ) { + return QString(); + } + + if ( junks[start].toUpper() == AKONADI_PARAM_CHARSET ) { + if ( junks.count() <= ( start + 2 ) ) { + return QString(); + } + if ( junks[start + 2].toUpper() == AKONADI_PARAM_MIMETYPE ) { + if ( junks.count() <= ( start + 3 ) ) { + return QString(); + } else { + mimeType = QString::fromLatin1( junks[start + 3].toLower() ); + } + } + } else { + if ( junks[start].toUpper() == AKONADI_PARAM_MIMETYPE ) { + if ( junks.count() <= ( start + 1 ) ) { + return QString(); + } else { + mimeType = QString::fromLatin1( junks[start + 1].toLower() ); + } + } + } + + if ( mimeType.isEmpty() ) { + mimeType = QString::fromLatin1( "message/rfc822" ); + } + + return mimeType; +} + + +QVector SearchHelper::listCollectionsRecursive( const QVector &ancestors, const QStringList &mimeTypes ) +{ + QVector recursiveChildren; + Q_FOREACH ( qint64 ancestor, ancestors ) { + QVector searchChildren; + + { // Free the query before entering recursion to prevent too many opened connections + + Query::Condition mimeTypeCondition; + mimeTypeCondition.addColumnCondition( CollectionMimeTypeRelation::rightFullColumnName(), Query::Equals, MimeType::idFullColumnName() ); + // Exclude top-level collections and collections that cannot have items! + mimeTypeCondition.addValueCondition( MimeType::nameFullColumnName(), Query::NotEquals, QLatin1String( "inode/directory" ) ); + if ( !mimeTypes.isEmpty() ) { + mimeTypeCondition.addValueCondition( MimeType::nameFullColumnName(), Query::In, mimeTypes ); + } + + CountQueryBuilder qb( Collection::tableName(), MimeType::nameFullColumnName(), CountQueryBuilder::All ); + qb.addColumn( Collection::idFullColumnName() ); + qb.addJoin( QueryBuilder::LeftJoin, CollectionMimeTypeRelation::tableName(), CollectionMimeTypeRelation::leftFullColumnName(), Collection::idFullColumnName() ); + qb.addJoin( QueryBuilder::LeftJoin, MimeType::tableName(), mimeTypeCondition ); + if ( ancestor == 0 ) { + qb.addValueCondition( Collection::parentIdFullColumnName(), Query::Is, QVariant() ); + } else { + // Also include current ancestor's result, so that we know whether we should search in the ancestor too + Query::Condition idCond( Query::Or ); + idCond.addValueCondition( Collection::parentIdFullColumnName(), Query::Equals, ancestor ); + idCond.addValueCondition( Collection::idFullColumnName(), Query::Equals, ancestor ); + qb.addCondition( idCond ); + } + qb.addValueCondition( Collection::isVirtualFullColumnName(), Query::Equals, false ); + qb.addGroupColumn( Collection::idFullColumnName() ); + qb.exec(); + + QSqlQuery query = qb.query(); + while ( query.next() ) { + const qint64 id = query.value( 1 ).toLongLong(); + // Don't add ancestor into search children, we are resolving it right now + if ( id != ancestor ) { + searchChildren << id; + } + if ( query.value( 0 ).toInt() > 0 ) { // count( mimeTypeTable.name ) > 0 + recursiveChildren << id; + } + } + } + if ( !searchChildren.isEmpty() ) { + recursiveChildren << listCollectionsRecursive( searchChildren, mimeTypes ); + } + } + + return recursiveChildren; +} diff --git a/akonadi/server/src/handler/searchhelper.h b/akonadi/server/src/handler/searchhelper.h new file mode 100644 index 00000000..a64bb61c --- /dev/null +++ b/akonadi/server/src/handler/searchhelper.h @@ -0,0 +1,42 @@ +/*************************************************************************** + * 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 +#include +#include + +namespace Akonadi { +namespace Server { + +class SearchHelper +{ + public: + static QList splitLine( const QByteArray &line ); + static QString extractMimetype( const QList &junks, int start ); + static QVector listCollectionsRecursive( const QVector &ancestors, const QStringList &mimeTypes ); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/searchpersistent.cpp b/akonadi/server/src/handler/searchpersistent.cpp new file mode 100644 index 00000000..d4c20510 --- /dev/null +++ b/akonadi/server/src/handler/searchpersistent.cpp @@ -0,0 +1,124 @@ +/*************************************************************************** + * 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 "akonadi.h" +#include "connection.h" +#include "response.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/transaction.h" +#include "handlerhelper.h" +#include "search/searchmanager.h" +#include "imapstreamparser.h" +#include "libs/protocol_p.h" +#include "libs/imapparser_p.h" +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +SearchPersistent::SearchPersistent() + : Handler() +{ +} + +SearchPersistent::~SearchPersistent() +{ +} + +bool SearchPersistent::parseStream() +{ + QString collectionName = m_streamParser->readUtf8String(); + if ( collectionName.isEmpty() ) { + return failureResponse( "No name specified" ); + } + + DataStore *db = connection()->storageBackend(); + Transaction transaction( db ); + + const QString queryString = m_streamParser->readUtf8String(); + if ( queryString.isEmpty() ) { + return failureResponse( "No query specified" ); + } + + // for legacy clients we have to guess the language + QString lang = QLatin1String( "SPARQL" ); + + QList mimeTypes; + QString queryCollections; + QStringList queryAttributes; + if ( m_streamParser->hasList() ) { + m_streamParser->beginList(); + while ( !m_streamParser->atListEnd() ) { + const QByteArray key = m_streamParser->readString(); + if ( key == AKONADI_PARAM_MIMETYPE ) { + mimeTypes = m_streamParser->readParenthesizedList(); + } else if ( key == AKONADI_PARAM_PERSISTENTSEARCH_QUERYCOLLECTIONS ) { + const QList collections = m_streamParser->readParenthesizedList(); + queryCollections = QString::fromLatin1( ImapParser::join( collections, " " ) ); + } else if ( key == AKONADI_PARAM_PERSISTENTSEARCH_QUERYLANG ) { + queryAttributes << QLatin1String( AKONADI_PARAM_PERSISTENTSEARCH_QUERYLANG) + << QString::fromUtf8( m_streamParser->readString() ); + } else if ( key == AKONADI_PARAM_REMOTE ) { + queryAttributes << QLatin1String( AKONADI_PARAM_REMOTE ); + } else if ( key == AKONADI_PARAM_RECURSIVE ) { + queryAttributes << QLatin1String( AKONADI_PARAM_RECURSIVE ); + } + } + } + + Collection col; + col.setQueryString( queryString ); + col.setQueryAttributes( queryAttributes.join( QLatin1String( " " ) ) ); + col.setQueryCollections( queryCollections ); + col.setParentId( 1 ); // search root + col.setResourceId( 1 ); // search resource + col.setName( collectionName ); + 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 QByteArray &mimeType, mimeTypes ) { + col.addMimeType( MimeType::retrieveByName( QString::fromLatin1( mimeType ) ) ); + } + + if ( !transaction.commit() ) { + return failureResponse( "Unable to commit transaction" ); + } + + SearchManager::instance()->updateSearch( col ); + + const QByteArray b = HandlerHelper::collectionToByteArray( col ); + + Response colResponse; + colResponse.setUntagged(); + colResponse.setString( b ); + Q_EMIT responseAvailable( colResponse ); + + return successResponse( "SEARCH_STORE completed" ); +} diff --git a/akonadi/server/src/handler/searchpersistent.h b/akonadi/server/src/handler/searchpersistent.h new file mode 100644 index 00000000..a1e6a52d --- /dev/null +++ b/akonadi/server/src/handler/searchpersistent.h @@ -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. * + ***************************************************************************/ + +#ifndef AKONADISEARCHPERSISTENT_H +#define AKONADISEARCHPERSISTENT_H + +#include + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the search_store search_delete commands. + + A persistent search can have the following forms: + @verbatim + 123 SEARCH_STORE [ '(' ( )* ')' ] + @endverbatim +*/ +class SearchPersistent : public Handler +{ + Q_OBJECT + public: + SearchPersistent(); + + ~SearchPersistent(); + + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/searchresult.cpp b/akonadi/server/src/handler/searchresult.cpp new file mode 100644 index 00000000..819e3cdc --- /dev/null +++ b/akonadi/server/src/handler/searchresult.cpp @@ -0,0 +1,103 @@ +/* + * Copyright 2013 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 "searchresult.h" +#include "connection.h" +#include "imapstreamparser.h" +#include "protocol_p.h" +#include "storage/selectquerybuilder.h" +#include "storage/itemqueryhelper.h" +#include "search/searchtaskmanager.h" +#include "akdebug.h" + +using namespace Akonadi::Server; + +SearchResult::SearchResult( Scope::SelectionScope scope ) + : Handler() + , mScope( scope ) +{ +} + +SearchResult::~SearchResult() +{ +} + + +bool SearchResult::parseStream() +{ + const QByteArray searchId = m_streamParser->readString(); + const qint64 collectionId = m_streamParser->readNumber(); + if ( mScope.scope() != Scope::Uid && mScope.scope() != Scope::Rid ) { + fail( searchId, "Only UID or RID scopes are allowed in SEARECH_RESULT" ); + return false; + } + + if ( mScope.scope() == Scope::Uid ) { + // Handle empty UID set + m_streamParser->beginList(); + if ( !m_streamParser->atListEnd() ) { + mScope.parseScope( m_streamParser ); + // FIXME: A hack to move stream parser beyond ')' + m_streamParser->readChar(); + } + } else { + mScope.parseScope( m_streamParser ); + } + + QSet ids; + if ( mScope.scope() == Scope::Rid && !mScope.ridSet().isEmpty() ) { + QueryBuilder qb( PimItem::tableName() ); + qb.addColumn( PimItem::idFullColumnName() ); + ItemQueryHelper::remoteIdToQuery( mScope.ridSet(), connection()->context(), qb ); + qb.addValueCondition( PimItem::collectionIdFullColumnName(), Query::Equals, collectionId ); + + if ( !qb.exec() ) { + fail( searchId, "Failed to convert RID to UID" ); + return false; + } + + QSqlQuery query = qb.query(); + while ( query.next() ) { + ids << query.value( 0 ).toLongLong(); + } + } else if ( mScope.scope() == Scope::Uid && !mScope.uidSet().isEmpty() ) { + Q_FOREACH ( const ImapInterval &interval, mScope.uidSet().intervals() ) { + if ( !interval.hasDefinedBegin() && !interval.hasDefinedEnd() ) { + fail( searchId, "Open UID intervals not allowed in SEARCH_RESULT" ); + return false; + } + + for ( int i = interval.begin(); i <= interval.end(); ++i ) { + ids << i; + } + } + } + SearchTaskManager::instance()->pushResults( searchId, ids, connection() ); + + successResponse( "Done" ); + return true; +} + +void SearchResult::fail( const QByteArray &searchId, const char* error ) +{ + SearchTaskManager::instance()->pushResults( searchId, QSet(), connection() ); + failureResponse( error ); +} diff --git a/akonadi/server/src/handler/searchresult.h b/akonadi/server/src/handler/searchresult.h new file mode 100644 index 00000000..f0fb146c --- /dev/null +++ b/akonadi/server/src/handler/searchresult.h @@ -0,0 +1,61 @@ +/* + * Copyright 2013 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 AKONADI_SEARCHRESULT_H +#define AKONADI_SEARCHRESULT_H + +#include + +#include "scope.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the search_result command + + @verbatim + tag " " scope-selector " SEARCH_RESULT " searchId " " result + scope-selector = [ "UID" / "RID" ] + result = [ scope / "DONE" ] + @endverbatim +*/ +class SearchResult : public Handler +{ + Q_OBJECT + public: + SearchResult( Scope::SelectionScope scope ); + ~SearchResult(); + + bool parseStream(); + + private: + void fail( const QByteArray &searchId, const char *error ); + + Scope mScope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SEARCHRESULT_H diff --git a/akonadi/server/src/handler/select.cpp b/akonadi/server/src/handler/select.cpp new file mode 100644 index 00000000..1c5dd8a6 --- /dev/null +++ b/akonadi/server/src/handler/select.cpp @@ -0,0 +1,122 @@ +/*************************************************************************** + * 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 "select.h" + +#include + +#include "akonadi.h" +#include "connection.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "handlerhelper.h" +#include "imapstreamparser.h" +#include "storage/selectquerybuilder.h" +#include "commandcontext.h" + +#include "response.h" + +#include + +using namespace Akonadi::Server; + +Select::Select( Scope::SelectionScope scope ) + : Handler() + , mScope( scope ) +{ +} + +bool Select::parseStream() +{ + // as per rfc, even if the following select fails, we need to reset + connection()->context()->setCollection( Collection() ); + + QByteArray buffer = m_streamParser->readString(); + + bool silent = false; + if ( buffer == AKONADI_PARAM_SILENT ) { + silent = true; + buffer = m_streamParser->readString(); + } + + // collection + Collection col; + + if ( mScope == Scope::None || mScope == Scope::Uid ) { + col = HandlerHelper::collectionFromIdOrName( buffer ); + if ( !col.isValid() ) { + bool ok = false; + if ( buffer.toLongLong( &ok ) == 0 && ok ) { + silent = true; + } else { + return failureResponse( "Cannot select this collection" ); + } + } + } else if ( mScope == Scope::Rid ) { + if ( buffer.isEmpty() ) { + silent = true; // unselect + } else { + if ( !connection()->context()->resource().isValid() ) { + throw HandlerException( "Cannot select based on remote identifier without a resource scope" ); + } + SelectQueryBuilder qb; + qb.addValueCondition( Collection::remoteIdColumn(), Query::Equals, QString::fromUtf8( buffer ) ); + qb.addValueCondition( Collection::resourceIdColumn(), Query::Equals, connection()->context()->resource().id() ); + if ( !qb.exec() ) { + throw HandlerException( "Failed to select collection" ); + } + Collection::List results = qb.result(); + if ( results.count() != 1 ) { + throw HandlerException( QByteArray::number( results.count() ) + " collections found" ); + } + col = results.first(); + } + } + + // Responses: REQUIRED untagged responses: FLAGS, EXISTS, RECENT + // OPTIONAL OK untagged responses: UNSEEN, PERMANENTFLAGS + Response response; + if ( !silent ) { + response.setUntagged(); + response.setString( "FLAGS (" + Flag::joinByName( Flag::retrieveAll(), QLatin1String( " " ) ).toLatin1() + ")" ); + Q_EMIT responseAvailable( response ); + + const int itemCount = HandlerHelper::itemCount( col ); + if ( itemCount < 0 ) { + return failureResponse( "Unable to determine item count" ); + } + response.setString( QByteArray::number( itemCount ) + " EXISTS" ); + Q_EMIT responseAvailable( response ); + + int readCount = HandlerHelper::itemWithFlagsCount( col, QStringList() << QLatin1String( AKONADI_FLAG_SEEN ) + << QLatin1String( AKONADI_FLAG_IGNORED ) ); + if ( readCount < 0 || itemCount < readCount ) { + return failureResponse( "Unable to retrieve unseen count" ); + } + response.setString( "OK [UNSEEN " + QByteArray::number( itemCount - readCount ) + "] Message 0 is first unseen" ); + Q_EMIT responseAvailable( response ); + } + + response.setSuccess(); + response.setTag( tag() ); + response.setString( "Completed" ); + Q_EMIT responseAvailable( response ); + + connection()->context()->setCollection( col ); + return true; +} diff --git a/akonadi/server/src/handler/select.h b/akonadi/server/src/handler/select.h new file mode 100644 index 00000000..e8d69fc1 --- /dev/null +++ b/akonadi/server/src/handler/select.h @@ -0,0 +1,48 @@ +/*************************************************************************** + * 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 AKONADISELECT_H +#define AKONADISELECT_H + +#include +#include "scope.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the select command. + */ +class Select : public Handler +{ + Q_OBJECT + public: + explicit Select( Scope::SelectionScope scope ); + bool parseStream(); + + private: + Scope::SelectionScope mScope; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/status.cpp b/akonadi/server/src/handler/status.cpp new file mode 100644 index 00000000..8c6823da --- /dev/null +++ b/akonadi/server/src/handler/status.cpp @@ -0,0 +1,110 @@ +/*************************************************************************** + * 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 + +#include "akonadi.h" +#include "connection.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/countquerybuilder.h" + +#include "response.h" +#include "handlerhelper.h" +#include "imapstreamparser.h" + +#include + +using namespace Akonadi::Server; + +Status::Status(): Handler() +{ +} + +Status::~Status() +{ +} + +bool Status::parseStream() +{ + // Arguments: mailbox name + // status data item names + + // Syntax: + // status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")" + // status-att = "MESSAGES" / "RECENT" / "UNSEEN" / "SIZE" + + const QByteArray mailbox = m_streamParser->readString(); + QList attributeList = m_streamParser->readParenthesizedList(); + const Collection col = HandlerHelper::collectionFromIdOrName( mailbox ); + + if ( !col.isValid() ) { + return failureResponse( "No status for this folder" ); + } + + // Responses: + // REQUIRED untagged responses: STATUS + + qint64 itemCount, itemSize; + if ( !HandlerHelper::itemStatistics( col, itemCount, itemSize ) ) { + return failureResponse( "Failed to query statistics." ); + } + + // build STATUS response + QByteArray statusResponse; + // MESSAGES - The number of messages in the mailbox + if ( attributeList.contains( AKONADI_ATTRIBUTE_MESSAGES ) ) { + statusResponse += AKONADI_ATTRIBUTE_MESSAGES " "; + statusResponse += QByteArray::number( itemCount ); + } + + if ( attributeList.contains( AKONADI_ATTRIBUTE_UNSEEN ) ) { + if ( !statusResponse.isEmpty() ) { + statusResponse += " "; + } + statusResponse += AKONADI_ATTRIBUTE_UNSEEN " "; + + // itemWithFlagCount is twice as fast as itemWithoutFlagCount... + const int count = HandlerHelper::itemWithFlagsCount( col, QStringList() << QLatin1String( AKONADI_FLAG_SEEN ) + << QLatin1String( AKONADI_FLAG_IGNORED ) ); + if ( count < 0 ) { + return failureResponse( "Unable to retrieve unread count" ); + } + statusResponse += QByteArray::number( itemCount - count ); + } + if ( attributeList.contains( AKONADI_PARAM_SIZE ) ) { + if ( !statusResponse.isEmpty() ) { + statusResponse += " "; + } + statusResponse += AKONADI_PARAM_SIZE " "; + statusResponse += QByteArray::number( itemSize ); + } + + Response response; + response.setUntagged(); + response.setString( "STATUS \"" + HandlerHelper::pathForCollection( col ).toUtf8() + "\" (" + statusResponse + ')' ); + Q_EMIT responseAvailable( response ); + + response.setSuccess(); + response.setTag( tag() ); + response.setString( "STATUS completed" ); + Q_EMIT responseAvailable( response ); + return true; +} diff --git a/akonadi/server/src/handler/status.h b/akonadi/server/src/handler/status.h new file mode 100644 index 00000000..550d587a --- /dev/null +++ b/akonadi/server/src/handler/status.h @@ -0,0 +1,47 @@ +/*************************************************************************** + * 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 + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the STATUS command. + */ +class Status : public Handler +{ + Q_OBJECT +public: + Status(); + + ~Status(); + + bool parseStream(); + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/store.cpp b/akonadi/server/src/handler/store.cpp new file mode 100644 index 00000000..6664a09a --- /dev/null +++ b/akonadi/server/src/handler/store.cpp @@ -0,0 +1,434 @@ +/*************************************************************************** + * 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 + +#include "akonadi.h" +#include "connection.h" +#include "handlerhelper.h" +#include "response.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 "libs/imapparser_p.h" +#include "libs/protocol_p.h" +#include "imapstreamparser.h" + +#include +#include +#include +#include + +#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; +} + +Store::Store( Scope::SelectionScope scope ) + : Handler() + , mScope( scope ) + , mPos( 0 ) + , mPreviousRevision( -1 ) + , mSize( 0 ) + , mCheckRevision( false ) +{ +} + +bool Store::replaceFlags( const PimItem::List &item, const QVector &flags ) +{ + Flag::List flagList = HandlerHelper::resolveFlags( flags ); + DataStore *store = connection()->storageBackend(); + + if ( !store->setItemsFlags( item, flagList ) ) { + throw HandlerException( "Store::replaceFlags: Unable to set new item flags" ); + } + + return true; +} + +bool Store::addFlags( const PimItem::List &items, const QVector &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 QVector &flags ) +{ + DataStore *store = connection()->storageBackend(); + + QVector flagList; + flagList.reserve( flags.size() ); + for ( int i = 0; i < flags.count(); ++i ) { + Flag flag = Flag::retrieveByName( QString::fromUtf8( flags[i] ) ); + if ( !flag.isValid() ) { + continue; + } + + flagList.append( flag ); + } + + if ( !store->removeItemsFlags( items, flagList ) ) { + akDebug() << "Store::deleteFlags: Unable to remove item flags"; + return false; + } + return true; +} + +bool Store::replaceTags( const PimItem::List &item, const ImapSet &tags ) +{ + const Tag::List tagList = HandlerHelper::resolveTags( tags ); + if ( !connection()->storageBackend()->setItemsTags( item, tagList ) ) { + throw HandlerException( "Store::replaceTags: Unable to set new item tags" ); + } + return true; +} + +bool Store::addTags( const PimItem::List &items, const ImapSet &tags, bool &tagsChanged ) +{ + const Tag::List tagList = HandlerHelper::resolveTags( tags ); + 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 ImapSet &tags ) +{ + const Tag::List tagList = HandlerHelper::resolveTags( tags ); + if ( !connection()->storageBackend()->removeItemsTags( items, tagList ) ) { + akDebug() << "Store::deleteTags: Unable to remove item tags"; + return false; + } + return true; +} + +bool Store::parseStream() +{ + parseCommand(); + DataStore *store = connection()->storageBackend(); + Transaction transaction( store ); + // Set the same modification time for each item. + const QDateTime modificationtime = QDateTime::currentDateTime().toUTC(); + + // retrieve selected items + SelectQueryBuilder qb; + ItemQueryHelper::scopeToQuery( mScope, 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 ( mCheckRevision ) { + // 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 = QString::fromLatin1( "[LRCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with dirty payload, aborting STORE." ); + throw HandlerException( 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)mPreviousRevision ) { + throw HandlerException( "[LLCONFLICT] Item was modified elsewhere, aborting STORE." ); + } + } + } + + QSet changes; + qint64 partSizes = 0; + bool invalidateCache = false; + bool undirty = false; + bool silent = false; + bool notify = true; + + // apply modifications + m_streamParser->beginList(); + while ( !m_streamParser->atListEnd() ) { + // parse the command + QByteArray command = m_streamParser->readString(); + if ( command.isEmpty() ) { + throw HandlerException( "Syntax error" ); + } + Operation op = Replace; + if ( command.startsWith( '+' ) ) { + op = Add; + command = command.mid( 1 ); + } else if ( command.startsWith( '-' ) ) { + op = Delete; + command = command.mid( 1 ); + } + if ( command.endsWith( AKONADI_PARAM_DOT_SILENT ) ) { + command.chop( 7 ); + silent = true; + } +// akDebug() << "STORE: handling command: " << command; + + // handle commands that can be applied to more than one item + if ( command == AKONADI_PARAM_FLAGS ) { + bool flagsChanged = true; + const QVector flags = m_streamParser->readParenthesizedList().toVector(); + if ( op == Replace ) { + flagsChanged = replaceFlags( pimItems, flags ); + } else if ( op == Add ) { + if ( !addFlags( pimItems, flags, flagsChanged ) ) { + return failureResponse( "Unable to add item flags." ); + } + } else if ( op == Delete ) { + if ( !( flagsChanged = deleteFlags( pimItems, flags ) ) ) { + return failureResponse( "Unable to remove item flags." ); + } + } + + if ( flagsChanged && !changes.contains( AKONADI_PARAM_FLAGS ) ) { + changes << AKONADI_PARAM_FLAGS; + } + continue; + } + + if ( command == AKONADI_PARAM_TAGS ) { + bool tagsChanged = true; + const ImapSet tags = m_streamParser->readSequenceSet(); + if ( op == Replace ) { + tagsChanged = replaceTags( pimItems, tags ); + } else if ( op == Add ) { + if ( !addTags( pimItems, tags, tagsChanged ) ) { + return failureResponse( "Unable to add item tags." ); + } + } else if ( op == Delete ) { + if ( !( tagsChanged = deleteTags( pimItems, tags ) ) ) { + return failureResponse( "Unable to remove item tags." ); + } + } + + if ( tagsChanged && !changes.contains( AKONADI_PARAM_TAGS ) ) { + changes << AKONADI_PARAM_TAGS; + } + continue; + } + + // handle commands that can only be applied to one item + if ( pimItems.size() > 1 ) { + throw HandlerException( "This Modification can only be applied to a single item" ); + } + PimItem &item = pimItems.first(); + if ( !item.isValid() ) { + throw HandlerException( "Invalid item in query result!?" ); + } + + if ( command == AKONADI_PARAM_REMOTEID ) { + const QString rid = m_streamParser->readUtf8String(); + if ( item.remoteId() != rid ) { + if ( !connection()->isOwnerResource( item ) ) { + throw HandlerException( "Only resources can modify remote identifiers" ); + } + item.setRemoteId( rid ); + changes << AKONADI_PARAM_REMOTEID; + } + } else if ( command == AKONADI_PARAM_GID ) { + const QString gid = m_streamParser->readUtf8String(); + if ( item.gid() != gid ) { + item.setGid( gid ); + } + changes << AKONADI_PARAM_GID; + } else if ( command == AKONADI_PARAM_REMOTEREVISION ) { + const QString remoteRevision = m_streamParser->readUtf8String(); + if ( item.remoteRevision() != remoteRevision ) { + if ( !connection()->isOwnerResource( item ) ) { + throw HandlerException( "Only resources can modify remote revisions" ); + } + item.setRemoteRevision( remoteRevision ); + changes << AKONADI_PARAM_REMOTEREVISION; + } + } else if ( command == AKONADI_PARAM_UNDIRTY ) { + m_streamParser->readString(); // read the 'false' string + item.setDirty( false ); + undirty = true; + } else if ( command == AKONADI_PARAM_INVALIDATECACHE ) { + invalidateCache = true; + } else if ( command == AKONADI_PARAM_SILENT ) { + notify = false; + } else if ( command == AKONADI_PARAM_SIZE ) { + mSize = m_streamParser->readNumber(); + changes << AKONADI_PARAM_SIZE; + } else if ( command == AKONADI_PARAM_PARTS ) { + const QList parts = m_streamParser->readParenthesizedList(); + if ( op == Delete ) { + if ( !store->removeItemParts( item, parts ) ) { + return failureResponse( "Unable to remove item parts." ); + } + changes += QSet::fromList( parts ); + } + } else if ( command == AKONADI_CMD_COLLECTION ) { + throw HandlerException( "Item moving via STORE is deprecated, update your Akonadi client" ); + } else { // parts/attributes + QByteArray partName; + qint64 partSize; + PartStreamer streamer( connection(), m_streamParser, item, this ); + connect( &streamer, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SIGNAL(responseAvailable(Akonadi::Server::Response)) ); + if ( !streamer.stream( command, true, partName, partSize ) ) { + return failureResponse( streamer.error() ); + } + + changes << partName; + partSizes += partSize; + } // parts/attribute modification + } + + QString datetime; + if ( !changes.isEmpty() || invalidateCache || undirty ) { + + // update item size + if ( pimItems.size() == 1 && ( mSize > 0 || partSizes > 0 ) ) { + pimItems.first().setSize( qMax( mSize, 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() ) { + throw HandlerException( "Unable to write item changes into the database" ); + } + + if ( invalidateCache ) { + if ( !store->invalidateItemCache( item ) ) { + throw HandlerException( "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 ( notify && !changes.isEmpty() && !onlyFlagsChanged && !onlyGIDChanged ) { + // Don't send FLAGS notification in itemChanged + changes.remove( AKONADI_PARAM_FLAGS ); + store->notificationCollector()->itemChanged( item, changes ); + } + + if ( !silent ) { + sendPimItemResponse( item ); + } + } + + if ( !transaction.commit() ) { + return failureResponse( "Cannot commit transaction." ); + } + + datetime = QLocale::c().toString( modificationtime, QLatin1String( "dd-MMM-yyyy hh:mm:ss +0000" ) ); + } else { + datetime = QLocale::c().toString( pimItems.first().datetime(), QLatin1String( "dd-MMM-yyyy hh:mm:ss +0000" ) ); + } + + // TODO: When implementing support for modifying multiple items at once, the revisions of the items should be in the responses. + // or only modified items should appear in the repsponse. + Response response; + response.setTag( tag() ); + response.setSuccess(); + response.setString( "DATETIME " + ImapParser::quote( datetime.toUtf8() ) + " STORE completed" ); + + Q_EMIT responseAvailable( response ); + return true; +} + +void Store::parseCommand() +{ + mScope.parseScope( m_streamParser ); + + // parse the stuff before the modification list + while ( !m_streamParser->hasList() ) { + const QByteArray command = m_streamParser->readString(); + if ( command.isEmpty() ) { // ie. we are at command end + throw HandlerException( "No modification list provided in STORE command" ); + } else if ( command == AKONADI_PARAM_REVISION ) { + mPreviousRevision = m_streamParser->readNumber(); + mCheckRevision = true; + } else if ( command == AKONADI_PARAM_SIZE ) { + mSize = m_streamParser->readNumber(); + } + } +} + +void Store::sendPimItemResponse( const PimItem &pimItem ) +{ + QList attrs; + attrs.push_back( AKONADI_PARAM_REVISION ); + attrs.push_back( QByteArray::number( pimItem.rev() ) ); + + QByteArray result; + result += QByteArray::number( pimItem.id() ); + result += " FETCH ("; + result += ImapParser::join( attrs, " " ); + result += ')'; + + Response response; + response.setUntagged(); + response.setString( result ); + Q_EMIT responseAvailable( response ); +} diff --git a/akonadi/server/src/handler/store.h b/akonadi/server/src/handler/store.h new file mode 100644 index 00000000..ad3a5a0f --- /dev/null +++ b/akonadi/server/src/handler/store.h @@ -0,0 +1,135 @@ +/*************************************************************************** + * 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 +#include +#include + +#include "storage/entity.h" +#include "libs/imapset_p.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 mofidied: + - 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: + Store( Scope::SelectionScope scope ); + bool parseStream(); + + private: + enum Operation { + Replace, + Add, + Delete + }; + + void parseCommand(); + + bool replaceFlags( const PimItem::List &items, const QVector &flags ); + bool addFlags( const PimItem::List &items, const QVector &flags, bool &flagsChanged ); + bool deleteFlags( const PimItem::List &items, const QVector &flags ); + bool replaceTags( const PimItem::List &items, const ImapSet &tags ); + bool addTags( const PimItem::List &items, const ImapSet &tags, bool &tagsChanged ); + bool deleteTags( const PimItem::List &items, const ImapSet &tags ); + bool setGid( const PimItem &item, const QString &gid ); + void sendPimItemResponse( const PimItem &pimItem ); + + private: + Scope mScope; + int mPos; + qint64 mPreviousRevision; + qint64 mSize; + bool mCheckRevision; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/subscribe.cpp b/akonadi/server/src/handler/subscribe.cpp new file mode 100644 index 00000000..7ac06980 --- /dev/null +++ b/akonadi/server/src/handler/subscribe.cpp @@ -0,0 +1,72 @@ +/* + 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 "subscribe.h" + +#include "imapstreamparser.h" +#include +#include +#include +#include +#include + +using namespace Akonadi::Server; + +Subscribe::Subscribe( bool subscribe ) + : mSubscribe( subscribe ) +{ +} + +bool Subscribe::parseStream() +{ + DataStore *store = connection()->storageBackend(); + Transaction transaction( store ); + + QByteArray buffer; + while ( !m_streamParser->atCommandEnd() ) { + buffer = m_streamParser->readString(); + if ( buffer.isEmpty() ) { + break; + } + Collection col = HandlerHelper::collectionFromIdOrName( buffer ); + if ( !col.isValid() ) { + return failureResponse( "Invalid collection" ); + } + if ( col.enabled() == mSubscribe ) { + continue; + } + // TODO do all changes in one db operation + col.setEnabled( mSubscribe ); + if ( !col.update() ) { + return failureResponse( "Unable to change subscription" ); + } + store->notificationCollector()->collectionChanged( col, QList() << AKONADI_PARAM_ENABLED ); + if ( mSubscribe ) { + store->notificationCollector()->collectionSubscribed( col ); + } else { + store->notificationCollector()->collectionUnsubscribed( col ); + } + } + + if ( !transaction.commit() ) { + return failureResponse( "Cannot commit transaction." ); + } + + return successResponse( "Completed" ); +} diff --git a/akonadi/server/src/handler/subscribe.h b/akonadi/server/src/handler/subscribe.h new file mode 100644 index 00000000..d8c2e991 --- /dev/null +++ b/akonadi/server/src/handler/subscribe.h @@ -0,0 +1,56 @@ +/* + 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_SUBSCRIBE_H +#define AKONADI_SUBSCRIBE_H + +#include + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the subscribe/unsubscribe commands. + Compatible with RFC 3501 section 6.3.7 and 6.3.8. + + Locally subscribe/unsubscribe a set of collections. + +

Syntax

+ @verbatim + tag "SUBSCRIBE " collection *( " " collection ) + tag "UNSUBSCRIBE " collection *( " " collection ) + @endverbatim + */ +class Subscribe : public Handler +{ + Q_OBJECT + public: + Subscribe( bool subscribe ); + bool parseStream(); + + private: + bool mSubscribe; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/tagappend.cpp b/akonadi/server/src/handler/tagappend.cpp new file mode 100644 index 00000000..54f9f2dc --- /dev/null +++ b/akonadi/server/src/handler/tagappend.cpp @@ -0,0 +1,175 @@ +/* + 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 "imapstreamparser.h" +#include "response.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" +#include "storage/countquerybuilder.h" +#include "entities.h" +#include "libs/protocol_p.h" + +using namespace Akonadi::Server; + +TagAppend::TagAppend() + : Handler() +{ +} + +TagAppend::~TagAppend() +{ +} + +bool TagAppend::parseStream() +{ + m_streamParser->beginList(); + + typedef QPair AttributePair; + QList attributes; + QString remoteId; + bool merge = false; + QString gid; + QByteArray type; + qint64 parentId = -1; + + while ( !m_streamParser->atListEnd() ) { + const QByteArray param = m_streamParser->readString(); + + if ( param == AKONADI_PARAM_GID ) { + gid = QString::fromLatin1( m_streamParser->readString() ); + } else if ( param == AKONADI_PARAM_PARENT ) { + parentId = m_streamParser->readNumber(); + } else if ( param == AKONADI_PARAM_REMOTEID ) { + if ( !connection()->context()->resource().isValid() ) { + throw HandlerException( "Only resource can create tag with remote ID" ); + } + remoteId = QString::fromLatin1( m_streamParser->readString() ); + } else if ( param == AKONADI_PARAM_MERGE ) { + merge = true; + } else if ( param == AKONADI_PARAM_MIMETYPE ) { + type = m_streamParser->readString(); + } else { + attributes << qMakePair( param, m_streamParser->readString() ); + } + } + + TagType tagType; + if ( !type.isEmpty() ) { + tagType= TagType::retrieveByName( QString::fromLatin1( type ) ); + if ( !tagType.isValid() ) { + TagType t( QString::fromLatin1( type ) ); + if ( !t.insert() ) { + return failureResponse( QByteArray( "Unable to create tagtype '" ) + type + QByteArray( "'." ) ); + } + tagType = t; + } + } + + qint64 tagId = -1; + if ( merge ) { + QueryBuilder qb( Tag::tableName() ); + qb.addColumn( Tag::idColumn() ); + qb.addValueCondition( Tag::gidColumn(), Query::Equals, gid ); + if ( !qb.exec() ) { + throw HandlerException( "Unable to list tags" ); + } + if (qb.query().next()) { + tagId = qb.query().value( 0 ).toLongLong(); + } + } + if (tagId < 0) { + Tag insertedTag; + insertedTag.setGid( gid ); + if ( parentId >= 0 ) { + insertedTag.setParentId( parentId ); + } + if ( tagType.isValid() ) { + insertedTag.setTypeId( tagType.id() ); + } + if ( !insertedTag.insert( &tagId ) ) { + throw HandlerException( "Failed to store tag" ); + } + + Q_FOREACH ( const AttributePair &pair, attributes ) { + TagAttribute attribute; + attribute.setTagId( tagId ); + attribute.setType( pair.first ); + attribute.setValue( pair.second ); + if ( !attribute.insert() ) { + throw HandlerException( "Failed to store tag attribute" ); + } + } + + DataStore::self()->notificationCollector()->tagAdded( insertedTag ); + } + + if ( !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() ) { + throw HandlerException( "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(), remoteId ); + ret = qb.exec(); + } else { + TagRemoteIdResourceRelation rel; + rel.setTagId( tagId ); + rel.setResourceId( resourceId ); + rel.setRemoteId( remoteId ); + ret = rel.insert(); + } + if ( !ret ) { + throw HandlerException( "Failed to store tag remote ID" ); + } + } + + ImapSet set; + set.add( QVector() << tagId ); + TagFetchHelper helper( connection(), set ); + connect( &helper, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SIGNAL(responseAvailable(Akonadi::Server::Response)) ); + + if ( !helper.fetchTags( AKONADI_CMD_TAGFETCH ) ) { + return false; + } + + Response response; + response.setTag( tag() ); + response.setSuccess(); + response.setString( "Append completed" ); + Q_EMIT responseAvailable( response ); + return true; +} + + diff --git a/akonadi/server/src/handler/tagappend.h b/akonadi/server/src/handler/tagappend.h new file mode 100644 index 00000000..ede2aae2 --- /dev/null +++ b/akonadi/server/src/handler/tagappend.h @@ -0,0 +1,43 @@ +/* + 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: + TagAppend(); + ~TagAppend(); + + bool parseStream(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_TAGAPPEND_H diff --git a/akonadi/server/src/handler/tagfetch.cpp b/akonadi/server/src/handler/tagfetch.cpp new file mode 100644 index 00000000..7fb6e343 --- /dev/null +++ b/akonadi/server/src/handler/tagfetch.cpp @@ -0,0 +1,57 @@ +/*************************************************************************** + * 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 "imapstreamparser.h" +#include "connection.h" +#include "libs/imapset_p.h" +#include "libs/protocol_p.h" +#include "tagfetchhelper.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +TagFetch::TagFetch( Scope::SelectionScope scope ) + : Handler() + , mScope( scope ) +{ +} + +TagFetch::~TagFetch() +{ +} + +bool TagFetch::parseStream() +{ + if ( mScope.scope() != Scope::Uid ) { + throw HandlerException( "Only UID-based TAGFETCH is supported" ); + } + + mScope.parseScope( m_streamParser ); + + TagFetchHelper helper( connection(), mScope.uidSet() ); + connect( &helper, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SIGNAL(responseAvailable(Akonadi::Server::Response)) ); + + if ( !helper.fetchTags( AKONADI_CMD_TAGFETCH ) ) { + return false; + } + + return successResponse( "UID TAGFETCH completed" ); +} diff --git a/akonadi/server/src/handler/tagfetch.h b/akonadi/server/src/handler/tagfetch.h new file mode 100644 index 00000000..2b47e34a --- /dev/null +++ b/akonadi/server/src/handler/tagfetch.h @@ -0,0 +1,50 @@ +/*************************************************************************** + * 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" +#include "scope.h" + +namespace Akonadi { +namespace Server { + +/** + @ingroup akonadi_server_handler + + Handler for the FETCHTAG command. + */ +class TagFetch : public Handler +{ + Q_OBJECT +public: + TagFetch( Scope::SelectionScope scope ); + ~TagFetch(); + + bool parseStream(); + + private: + Scope mScope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handler/tagfetchhelper.cpp b/akonadi/server/src/handler/tagfetchhelper.cpp new file mode 100644 index 00000000..ac72aa49 --- /dev/null +++ b/akonadi/server/src/handler/tagfetchhelper.cpp @@ -0,0 +1,184 @@ +/* + 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 "response.h" +#include "libs/imapparser_p.h" +#include "libs/protocol_p.h" +#include "storage/querybuilder.h" +#include "storage/queryhelper.h" +#include "entities.h" +#include "connection.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +TagFetchHelper::TagFetchHelper( Connection *connection, const ImapSet &set ) + : QObject() + , mConnection( connection ) + , mSet( set ) +{ +} + +QSqlQuery TagFetchHelper::buildAttributeQuery() const +{ + QueryBuilder qb( TagAttribute::tableName() ); + qb.addColumn( TagAttribute::tagIdColumn() ); + qb.addColumn( TagAttribute::typeColumn() ); + qb.addColumn( TagAttribute::valueColumn() ); + qb.addSortColumn( TagAttribute::tagIdColumn(), Query::Descending ); + + QueryHelper::setToQuery( mSet, TagAttribute::tagIdColumn(), 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 ); + QueryHelper::setToQuery( mSet, Tag::idFullColumnName(), qb ); + if ( !qb.exec() ) { + throw HandlerException( "Unable to list tags" ); + } + + qb.query().next(); + return qb.query(); +} + +QList TagFetchHelper::fetchTagAttributes( qint64 tagId ) +{ + QList attributes; + + QSqlQuery attributeQuery = buildAttributeQuery( tagId ); + while ( attributeQuery.isValid() ) { + const QByteArray attrName = attributeQuery.value( 1 ).toByteArray(); + const QByteArray attrValue = attributeQuery.value( 2 ).toByteArray(); + + attributes << attrName << ImapParser::quote( attrValue ); + attributeQuery.next(); + } + return attributes; +} + +QByteArray TagFetchHelper::tagToByteArray(qint64 tagId, const QByteArray &gid, qint64 parentId, const QByteArray &type, const QByteArray &remoteId, const QList &tagAttributes) +{ + QList attributes; + attributes << AKONADI_PARAM_UID << QByteArray::number( tagId ); + attributes << AKONADI_PARAM_GID << ImapParser::quote( gid ); + attributes << AKONADI_PARAM_PARENT << QByteArray::number( parentId ); + attributes << AKONADI_PARAM_MIMETYPE " " + ImapParser::quote( type ); + if ( !remoteId.isEmpty() ) { + attributes << AKONADI_PARAM_REMOTEID << remoteId; + } + + attributes << tagAttributes; + + return ImapParser::join( attributes, " " ); +} + +bool TagFetchHelper::fetchTags(const QByteArray& responseIdentifier) +{ + + QSqlQuery tagQuery = buildTagQuery(); + QSqlQuery attributeQuery = buildAttributeQuery(); + + Response response; + response.setUntagged(); + while ( tagQuery.isValid() ) { + const qint64 tagId = tagQuery.value( 0 ).toLongLong(); + const QByteArray gid = tagQuery.value( 1 ).toByteArray(); + const qint64 parentId = tagQuery.value( 2 ).toLongLong(); + //we're ignoring the type id + const QByteArray type = tagQuery.value( 4 ).toByteArray(); + QByteArray remoteId; + if ( mConnection->context()->resource().isValid() ) { + remoteId = tagQuery.value( 5 ).toByteArray(); + } + + QList tagAttributes; + while ( attributeQuery.isValid() ) { + const qint64 id = attributeQuery.value( 0 ).toLongLong(); + if ( id > tagId ) { + attributeQuery.next(); + continue; + } else if ( id < tagId ) { + break; + } + + const QByteArray attrName = attributeQuery.value( 1 ).toByteArray(); + const QByteArray attrValue = attributeQuery.value( 2 ).toByteArray(); + + tagAttributes << attrName << ImapParser::quote( attrValue ); + attributeQuery.next(); + } + QByteArray tagReply = QByteArray::number( tagId ) + ' ' + responseIdentifier + " ("; + tagReply += tagToByteArray(tagId, gid, parentId, type, remoteId, tagAttributes) + ')'; + response.setUntagged(); + response.setString( tagReply ); + Q_EMIT responseAvailable( response ); + + tagQuery.next(); + } + + return true; +} + diff --git a/akonadi/server/src/handler/tagfetchhelper.h b/akonadi/server/src/handler/tagfetchhelper.h new file mode 100644 index 00000000..715a2b9d --- /dev/null +++ b/akonadi/server/src/handler/tagfetchhelper.h @@ -0,0 +1,69 @@ +/* + 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 "libs/imapset_p.h" + +namespace Akonadi { + +class ImapSet; + +namespace Server { + +class ImapStreamParser; + +class Connection; +class Response; + +class TagFetchHelper : public QObject +{ + Q_OBJECT + + public: + TagFetchHelper( Connection *connection, const ImapSet &set ); + + bool fetchTags( const QByteArray &responseIdentifier ); + + static QList fetchTagAttributes( qint64 tagId ); + static QByteArray tagToByteArray( qint64 tagId, const QByteArray &gid, qint64 parentId, const QByteArray &type, const QByteArray &remoteId, const QList &tagAttributes ); + + Q_SIGNALS: + void responseAvailable( const Akonadi::Server::Response &response ); + + private: + QSqlQuery buildTagQuery(); + QSqlQuery buildAttributeQuery() const; + static QSqlQuery buildAttributeQuery( qint64 id ); + + private: + ImapStreamParser *mStreamParser; + + Connection *mConnection; + ImapSet mSet; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_TAGFETCHHELPER_H diff --git a/akonadi/server/src/handler/tagremove.cpp b/akonadi/server/src/handler/tagremove.cpp new file mode 100644 index 00000000..05feaf80 --- /dev/null +++ b/akonadi/server/src/handler/tagremove.cpp @@ -0,0 +1,85 @@ +/* + 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/querybuilder.h" +#include "storage/selectquerybuilder.h" +#include "storage/queryhelper.h" +#include "storage/datastore.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +TagRemove::TagRemove( Scope::SelectionScope scope ) + : Handler() + , mScope( scope ) +{ +} + +TagRemove::~TagRemove() +{ +} + +bool TagRemove::parseStream() +{ + if ( mScope.scope() != Scope::Uid ) { + throw HandlerException( "Only UID-based TAGREMOVE is supported" ); + } + + mScope.parseScope( m_streamParser ); + + // Get all PIM items that we will untag + SelectQueryBuilder itemsQuery; + itemsQuery.addJoin( QueryBuilder::LeftJoin, PimItemTagRelation::tableName(), PimItemTagRelation::leftFullColumnName(), PimItem::idFullColumnName() ); + QueryHelper::setToQuery( mScope.uidSet(), PimItemTagRelation::rightColumn(), itemsQuery ); + + if ( !itemsQuery.exec() ) { + throw HandlerException( "Untagging failed" ); + } + const PimItem::List items = itemsQuery.result(); + + SelectQueryBuilder tagQuery; + QueryHelper::setToQuery( mScope.uidSet(), Tag::idFullColumnName(), tagQuery ); + if ( !tagQuery.exec() ) { + throw HandlerException( "Failed to obtain tags" ); + } + const Tag::List tags = tagQuery.result(); + + QSet removedTags; + Q_FOREACH ( const Tag &tag, tags ) { + removedTags << tag.id(); + } + if ( !items.isEmpty() ) { + DataStore::self()->notificationCollector()->itemsTagsChanged( items, QSet(), removedTags ); + } + + Q_FOREACH ( const Tag &tag, tags ) { + DataStore::self()->notificationCollector()->tagRemoved( tag ); + } + + // Just remove the tags, table constraints will take care of the rest + QueryBuilder qb( Tag::tableName(), QueryBuilder::Delete ); + QueryHelper::setToQuery( mScope.uidSet(), Tag::idFullColumnName(), qb ); + if ( !qb.exec() ) { + throw HandlerException( "Deletion failed" ); + } + + return successResponse( "TAGREMOVE complete" ); +} + diff --git a/akonadi/server/src/handler/tagremove.h b/akonadi/server/src/handler/tagremove.h new file mode 100644 index 00000000..5cdf4153 --- /dev/null +++ b/akonadi/server/src/handler/tagremove.h @@ -0,0 +1,45 @@ +/* + 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" +#include "scope.h" + +namespace Akonadi { +namespace Server { + +class TagRemove : public Handler +{ + Q_OBJECT + public: + TagRemove( Scope::SelectionScope scope ); + ~TagRemove(); + + virtual bool parseStream(); + + private: + Scope mScope; +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_TAGREMOVE_H diff --git a/akonadi/server/src/handler/tagstore.cpp b/akonadi/server/src/handler/tagstore.cpp new file mode 100644 index 00000000..48d48069 --- /dev/null +++ b/akonadi/server/src/handler/tagstore.cpp @@ -0,0 +1,109 @@ +/* + 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 "scope.h" +#include "tagfetchhelper.h" +#include "imapstreamparser.h" +#include "response.h" +#include "storage/datastore.h" +#include "libs/protocol_p.h" + +using namespace Akonadi::Server; + +TagStore::TagStore() + : Handler() +{ +} + +TagStore::~TagStore() +{ +} + +bool TagStore::parseStream() +{ + const qint64 tagId = m_streamParser->readNumber(); + + if ( !m_streamParser->hasList() ) { + failureResponse( "No changes to store" ); + return false; + } + + Tag changedTag = Tag::retrieveById( tagId ); + if ( !changedTag.isValid() ) { + throw HandlerException( "No such tag" ); + } + + // Retrieve all tag's attributes + const TagAttribute::List attributes = TagAttribute::retrieveFiltered( TagAttribute::tagIdFullColumnName(), tagId ); + QMap attributesMap; + Q_FOREACH ( const TagAttribute &attribute, attributes ) { + attributesMap.insert( attribute.type(), attribute ); + } + + m_streamParser->beginList(); + while ( !m_streamParser->atListEnd() ) { + const QByteArray attr = m_streamParser->readString(); + + if ( attr == AKONADI_PARAM_PARENT ) { + const qint64 parent = m_streamParser->readNumber(); + changedTag.setParentId( parent ); + } else if ( attr == AKONADI_PARAM_GID ) { + throw HandlerException( "Changing tag GID is not allowed" ); + } else if ( attr == AKONADI_PARAM_UID ) { + throw HandlerException( "Changing tag UID is not allowed" ); + } else { + if ( attr.startsWith( '-' ) ) { + const QByteArray attrName = attr.mid( 1 ); + if ( attributesMap.contains( attrName ) ) { + TagAttribute attribute = attributesMap.value( attrName ); + TagAttribute::remove( attribute.id() ); + } + } else if ( attributesMap.contains( attr ) ) { + TagAttribute attribute = attributesMap.value( attr ); + attribute.setValue( m_streamParser->readString() ); + attribute.update(); + } else { + TagAttribute attribute; + attribute.setTagId( tagId ); + attribute.setType( attr ); + attribute.setValue( m_streamParser->readString() ); + attribute.insert(); + } + } + } + + DataStore::self()->notificationCollector()->tagChanged( changedTag ); + + ImapSet set; + set.add( QVector() << tagId ); + TagFetchHelper helper( connection(), set ); + connect( &helper, SIGNAL(responseAvailable(Akonadi::Server::Response)), + this, SIGNAL(responseAvailable(Akonadi::Server::Response)) ); + if ( !helper.fetchTags( AKONADI_CMD_TAGFETCH ) ) { + return false; + } + + Response response; + response.setTag( tag() ); + response.setSuccess(); + response.setString( "TAGSTORE completed" ); + Q_EMIT responseAvailable( response ); + return true; +} diff --git a/akonadi/server/src/handler/tagstore.h b/akonadi/server/src/handler/tagstore.h new file mode 100644 index 00000000..bf74f1d1 --- /dev/null +++ b/akonadi/server/src/handler/tagstore.h @@ -0,0 +1,44 @@ +/* + 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: + TagStore(); + ~TagStore(); + + bool parseStream(); + +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_TAGSTORE_H diff --git a/akonadi/server/src/handler/transaction.cpp b/akonadi/server/src/handler/transaction.cpp new file mode 100644 index 00000000..24a8f131 --- /dev/null +++ b/akonadi/server/src/handler/transaction.cpp @@ -0,0 +1,65 @@ +/* + 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 "storage/datastore.h" +#include "connection.h" +#include "response.h" +#include "imapstreamparser.h" + +#include + +using namespace Akonadi::Server; + +TransactionHandler::TransactionHandler( Mode mode ) + : mMode( mode ) +{ +} + +bool TransactionHandler::parseStream() +{ + DataStore *store = connection()->storageBackend(); + + if ( mMode == Begin ) { + if ( !store->beginTransaction() ) { + return failureResponse( "Unable to begin transaction." ); + } + } + + if ( mMode == Rollback ) { + if ( !store->inTransaction() ) { + return failureResponse( "There is no transaction in progress." ); + } + if ( !store->rollbackTransaction() ) { + return failureResponse( "Unable to roll back transaction." ); + } + } + + if ( mMode == Commit ) { + if ( !store->inTransaction() ) { + return failureResponse( "There is no transaction in progress." ); + } + if ( !store->commitTransaction() ) { + return failureResponse( "Unable to commit transaction." ); + } + } + + const QMetaEnum me = metaObject()->enumerator( metaObject()->indexOfEnumerator( "Mode" ) ); + return successResponse( me.valueToKey( mMode ) + QByteArray( " completed" ) ); +} diff --git a/akonadi/server/src/handler/transaction.h b/akonadi/server/src/handler/transaction.h new file mode 100644 index 00000000..f2a10b01 --- /dev/null +++ b/akonadi/server/src/handler/transaction.h @@ -0,0 +1,55 @@ +/* + 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 + +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: + enum Mode { + Begin, + Commit, + Rollback + }; + + TransactionHandler( Mode mode ); + bool parseStream(); + + private: + Mode mMode; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/handlerhelper.cpp b/akonadi/server/src/handlerhelper.cpp new file mode 100644 index 00000000..763ea30c --- /dev/null +++ b/akonadi/server/src/handlerhelper.cpp @@ -0,0 +1,447 @@ +/*************************************************************************** + * 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 "imapstreamparser.h" +#include "storage/countquerybuilder.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" +#include "storage/queryhelper.h" +#include "libs/imapparser_p.h" +#include "libs/protocol_p.h" +#include "commandcontext.h" +#include "handler.h" + +#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( QLatin1String( "/" ) ); +} + +bool HandlerHelper::itemStatistics( const Collection &col, qint64 &count, qint64 &size ) +{ + QueryBuilder qb( PimItem::tableName() ); + qb.addAggregation( PimItem::idColumn(), QLatin1String( "count" ) ); + qb.addAggregation( PimItem::sizeColumn(), QLatin1String( "sum" ) ); + + 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 false; + } + if ( !qb.query().next() ) { + akError() << "Error during retrieving result of statistics query:" << qb.query().lastError().text(); + return false; + } + count = qb.query().value( 0 ).toLongLong(); + size = qb.query().value( 1 ).toLongLong(); + return true; +} + +int HandlerHelper::itemWithFlagsCount( const Collection &col, const QStringList &flags ) +{ + CountQueryBuilder qb( PimItem::tableName(), PimItem::idFullColumnName(), CountQueryBuilder::Distinct ); + qb.addJoin( QueryBuilder::InnerJoin, PimItemFlagRelation::tableName(), + PimItem::idFullColumnName(), PimItemFlagRelation::leftFullColumnName() ); + 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::collectionIdFullColumnName(), Query::Equals, col.id() ); + } + Query::Condition cond( Query::Or ); + // We use the below instead of an inner join in the query above because postgres seems + // to struggle to optimize the two inner joins, despite having indices that should + // facilitate that. This exploits the fact that the Flag::retrieveByName is fast because + // it hits an in-memory cache. + Q_FOREACH ( const QString &flag, flags ) { + const Flag f = Flag::retrieveByName( flag ); + cond.addValueCondition( PimItemFlagRelation::rightFullColumnName(), Query::Equals, f.id() ); + } + qb.addCondition( cond ); + if ( !qb.exec() ) { + return -1; + } + return qb.result(); +} + +int HandlerHelper::itemCount( const Collection &col ) +{ + CountQueryBuilder qb( PimItem::tableName() ); + qb.addValueCondition( PimItem::collectionIdColumn(), Query::Equals, col.id() ); + if ( !qb.exec() ) { + return -1; + } + return qb.result(); +} + +int HandlerHelper::parseCachePolicy( const QByteArray &data, Collection &col, int start, bool *changed ) +{ + bool inheritChanged = false; + bool somethingElseChanged = false; + + QList params; + int end = ImapParser::parseParenthesizedList( data, params, start ); + for ( int i = 0; i < params.count() - 1; i += 2 ) { + const QByteArray key = params[i]; + const QByteArray value = params[i + 1]; + + if ( key == AKONADI_PARAM_INHERIT ) { + const bool inherit = value == "true"; + inheritChanged = col.cachePolicyInherit() != inherit; + col.setCachePolicyInherit( inherit ); + } else if ( key == AKONADI_PARAM_INTERVAL ) { + const int interval = value.toInt(); + somethingElseChanged = somethingElseChanged || interval != col.cachePolicyCheckInterval(); + col.setCachePolicyCheckInterval( interval ); + } else if ( key == AKONADI_PARAM_CACHETIMEOUT ) { + const int timeout = value.toInt(); + somethingElseChanged = somethingElseChanged || timeout != col.cachePolicyCacheTimeout(); + col.setCachePolicyCacheTimeout( timeout ); + } else if ( key == AKONADI_PARAM_SYNCONDEMAND ) { + const bool syncOnDemand = value == "true"; + somethingElseChanged = somethingElseChanged || syncOnDemand != col.cachePolicySyncOnDemand(); + col.setCachePolicySyncOnDemand( syncOnDemand ); + } else if ( key == AKONADI_PARAM_LOCALPARTS ) { + QList tmp; + QStringList partsList; + ImapParser::parseParenthesizedList( value, tmp ); + Q_FOREACH ( const QByteArray &ba, tmp ) { + partsList << QString::fromLatin1( ba ); + } + const QString parts = partsList.join( QLatin1String( " " ) ); + somethingElseChanged = somethingElseChanged || col.cachePolicyLocalParts() != parts; + col.setCachePolicyLocalParts( parts ); + } + } + + if ( changed && ( inheritChanged || ( !col.cachePolicyInherit() && somethingElseChanged ) ) ) { + *changed = true; + } + + return end; +} + +QByteArray HandlerHelper::cachePolicyToByteArray( const Collection &col ) +{ + QByteArray rv = AKONADI_PARAM_CACHEPOLICY " ("; + rv += AKONADI_PARAM_INHERIT " " + ( col.cachePolicyInherit() ? QByteArray( "true" ) : QByteArray( "false" ) ); + rv += " " AKONADI_PARAM_INTERVAL " " + QByteArray::number( col.cachePolicyCheckInterval() ); + rv += " " AKONADI_PARAM_CACHETIMEOUT " " + QByteArray::number( col.cachePolicyCacheTimeout() ); + rv += " " AKONADI_PARAM_SYNCONDEMAND " " + ( col.cachePolicySyncOnDemand() ? QByteArray( "true" ) : QByteArray( "false" ) ); + rv += " " AKONADI_PARAM_LOCALPARTS " (" + col.cachePolicyLocalParts().toLatin1() + ')'; + rv += ')'; + return rv; +} + +QByteArray HandlerHelper::tristateToByteArray( const Tristate &tristate ) +{ + if ( tristate == Tristate::True ) { + return "TRUE"; + } else if ( tristate == Tristate::False ) { + return "FALSE"; + } + return "DEFAULT"; +} + +QByteArray HandlerHelper::collectionToByteArray( const Collection &col, bool hidden, bool includeStatistics, + int ancestorDepth, const QStack &ancestors, bool isReferenced ) +{ + QByteArray b = QByteArray::number( col.id() ) + ' ' + + QByteArray::number( col.parentId() ) + " ("; + + b += AKONADI_PARAM_NAME " " + ImapParser::quote( col.name().toUtf8() ) + ' '; + if ( hidden ) { + b += AKONADI_PARAM_MIMETYPE " () "; + } else { + b += AKONADI_PARAM_MIMETYPE " (" + MimeType::joinByName( col.mimeTypes(), QLatin1String( " " ) ).toLatin1() + ") "; + } + b += AKONADI_PARAM_REMOTEID " " + ImapParser::quote( col.remoteId().toUtf8() ); + b += " " AKONADI_PARAM_REMOTEREVISION " " + ImapParser::quote( col.remoteRevision().toUtf8() ); + b += " " AKONADI_PARAM_RESOURCE " " + ImapParser::quote( col.resource().name().toUtf8() ); + b += " " AKONADI_PARAM_VIRTUAL " " + QByteArray::number( col.isVirtual() ) + ' '; + + if ( includeStatistics ) { + qint64 itemCount, itemSize; + if ( itemStatistics( col, itemCount, itemSize ) ) { + b += AKONADI_ATTRIBUTE_MESSAGES " " + QByteArray::number( itemCount ) + ' '; + // itemWithFlagCount is twice as fast as itemWithoutFlagCount, so emulated that... + b += AKONADI_ATTRIBUTE_UNSEEN " "; + b += QByteArray::number( itemCount - itemWithFlagsCount( col, QStringList() << QLatin1String( AKONADI_FLAG_SEEN ) + << QLatin1String( AKONADI_FLAG_IGNORED ) ) ); + b += " " AKONADI_PARAM_SIZE " " + QByteArray::number( itemSize ) + ' '; + } + } + + if ( !col.queryString().isEmpty() ) { + b += AKONADI_PARAM_PERSISTENTSEARCH " "; + QList args; + args.append( col.queryAttributes().toLatin1() ); + args.append( AKONADI_PARAM_PERSISTENTSEARCH_QUERYSTRING ); + args.append( ImapParser::quote( col.queryString().toUtf8() ) ); + args.append( AKONADI_PARAM_PERSISTENTSEARCH_QUERYCOLLECTIONS ); + args.append( "(" + col.queryCollections().toLatin1() + ")" ); + b += ImapParser::quote( "(" + ImapParser::join( args, " " ) + ")" ); + b += ' '; + } + + b += HandlerHelper::cachePolicyToByteArray( col ) + ' '; + if ( ancestorDepth > 0 ) { + b += HandlerHelper::ancestorsToByteArray( ancestorDepth, ancestors ) + ' '; + } + + if ( isReferenced ) { + b += AKONADI_PARAM_REFERENCED " TRUE "; + } + b += AKONADI_PARAM_ENABLED " "; + if ( col.enabled() ) { + b += "TRUE "; + } else { + b += "FALSE "; + } + b += AKONADI_PARAM_DISPLAY " " + tristateToByteArray( col.displayPref() ) + ' '; + b += AKONADI_PARAM_SYNC " " + tristateToByteArray( col.syncPref() ) + ' '; + b += AKONADI_PARAM_INDEX " " + tristateToByteArray( col.indexPref() ) + ' '; + + const CollectionAttribute::List attrs = col.attributes(); + for ( int i = 0; i < attrs.size(); ++i ) { + const CollectionAttribute &attr = attrs[i]; + //Workaround to skip invalid "PARENT " attributes that were accidentaly created after 6e5bbf6 + if ( attr.type() == "PARENT" ) { + continue; + } + b += attr.type() + ' ' + ImapParser::quote( attr.value() ); + if ( i != attrs.size() - 1 ) { + b += ' '; + } + } + b+= ')'; + + return b; +} + +QByteArray HandlerHelper::ancestorsToByteArray( int ancestorDepth, const QStack &_ancestors ) +{ + QByteArray b; + if ( ancestorDepth > 0 ) { + b += AKONADI_PARAM_ANCESTORS " ("; + QStack ancestors( _ancestors ); + for ( int i = 0; i < ancestorDepth; ++i ) { + if ( ancestors.isEmpty() ) { + b += "(0 \"\")"; + break; + } + b += '('; + const Collection c = ancestors.pop(); + b += QByteArray::number( c.id() ) + " "; + b += ImapParser::quote( c.remoteId().toUtf8() ); + b += ")"; + if ( i != ancestorDepth - 1 ) { + b += ' '; + } + } + b += ')'; + } + return b; +} + +int HandlerHelper::parseDepth( const QByteArray &depth ) +{ + if ( depth.isEmpty() ) { + throw ImapParserException( "No depth specified" ); + } + if ( depth == "INF" ) { + return INT_MAX; + } + bool ok = false; + int result = depth.toInt( &ok ); + if ( !ok ) { + throw ImapParserException( "Invalid depth argument" ); + } + return result; +} + +Flag::List HandlerHelper::resolveFlags( const QVector &flagNames ) +{ + Flag::List flagList; + 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::resolveTags( const ImapSet &tags ) +{ + 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 QVector &tagsGIDs) +{ + Tag::List tagList; + if (tagsGIDs.isEmpty()) { + return tagList; + } + + Q_FOREACH (const QByteArray &tagGID, tagsGIDs) { + Tag::List tags = Tag::retrieveFiltered(Tag::gidColumn(), tagGID); + Tag tag; + if (tags.isEmpty()) { + tag.setGid(QString::fromUtf8(tagGID)); + tag.setParentId(0); + + TagType type = TagType::retrieveByName(QLatin1String("PLAIN")); + if (!type.isValid()) { + type.setName(QLatin1String("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 QVector< QByteArray >& 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"); + } + + Q_FOREACH (const QByteArray &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(QVector() << tagRID); + if (tags.count() != 1) { + throw HandlerException("Unable to resolve tag"); + } + tag = tags[0]; + TagRemoteIdResourceRelation rel; + rel.setRemoteId(QString::fromUtf8(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; +} diff --git a/akonadi/server/src/handlerhelper.h b/akonadi/server/src/handlerhelper.h new file mode 100644 index 00000000..22e6e1c6 --- /dev/null +++ b/akonadi/server/src/handlerhelper.h @@ -0,0 +1,130 @@ +/*************************************************************************** + * 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 +#include + +#include +#include +#include +#include + +namespace Akonadi { +namespace Server { + +class CommandContext; + +class ImapStreamParser; + +/** + 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 amount of existing items in the given collection. + @return -1 on error + */ + static int itemCount( const Collection &col ); + + /** + * Queries for collection statistics. + * @param col The collection to query. + * @param count The total amount of items in this collection. + * @param size The size of all items in this collection. + * @return @c false on a query error, @c true otherwise + */ + static bool itemStatistics( const Collection &col, qint64 &count, qint64 &size ); + + /** + Returns the amount of existing items in the given collection + which have a given flag set. + @return -1 on error. + */ + static int itemWithFlagsCount( const Collection &col, const QStringList &flags ); + + /** + Parse cache policy and update the given Collection object accoordingly. + @param changed Indicates whether or not the cache policy already available in @p col + has actually changed + @todo Error handling. + */ + static int parseCachePolicy( const QByteArray &data, Collection &col, int start = 0, bool *changed = 0 ); + + /** + Returns the protocol representation of the cache policy of the given + Collection object. + */ + static QByteArray cachePolicyToByteArray( const Collection &col ); + + static QByteArray tristateToByteArray( const Tristate &tristate ); + + /** + Returns the protocol representation of the given collection. + Make sure DataStore::activeCachePolicy() has been called before to include + the effective cache policy + */ + static QByteArray collectionToByteArray( const Collection &col, bool hidden = false, bool includeStatistics = false, + int ancestorDepth = 0, const QStack &ancestors = QStack(), bool isReferenced = false ); + + /** + Returns the protocol representation of a collection ancestor chain. + */ + static QByteArray ancestorsToByteArray( int ancestorDepth, const QStack &ancestors ); + + /** + Parses the listing/ancestor depth parameter. + */ + static int parseDepth( const QByteArray &depth ); + + /** + Converts a bytearray list of flag names into flag records. + @throws HandlerException on errors during datbase operations + */ + static Flag::List resolveFlags( const QVector &flagNames ); + + /** + Converts a imap set of tags into tag records. + @throws HandlerException on errors during datbase operations + */ + static Tag::List resolveTags( const ImapSet &tags ); + + static Tag::List resolveTagsByGID( const QVector &tagsGIDs ); + + static Tag::List resolveTagsByRID( const QVector &tagsRIDs, CommandContext *context ); +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/imapstreamparser.cpp b/akonadi/server/src/imapstreamparser.cpp new file mode 100644 index 00000000..425b7b7d --- /dev/null +++ b/akonadi/server/src/imapstreamparser.cpp @@ -0,0 +1,849 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + Copyright (c) 2009 Andras Mantia + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#include "imapstreamparser.h" +#include "response.h" +#include "tracer.h" + +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +ImapStreamParser::ImapStreamParser( QIODevice *socket ) + : m_socket( socket ) + , m_position( 0 ) + , m_literalSize( 0 ) + , m_peeking( false ) + , m_timeout( 30 * 1000 ) +{ +} + +ImapStreamParser::~ImapStreamParser() +{ +} + +void ImapStreamParser::setWaitTimeout(int msecs) +{ + m_timeout = msecs; +} + +QString ImapStreamParser::readUtf8String() +{ + QByteArray tmp; + tmp = readString(); + QString result = QString::fromUtf8( tmp ); + return result; +} + +QByteArray ImapStreamParser::readString() +{ + QByteArray result; + if ( !waitForMoreData( m_data.length() == 0 ) ) { + throw ImapParserException( "Unable to read more data" ); + } + stripLeadingSpaces(); + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + + // literal string + // TODO: error handling + if ( hasLiteral() ) { + while ( !atLiteralEnd() ) { + result += readLiteralPart(); + } + return result; + } + + // quoted string + return parseQuotedString(); +} + +QByteArray ImapStreamParser::peekString() +{ + m_peeking = true; + int pos = m_position; + const QByteArray string = readString(); + m_position = pos; + m_peeking = false; + return string; +} + +bool ImapStreamParser::hasString() +{ + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + int savedPos = m_position ; + stripLeadingSpaces(); + int pos = m_position; + m_position = savedPos; + if ( m_data[pos] == '{' ) { + return true; //literal string + } + if ( m_data[pos] == '"' ) { + return true; //quoted string + } + if ( m_data[pos] != ' ' && + m_data[pos] != '(' && + m_data[pos] != ')' && + m_data[pos] != '[' && + m_data[pos] != ']' && + m_data[pos] != '\n' && + m_data[pos] != '\r' ) { + return true; //unquoted string + } + + return false; //something else, not a string +} + +bool ImapStreamParser::hasLiteral( bool requestData ) +{ + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + int savedPos = m_position; + stripLeadingSpaces(); + if ( m_data[m_position] == '{' ) { + int end = -1; + do { + end = m_data.indexOf( '}', m_position ); + if ( !waitForMoreData( end == -1 ) ) { + throw ImapParserException( "Unable to read more data" ); + } + } while ( end == -1 ); + Q_ASSERT( end > m_position ); + m_literalSize = m_data.mid( m_position + 1, end - m_position - 1 ).toInt(); + // strip CRLF + m_position = end + 1; + + //IMAP inconsistency. IMAP always expects CRLF, but akonadi uses only LF. + if ( m_position < m_data.length() && m_data[m_position] == '\n' ) { + ++m_position; + } + + if ( m_literalSize >= 0 && requestData ) { + sendContinuationResponse( m_literalSize ); + } + return true; + } else { + m_position = savedPos; + return false; + } +} + +bool ImapStreamParser::atLiteralEnd() const +{ + return ( m_literalSize == 0 ); +} + +qint64 ImapStreamParser::remainingLiteralSize() +{ + return m_literalSize; +} + +QByteArray ImapStreamParser::readLiteralPart() +{ + static qint64 maxLiteralPartSize = 4096; + int size = qMin( maxLiteralPartSize, m_literalSize ); + + if ( !waitForMoreData( m_data.length() < m_position + size ) ) { + throw ImapParserException( "Unable to read more data" ); + } + + if ( m_data.length() < m_position + size ) { // Still not enough data + // Take what's already there + size = m_data.length() - m_position; + } + + QByteArray result = m_data.mid( m_position, size ); + m_position += size; + m_literalSize -= size; + Q_ASSERT( m_literalSize >= 0 ); + if ( !m_peeking ) { + m_data = m_data.right( m_data.size() - m_position ); + m_position = 0; + } + return result; +} + +bool ImapStreamParser::hasSequenceSet() +{ + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + int savedPos = m_position; + stripLeadingSpaces(); + int pos = m_position; + m_position = savedPos; + + if ( m_data[pos] == '*' || m_data[pos] == ':'|| isdigit( m_data[pos] ) ) { + return true; + } + + return false; +} + +ImapSet ImapStreamParser::readSequenceSet() +{ + ImapSet result; + if ( !waitForMoreData( m_data.length() == 0 ) ) { + throw ImapParserException( "Unable to read more data" ); + } + stripLeadingSpaces(); + qint64 value = -1, lower = -1, upper = -1; + Q_FOREVER { + if ( !waitForMoreData( m_data.length() <= m_position ) ) { + upper = value; + if ( lower < 0 ) { + lower = value; + } + if ( lower >= 0 && upper >= 0 ) { + result.add( ImapInterval( lower, upper ) ); + } + return result; + } + + if ( m_data[m_position] == '*' ) { + value = 0; + } else if ( m_data[m_position] == ':' ) { + lower = value; + } else if ( isdigit( m_data[m_position] ) ) { + bool ok = false; + value = readNumber( &ok ); + Q_ASSERT( ok ); // TODO handle error + --m_position; + } else { + upper = value; + if ( lower < 0 ) { + lower = value; + } + result.add( ImapInterval( lower, upper ) ); + lower = -1; + upper = -1; + value = -1; + if ( m_data[m_position] != ',' ) { + return result; + } + } + ++m_position; + } +} + +bool ImapStreamParser::hasList() +{ + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + int savedPos = m_position; + stripLeadingSpaces(); + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + int pos = m_position; + m_position = savedPos; + if ( m_data[pos] == '(' ) { + return true; + } + + return false; +} + +void ImapStreamParser::beginList() +{ + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + stripLeadingSpaces(); + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + if ( m_data[m_position] != '(' ) { + throw ImapParserException( "Stream not at a beginning of a list" ); + } + ++m_position; + return; +} + +bool ImapStreamParser::atListEnd() +{ + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + int savedPos = m_position; + stripLeadingSpaces(); + int pos = m_position; + m_position = savedPos; + if ( m_data[pos] == ')' ) { + m_position = pos + 1; + return true; + } + + return false; +} + +QList ImapStreamParser::readParenthesizedList() +{ + QList result; + if ( !waitForMoreData( m_data.length() <= m_position ) ) { + throw ImapParserException( "Unable to read more data" ); + } + + stripLeadingSpaces(); + if ( m_data[m_position] != '(' ) { + return result; //no list found + } + + bool concatToLast = false; + int count = 0; + int sublistbegin = m_position; + int i = m_position + 1; + Q_FOREVER { + if ( !waitForMoreData( m_data.length() <= i ) ) { + m_position = i; + throw ImapParserException( "Unable to read more data" ); + } + if ( m_data[i] == '(' ) { + ++count; + if ( count == 1 ) { + sublistbegin = i; + } + ++i; + continue; + } + if ( m_data[i] == ')' ) { + if ( count <= 0 ) { + m_position = i + 1; + return result; + } + if ( count == 1 ) { + result.append( m_data.mid( sublistbegin, i - sublistbegin + 1 ) ); + } + --count; + ++i; + continue; + } + if ( m_data[i] == ' ' ) { + ++i; + continue; + } + if ( m_data.at( i ) == '"' ) { + if ( count > 0 ) { + m_position = i; + parseQuotedString(); + i = m_position; + continue; + } + } + if ( m_data[i] == '[' ) { + concatToLast = true; + result.last() += '['; + ++i; + continue; + } + if ( m_data[i] == ']' ) { + concatToLast = false; + result.last() += ']'; + ++i; + continue; + } + if ( count == 0 ) { + m_position = i; + QByteArray ba; + if ( hasLiteral() ) { + while ( !atLiteralEnd() ) { + ba += readLiteralPart(); + } + } else { + ba = readString(); + } + + // We might sometime get some unwanted CRLF, but we're still not at the end + // of the list, would make further string reads fail so eat the CRLFs. + while ( ( m_position < m_data.size() ) && + ( m_data[m_position]=='\r' || m_data[m_position]=='\n' ) ) { + m_position++; + } + + i = m_position - 1; + if ( concatToLast ) { + result.last() += ba; + } else { + result.append( ba ); + } + } + ++i; + } + + throw ImapParserException( "Something went very very wrong!" ); +} + +QByteRef ImapStreamParser::readChar() +{ + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + m_position++; + return m_data[m_position - 1]; +} + +QDateTime ImapStreamParser::readDateTime() +{ + // 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 savedPos = m_position; + if ( !waitForMoreData( m_data.length() == 0 ) ) { + throw ImapParserException( "Unable to read more data" ); + } + stripLeadingSpaces(); + + bool quoted = false; + if ( m_data[m_position] == '"' ) { + quoted = true; + ++m_position; + + if ( m_data.length() <= m_position + 26 ) { + m_position = savedPos; + return QDateTime(); + } + } else { + if ( m_data.length() < m_position + 26 ) { + m_position = savedPos; + return QDateTime(); + } + } + + bool ok = true; + const int day = ( m_data[m_position] == ' ' ? m_data[m_position + 1] - '0' // single digit day + : m_data.mid( m_position, 2 ).toInt( &ok ) ); + if ( !ok ) { + m_position = savedPos; + return QDateTime(); + } + m_position += 3; + const QByteArray shortMonthNames( "janfebmaraprmayjunjulaugsepoctnovdec" ); + int month = shortMonthNames.indexOf( m_data.mid( m_position, 3 ).toLower() ); + if ( month == -1 ) { + m_position = savedPos; + return QDateTime(); + } + month = month / 3 + 1; + m_position += 4; + const int year = m_data.mid( m_position, 4 ).toInt( &ok ); + if ( !ok ) { + m_position = savedPos; + return QDateTime(); + } + m_position += 5; + const int hours = m_data.mid( m_position, 2 ).toInt( &ok ); + if ( !ok ) { + m_position = savedPos; + return QDateTime(); + } + m_position += 3; + const int minutes = m_data.mid( m_position, 2 ).toInt( &ok ); + if ( !ok ) { + m_position = savedPos; + return QDateTime(); + } + m_position += 3; + const int seconds = m_data.mid( m_position, 2 ).toInt( &ok ); + if ( !ok ) { + m_position = savedPos; + return QDateTime(); + } + m_position += 4; + const int tzhh = m_data.mid( m_position, 2 ).toInt( &ok ); + if ( !ok ) { + m_position = savedPos; + return QDateTime(); + } + m_position += 2; + const int tzmm = m_data.mid( m_position, 2 ).toInt( &ok ); + if ( !ok ) { + m_position = savedPos; + return QDateTime(); + } + int tzsecs = tzhh * 60 * 60 + tzmm * 60; + if ( m_data[m_position - 3] == '-' ) { + tzsecs = -tzsecs; + } + const QDate date( year, month, day ); + const QTime time( hours, minutes, seconds ); + QDateTime dateTime; + dateTime = QDateTime( date, time, Qt::UTC ); + if ( !dateTime.isValid() ) { + m_position = savedPos; + return QDateTime(); + } + dateTime = dateTime.addSecs( -tzsecs ); + + m_position += 2; + if ( m_data.length() <= m_position || !quoted ) { + return dateTime; + } + if ( m_data[m_position] == '"' ) { + ++m_position; + } + return dateTime; +} + +bool ImapStreamParser::hasDateTime() +{ + int savedPos = m_position; + QDateTime dateTime = readDateTime(); + m_position = savedPos; + return !dateTime.isNull(); +} + +QByteArray ImapStreamParser::parseQuotedString() +{ + QByteArray result; + if ( !waitForMoreData( m_data.length() == 0 ) ) { + throw ImapParserException( "Unable to read more data" ); + } + stripLeadingSpaces(); + int end = m_position; + result.clear(); + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + + bool foundSlash = false; + // quoted string + if ( m_data[m_position] == '"' ) { + ++m_position; + int i = m_position; + Q_FOREVER { + if ( !waitForMoreData( m_data.length() <= i ) ) { + m_position = i; + throw ImapParserException( "Unable to read more data" ); + } + + if ( foundSlash ) { + foundSlash = false; + if ( m_data[i] == 'r' ) { + result += '\r'; + } else if ( m_data[i] == 'n' ) { + result += '\n'; + } else if ( m_data[i] == '\\' ) { + result += '\\'; + } else if ( m_data[i] == '\"' ) { + result += '\"'; + } else { + throw ImapParserException( "Unexpected '\\' in quoted string" ); + } + ++i; + continue; + } + + if ( m_data[i] == '\\' ) { + foundSlash = true; + ++i; + continue; + } + + if ( m_data[i] == '"' ) { + end = i + 1; // skip the '"' + break; + } + + result += m_data[i]; + + ++i; + } + } + + // unquoted string + else { + bool reachedInputEnd = true; + int i = m_position; + Q_FOREVER { + if ( !waitForMoreData( m_data.length() <= i ) ) { + m_position = i; + throw ImapParserException( "Unable to read more data" ); + } + // unlike in the copy in KIMAP we do not want to consider [] brackets as separators, breaks payload version parsing + // if that ever gets fixed we can re-add them here, see svn revision 937879 + if ( m_data[i] == ' ' || m_data[i] == '(' || m_data[i] == ')' || m_data[i] == '\n' || m_data[i] == '\r' || m_data[i] == '"' ) { + end = i; + reachedInputEnd = false; + break; + } + if ( m_data[i] == '\\' ) { + foundSlash = true; + } + i++; + } + if ( reachedInputEnd ) { //FIXME: how can it get here? + end = m_data.length(); + } + + result = m_data.mid( m_position, end - m_position ); + + // transform unquoted NIL + if ( result == "NIL" ) { + result.clear(); + } + + // strip quotes + if ( foundSlash ) { + while ( result.contains( "\\\"" ) ) { + result.replace( "\\\"", "\"" ); + } + while ( result.contains( "\\\\" ) ) { + result.replace( "\\\\", "\\" ); + } + } + } + + m_position = end; + return result; +} + +qint64 ImapStreamParser::readNumber( bool *ok ) +{ + qint64 result; + if ( ok ) { + *ok = false; + } + if ( !waitForMoreData( m_data.length() == 0 ) ) { + throw ImapParserException( "Unable to read more data" ); + } + stripLeadingSpaces(); + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + if ( m_position >= m_data.length() ) { + throw ImapParserException( "Unable to read more data" ); + } + int i = m_position; + Q_FOREVER { + if ( !waitForMoreData( m_data.length() <= i ) ) { + m_position = i; + throw ImapParserException( "Unable to read more data" ); + } + if ( !isdigit( m_data.at( i ) ) ) { + break; + } + ++i; + } + const QByteArray tmp = m_data.mid( m_position, i - m_position ); + bool success = false; + result = tmp.toLongLong( &success ); + if ( ok ) { + *ok = success; + } else if ( !success ) { + throw ImapParserException( "Unable to parse number" ); + } + m_position = i; + return result; +} + +void ImapStreamParser::stripLeadingSpaces() +{ + for ( int i = m_position; i < m_data.length(); ++i ) { + if ( m_data[i] != ' ' ) { + m_position = i; + return; + } + } + m_position = m_data.length(); +} + +bool ImapStreamParser::waitForMoreData( bool wait ) +{ + if ( wait ) { + if ( m_socket->bytesAvailable() > 0 || + m_socket->waitForReadyRead( m_timeout ) ) { + m_data.append( m_socket->readAll() ); + } else { + return false; + } + } + return true; +} + +void ImapStreamParser::setData( const QByteArray &data ) +{ + m_data = data; +} + +QByteArray ImapStreamParser::readRemainingData() +{ + return m_data.mid( m_position ); +} + +bool ImapStreamParser::atCommandEnd() +{ + if ( !waitForMoreData( m_position >= m_data.length() ) ) { + throw ImapParserException( "Unable to read more data" ); + } + int savedPos = m_position; + stripLeadingSpaces(); + if ( m_data[m_position] == '\n' || m_data[m_position] == '\r' ) { + if ( m_position < m_data.length() && m_data[m_position] == '\r' ) { + ++m_position; + } + if ( m_position < m_data.length() && m_data[m_position] == '\n' ) { + ++m_position; + } + // We'd better empty m_data from time to time before it grows out of control + if ( !m_peeking ) { + m_data = m_data.right( m_data.size() - m_position ); + m_position = 0; + } + return true; //command end + } + m_position = savedPos; + return false; //something else +} + +QByteArray ImapStreamParser::readUntilCommandEnd() +{ + QByteArray result; + int i = m_position; + int paranthesisBalance = 0; + bool inQuotedString = false; + Q_FOREVER { + if ( !waitForMoreData( m_data.length() <= i ) ) { + m_position = i; + throw ImapParserException( "Unable to read more data" ); + } + if ( !inQuotedString && m_data[i] == '{' ) { + m_position = i - 1; + hasLiteral(); //init literal size + result.append( m_data.mid( i - 1, m_position - i + 1 ) ); + while ( !atLiteralEnd() ) { + result.append( readLiteralPart() ); + } + // Read the last character part and possible crlf + i = m_position; + do { + result.append( m_data[i] ); + ++i; + } while (m_data[i] == ' ' || m_data[i] == '\n' || m_data[i] == '\r'); + } + + if ( !inQuotedString && m_data[i] == '(' ) { + paranthesisBalance++; + } + if ( !inQuotedString && m_data[i] == ')' ) { + paranthesisBalance--; + } + result.append( m_data[i] ); + + if ( m_data[i] == '"' ) { + if ( m_data[i - 1] != '\\' ) { + inQuotedString = !inQuotedString; + } + } + + if ( ( i == m_data.length() && paranthesisBalance == 0 ) || m_data[i] == '\n' || m_data[i] == '\r' ) { + // Make sure we return \r\n and not just \r + if (m_data[i] == '\r' && m_data[i + 1] == '\n') { + ++i; + result.append( '\n' ); + } + break; //command end + } + ++i; + } + m_position = i + 1; + // We'd better empty m_data from time to time before it grows out of control + if ( !m_peeking ) { + m_data = m_data.right( m_data.size() - m_position ); + m_position = 0; + } + return result; +} + +void ImapStreamParser::skipCurrentCommand() +{ + int i = m_position; + Q_FOREVER { + if ( !waitForMoreData( m_data.length() <= i ) ) { + m_position = i; + throw ImapParserException( "Unable to read more data" ); + } + if ( m_data[i] == '\n' || m_data[i] == '\r' ) { + break; //command end + } + ++i; + } + m_position = i + 1; + // We'd better empty m_data from time to time before it grows out of control + if ( !m_peeking ) { + m_data = m_data.right( m_data.size() - m_position ); + m_position = 0; + } +} + +void ImapStreamParser::sendContinuationResponse( qint64 size ) +{ + const QByteArray block = "+ Ready for literal data (expecting " + + QByteArray::number( size ) + " bytes)\r\n"; + m_socket->write( block ); + m_socket->waitForBytesWritten( m_timeout ); + + Tracer::self()->connectionOutput( m_tracerId, block ); +} + +void ImapStreamParser::insertData( const QByteArray &data ) +{ + m_data = m_data.insert( m_position, data ); +} + +void ImapStreamParser::appendData( const QByteArray &data ) +{ + m_data = m_data + data; +} + +void ImapStreamParser::setTracerIdentifier( const QString &id ) +{ + m_tracerId = id; +} diff --git a/akonadi/server/src/imapstreamparser.h b/akonadi/server/src/imapstreamparser.h new file mode 100644 index 00000000..706ca71e --- /dev/null +++ b/akonadi/server/src/imapstreamparser.h @@ -0,0 +1,286 @@ +/* + Copyright (c) 2006 - 2007 Volker Krause + Copyright (c) 2009 Andras Mantia + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see 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_IMAPSTREAMPARSER_P_H +#define AKONADI_IMAPSTREAMPARSER_P_H + +#include "imapset_p.h" + +#include +#include +#include +#include +#include + +#include "exception.h" + +AKONADI_EXCEPTION_MAKE_INSTANCE( ImapParserException ); + +class QIODevice; + +class ImapStreamParserTest; + +namespace Akonadi { +namespace Server { + +/** + Parser for IMAP messages that operates on a local socket stream. +*/ +class ImapStreamParser +{ + friend class ::ImapStreamParserTest; + + public: + /** + * Construct the parser. + * @param socket the local socket to work with. + */ + ImapStreamParser( QIODevice *socket ); + + /** + * Destructor. + */ + ~ImapStreamParser(); + + /** + * Sets how long the parser should wait for socket to provide more data before + * aborting with error. + * + * Default value is 30 seconds. This method is used mainly to speed up + * unittests. + */ + void setWaitTimeout(int msecs); + + /** + * Get a string from the message. If the upcoming data is not a quoted string, unquoted string or a literal, + * the behavior is undefined. Use @ref hasString to be sure a string comes. This call might block. + * @return the next string from the message as an utf8 string + */ + QString readUtf8String(); + + /** + * Same as above, but without decoding it to utf8. + * @return the next string from the message + */ + QByteArray readString(); + + /** + * Same as above, but without actually moving the stream position forward. + * @return the next string from the stream, without modifying the stream. + */ + QByteArray peekString(); + + /** + * Get he next IMAP sequence set. If the upcoming data is not an IMAP sequence set, + * the behavior is undefined. Use @ref hasSequenceSet to be sure a sequence set comes. This call might block. + * @return the next IMAP sequence set. + */ + ImapSet readSequenceSet(); + + /** + * Get he next parenthesized list. If the upcoming data is not a parenthesized list, + * the behavior is undefined. Use @ref hasList to be sure a string comes. This call might block. + * @return the next parenthesized list. + */ + QList readParenthesizedList(); + + /** + * Read a single character. This call might block. + * @return the read character + */ + QByteRef readChar(); + + /** + * Get the next data as a number. This call might block. + * @param ok true if the data found was a number + * @return the number + */ + qint64 readNumber( bool *ok = 0 ); + + /** + * Check if the next data is a string or not. This call might block. + * @return true if a string follows + */ + bool hasString(); + + /** + * Check if the next data is a literal data or not. If a literal is found, the + * internal position pointer is set to the beginning of the literal data. + * This call might block. + * @return true if a literal follows + */ + bool hasLiteral( bool requestData = true); + + /** + * Read the next literal sequence. This might or might not be the full data. Example code to read a literal would be: + * @code + * ImapStreamParser parser; + * ... + * if (parser.hasLiteral()) + * { + * while (!parser.atLiteralEnd()) + * { + * QByteArray data = parser.readLiteralPart(); + * // do something with the data + * } + * } + * @endcode + * + * This call might block. + * + * @return part of a literal data + */ + QByteArray readLiteralPart(); + + /** + * Check if the literal data end was reached. See @ref hasLiteral and @ref readLiteralPart . + * @return true if the literal was completely read. + */ + bool atLiteralEnd() const; + + /** + * Get the amount of data that needs to be read for the last literal. If this is called right after hasLiteral, the actual size of the literal data + * is returned. + * @return the remaining literal size + */ + qint64 remainingLiteralSize(); + + /** + * Check if the next data is an IMAP sequence set. This call might block. + * @return true if an IMAP sequence set comes. + */ + bool hasSequenceSet(); + + /** + * Check if the next data is a parenthesized list. This call might block. + * @return true if a parenthesized list comes. + */ + bool hasList(); + + /** + * Begin reading a parenthesized list. This call might block. + * This call will throw an exception if the parser is not at a beginning of a list, + * that is hasList() returns false. + * @see hasList(), atListEnd + */ + void beginList(); + + /** + * Check if the next data is a parenthesized list end. This call might block. + * @return true if a parenthesized list end. In this case the closing parenthesis + * is read from the stream. + */ + bool atListEnd(); + + /** + * Read a date/time. + * @return the date and time or a null QDateTime, if no valid date/time was found + */ + QDateTime readDateTime(); + + /** + * Check if the next element in the data stream is a date or not. + * @return true, if a valid date follows + */ + bool hasDateTime(); + + /** + * Check if the command end was reached + * @return true if the end of command is reached + */ + bool atCommandEnd(); + + /** + * Return everything that remained from the command, including not yet + * requested literal parts. + * @return the remaining command data + * @see skipCurrentCommand + */ + QByteArray readUntilCommandEnd(); + + /** + * This reads until the end of the already sent command and does not + * request not yet sent literal parts. + * @see readUntilCommandEnd + */ + void skipCurrentCommand(); + + /** + * Return all the data that was read from the socket, but not processed yet. + * @return the remaining unprocessed data + */ + QByteArray readRemainingData(); + + void setData( const QByteArray &data ); + + /** + * Inserts some data back into the parse buffer at the current position. + * @param data data to be inserted + */ + void insertData( const QByteArray &data ); + + /** + * Appends some data to the end of the parse buffer. + * @param data data to be appended + */ + void appendData( const QByteArray &data ); + + /** + * Skips everything until the first character that isn't a space. + */ + void stripLeadingSpaces(); + + /** + * Set the identification used for Tracer calls. + */ + void setTracerIdentifier( const QString &id ); + + /** + * Inform the client to send more literal data. + * @param size size of the requested literal in bytes + */ + void sendContinuationResponse( qint64 size ); + + private: + QByteArray parseQuotedString(); + + /** + * If the condition is true, wait for more data to be available from the socket. + * If no data comes after a timeout (30000ms), it aborts and returns false. + * @param wait the condition + * @return true if more data is available + */ + bool waitForMoreData( bool wait ); + + QIODevice *m_socket; + QByteArray m_data; + QByteArray m_tag; + QString m_tracerId; + int m_position; + qint64 m_literalSize; + bool m_peeking; + int m_timeout; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/intervalcheck.cpp b/akonadi/server/src/intervalcheck.cpp new file mode 100644 index 00000000..34d70b0d --- /dev/null +++ b/akonadi/server/src/intervalcheck.cpp @@ -0,0 +1,95 @@ +/* + 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 int MINIMUM_AUTOSYNC_INTERVAL = 5; // minutes +static int MINIMUM_COLTREESYNC_INTERVAL = 5; // minutes + +IntervalCheck::IntervalCheck( QObject *parent ) + : CollectionScheduler( parent ) +{ +} + +IntervalCheck::~ IntervalCheck() +{ +} + +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/akonadi/server/src/intervalcheck.h b/akonadi/server/src/intervalcheck.h new file mode 100644 index 00000000..e362bab1 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/main.cpp b/akonadi/server/src/main.cpp new file mode 100644 index 00000000..030241a8 --- /dev/null +++ b/akonadi/server/src/main.cpp @@ -0,0 +1,86 @@ +/*************************************************************************** + * 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 "akapplication.h" +#include "akdbus.h" +#include "akdebug.h" +#include "akcrash.h" + +#include "protocol_p.h" + +#include +#include +#include +#include +#include + +#include + +#ifdef QT_STATICPLUGIN +#include + +Q_IMPORT_PLUGIN( qsqlite3 ) +#endif + +namespace po = boost::program_options; + +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( QLatin1String( "Akonadi Server\nDo not run manually, use 'akonadictl' instead to start/stop Akonadi." ) ); + +#if !defined(NDEBUG) + po::options_description debugOptions( "Debug options (use with care)" ); + debugOptions.add_options() + ( "start-without-control", "Allow to start the Akonadi server even without the Akonadi control process being available" ); + app.addCommandLineOptions( debugOptions ); +#endif + + app.parseCommandLine(); + + if ( !app.commandLineArguments().count( "start-without-control" ) && + !QDBusConnection::sessionBus().interface()->isServiceRegistered( AkDBus::serviceName( AkDBus::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(), SLOT(init())); + AkonadiCrash::setShutdownMethod( shutdownHandler ); + + const int result = app.exec(); + + Akonadi::Server::AkonadiServer::instance()->quit(); + + Q_CLEANUP_RESOURCE( akonadidb ); + + return result; +} diff --git a/akonadi/server/src/nepomuk/dbusoperators.cpp b/akonadi/server/src/nepomuk/dbusoperators.cpp new file mode 100644 index 00000000..ab4ed3bc --- /dev/null +++ b/akonadi/server/src/nepomuk/dbusoperators.cpp @@ -0,0 +1,160 @@ +/* + Copyright (c) 2008-2009 Sebastian Trueg + + 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) 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 6 of version 3 of the license. + + This library is distributed in the hope that 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 "dbusoperators.h" + +#include + +#include +#include + +void Nepomuk::Query::registerDBusTypes() +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType >(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); +} + +QDBusArgument &operator<<( QDBusArgument &arg, const Nepomuk::Query::Result &result ) +{ + // + // Signature: (sda{s(isss)}a{s(isss)}s) + // + + arg.beginStructure(); + + // resource URI and score + arg << QString::fromAscii( result.resourceUri().toEncoded() ) << result.score(); + + // request properties + arg.beginMap( QVariant::String, qMetaTypeId() ); + QHash rp = result.requestProperties(); + for ( QHash::const_iterator it = rp.constBegin(); it != rp.constEnd(); ++it ) { + arg.beginMapEntry(); + arg << QString::fromAscii( it.key().toEncoded() ) << it.value(); + arg.endMapEntry(); + } + arg.endMap(); + + // additional bindings + arg.beginMap( QVariant::String, qMetaTypeId() ); + const Soprano::BindingSet additionalBindings; // = result.additionalBindings(); + Q_FOREACH ( const QString &binding, additionalBindings.bindingNames() ) { + arg.beginMapEntry(); + arg << binding << additionalBindings[binding]; + arg.endMapEntry(); + } + arg.endMap(); + + // full text search excerpt + arg << QString();//<< result.excerpt(); + + arg.endStructure(); + + return arg; +} + +const QDBusArgument &operator>>( const QDBusArgument &arg, Nepomuk::Query::Result &result ) +{ + // + // Signature: (sda{s(isss)}s) + // + + arg.beginStructure(); + QString uri; + double score = 0.0; + + arg >> uri >> score; + result = Nepomuk::Query::Result( QUrl::fromEncoded( uri.toAscii() ), score ); + + arg.beginMap(); + while ( !arg.atEnd() ) { + QString rs; + Soprano::Node node; + arg.beginMapEntry(); + arg >> rs >> node; + arg.endMapEntry(); + result.addRequestProperty( QUrl::fromEncoded( rs.toAscii() ), node ); + } + arg.endMap(); + + Soprano::BindingSet additionalBindings; + arg.beginMap(); + while ( !arg.atEnd() ) { + QString binding; + Soprano::Node node; + arg.beginMapEntry(); + arg >> binding >> node; + arg.endMapEntry(); + additionalBindings.insert( binding, node ); + } + arg.endMap(); + + QString excerpt; + arg >> excerpt; +// result.setExcerpt( excerpt ); + + arg.endStructure(); + +// result.setAdditionalBindings( additionalBindings ); + + return arg; +} + +QDBusArgument &operator<<( QDBusArgument &arg, const Soprano::Node &node ) +{ + arg.beginStructure(); + arg << ( int )node.type(); + if ( node.type() == Soprano::Node::ResourceNode ) { + arg << QString::fromAscii( node.uri().toEncoded() ); + } else { + arg << node.toString(); + } + arg << node.language() << node.dataType().toString(); + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>( const QDBusArgument &arg, Soprano::Node &node ) +{ + // + // Signature: (isss) + // + arg.beginStructure(); + int type; + QString value, language, dataTypeUri; + arg >> type >> value >> language >> dataTypeUri; + if ( type == Soprano::Node::LiteralNode ) { + if ( dataTypeUri.isEmpty() ) { + node = Soprano::Node( Soprano::LiteralValue::createPlainLiteral( value, language ) ); + } else { + node = Soprano::Node( Soprano::LiteralValue::fromString( value, QUrl::fromEncoded( dataTypeUri.toAscii() ) ) ); + } + } else if ( type == Soprano::Node::ResourceNode ) { + node = Soprano::Node( QUrl::fromEncoded( value.toAscii() ) ); + } else if ( type == Soprano::Node::BlankNode ) { + node = Soprano::Node( value ); + } else { + node = Soprano::Node(); + } + arg.endStructure(); + return arg; +} diff --git a/akonadi/server/src/nepomuk/dbusoperators.h b/akonadi/server/src/nepomuk/dbusoperators.h new file mode 100644 index 00000000..e2384b68 --- /dev/null +++ b/akonadi/server/src/nepomuk/dbusoperators.h @@ -0,0 +1,53 @@ +/* + Copyright (c) 2008-2009 Sebastian Trueg + + 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) 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 6 of version 3 of the license. + + This library is distributed in the hope that 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 _NEPOMUK_SEARCH_DBUS_OPERATORS_H_ +#define _NEPOMUK_SEARCH_DBUS_OPERATORS_H_ + +#include + +#include "result.h" +#include "nepomukquery_export.h" + +Q_DECLARE_METATYPE( Nepomuk::Query::Result ) +Q_DECLARE_METATYPE( Soprano::Node ) +Q_DECLARE_METATYPE( QList ) +typedef QHash RequestPropertyMapDBus; +Q_DECLARE_METATYPE( RequestPropertyMapDBus ) + +namespace Nepomuk { + namespace Query { + /** + * Register the DBus types necessary for communication with the Nepomuk + * query service. This method is only made public for the query service + * itself which links to this library, too. + */ + NEPOMUKQUERY_EXPORT void registerDBusTypes(); + } +} + +// We export the non-public operators so that we do not need duplicated code in kdebase +NEPOMUKQUERY_EXPORT QDBusArgument &operator<<( QDBusArgument &arg, const Soprano::Node &node ); +NEPOMUKQUERY_EXPORT const QDBusArgument &operator>>( const QDBusArgument &arg, Soprano::Node &node ); + +NEPOMUKQUERY_EXPORT QDBusArgument &operator<<( QDBusArgument &arg, const Nepomuk::Query::Result &result ); +NEPOMUKQUERY_EXPORT const QDBusArgument &operator>>( const QDBusArgument &arg, Nepomuk::Query::Result &result ); + +#endif diff --git a/akonadi/server/src/nepomuk/nepomukquery_export.h b/akonadi/server/src/nepomuk/nepomukquery_export.h new file mode 100644 index 00000000..79ec2cf6 --- /dev/null +++ b/akonadi/server/src/nepomuk/nepomukquery_export.h @@ -0,0 +1,8 @@ +#ifndef NEPOMUKQUERY_EXPORT_H +#define NEPOMUKQUERY_EXPORT_H + +#ifndef NEPOMUKQUERY_EXPORT +#define NEPOMUKQUERY_EXPORT +#endif + +#endif diff --git a/akonadi/server/src/nepomuk/org.kde.nepomuk.Query.xml b/akonadi/server/src/nepomuk/org.kde.nepomuk.Query.xml new file mode 100644 index 00000000..ab1feddd --- /dev/null +++ b/akonadi/server/src/nepomuk/org.kde.nepomuk.Query.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/server/src/nepomuk/org.kde.nepomuk.QueryService.xml b/akonadi/server/src/nepomuk/org.kde.nepomuk.QueryService.xml new file mode 100644 index 00000000..ab7ad92f --- /dev/null +++ b/akonadi/server/src/nepomuk/org.kde.nepomuk.QueryService.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/server/src/nepomuk/queryserviceclient.cpp b/akonadi/server/src/nepomuk/queryserviceclient.cpp new file mode 100644 index 00000000..b27a751b --- /dev/null +++ b/akonadi/server/src/nepomuk/queryserviceclient.cpp @@ -0,0 +1,223 @@ +/* + Copyright (c) 2008-2009 Sebastian Trueg + + 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) 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 6 of version 3 of the license. + + This library is distributed in the hope that 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 "queryserviceclient.h" +#include +#include "dbusoperators.h" +#include "result.h" +#include "queryserviceinterface.h" +#include "queryinterface.h" +#include "dbusconnectionpool.h" + +#include +#include +#include +#include + +#include +#include + +class Nepomuk::Query::QueryServiceClient::Private +{ +public: + Private() + : queryServiceInterface( 0 ), + queryInterface( 0 ), + dbusConnection( Akonadi::Server::DBusConnectionPool::threadConnection() ), + m_queryActive( false ), + loop( 0 ) { + } + + void _k_finishedListing(); + void _k_handleQueryReply( QDBusPendingCallWatcher *watcher ); + void _k_serviceRegistered( const QString &service ); + void _k_serviceUnregistered( const QString &service ); + + org::kde::nepomuk::QueryService *queryServiceInterface; + org::kde::nepomuk::Query *queryInterface; + QDBusServiceWatcher *queryServiceWatcher; + + QueryServiceClient *q; + + QPointer m_pendingCallWatcher; + + QDBusConnection dbusConnection; + + bool m_queryActive; + QEventLoop *loop; + QString m_errorMessage; +}; + +void Nepomuk::Query::QueryServiceClient::Private::_k_finishedListing() +{ + m_queryActive = false; + Q_EMIT q->finishedListing(); + if ( loop ) { + q->close(); + } +} + +void Nepomuk::Query::QueryServiceClient::Private::_k_handleQueryReply( QDBusPendingCallWatcher *watcher ) +{ + QDBusPendingReply reply = *watcher; + if ( reply.isError() ) { + akDebug() << reply.error(); + m_errorMessage = reply.error().message(); + m_queryActive = false; + Q_EMIT q->error( m_errorMessage ); + if ( loop ) { + loop->exit(); + } + } else { + queryInterface = new org::kde::nepomuk::Query( queryServiceInterface->service(), + reply.value().path(), + dbusConnection ); + connect( queryInterface, SIGNAL(newEntries(QList)), + q, SIGNAL(newEntries(QList)) ); + connect( queryInterface, SIGNAL(entriesRemoved(QList)), + q, SIGNAL(entriesRemoved(QList)) ); + connect( queryInterface, SIGNAL(finishedListing()), + q, SLOT(_k_finishedListing()) ); + // run the listing async in case the event loop below is the only one we have + // and we need it to handle the signals and list returns results immediately + QTimer::singleShot( 0, queryInterface, SLOT(list()) ); + } + + delete watcher; +} + +void Nepomuk::Query::QueryServiceClient::Private::_k_serviceRegistered( const QString &service ) +{ + if ( service == QLatin1String( "org.kde.nepomuk.services.nepomukqueryservice" ) ) { + akDebug() << "NEP: nepomukqueryservice has registered again"; + delete queryServiceInterface; + + queryServiceInterface = new org::kde::nepomuk::QueryService( QLatin1String( "org.kde.nepomuk.services.nepomukqueryservice" ), + QLatin1String( "/nepomukqueryservice" ), + dbusConnection ); + Q_EMIT q->serviceAvailabilityChanged( true ); + } +} + +void Nepomuk::Query::QueryServiceClient::Private::_k_serviceUnregistered( const QString &service ) +{ + if ( service == QLatin1String( "org.kde.nepomuk.services.nepomukqueryservice" ) ) { + q->close(); + Q_EMIT q->serviceAvailabilityChanged( false ); + } +} + +Nepomuk::Query::QueryServiceClient::QueryServiceClient( QObject *parent ) + : QObject( parent ) + , d( new Private() ) +{ + d->q = this; + + Nepomuk::Query::registerDBusTypes(); + + // we use our own connection to be thread-safe + d->queryServiceInterface = new org::kde::nepomuk::QueryService( QLatin1String( "org.kde.nepomuk.services.nepomukqueryservice" ), + QLatin1String( "/nepomukqueryservice" ), + d->dbusConnection ); + d->queryServiceWatcher = new QDBusServiceWatcher( QLatin1String( "org.kde.nepomuk.services.nepomukqueryservice" ), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForOwnerChange, + this ); + connect( d->queryServiceWatcher, SIGNAL(serviceRegistered(QString)), this, SLOT(_k_serviceRegistered(QString)) ); + connect( d->queryServiceWatcher, SIGNAL(serviceUnregistered(QString)), this, SLOT(_k_serviceUnregistered(QString)) ); +} + +Nepomuk::Query::QueryServiceClient::~QueryServiceClient() +{ + close(); + delete d->queryServiceInterface; + delete d; +} + +bool Nepomuk::Query::QueryServiceClient::query( const QString &query, const QHash &encodedRps ) +{ + close(); + + if ( d->queryServiceInterface->isValid() ) { + d->m_queryActive = true; + d->m_pendingCallWatcher = new QDBusPendingCallWatcher( d->queryServiceInterface->asyncCall( QLatin1String( "sparqlQuery" ), + query, + QVariant::fromValue( encodedRps ) ), + this ); + connect( d->m_pendingCallWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), + this, SLOT(_k_handleQueryReply(QDBusPendingCallWatcher*)) ); + return true; + } else { + akDebug() << "Could not contact nepomuk query service."; + return false; + } +} + +bool Nepomuk::Query::QueryServiceClient::blockingQuery( const QString &q, const QHash &encodedRps ) +{ + if ( query( q, encodedRps ) ) { + QEventLoop loop; + d->loop = &loop; + loop.exec(); + d->loop = 0; + close(); + return true; + } else { + return false; + } +} + +void Nepomuk::Query::QueryServiceClient::close() +{ + // drop pending query calls + // TODO: This could lead to dangling queries in the service when close is called before the pending call has returned!!! + // We could also use a stack of pending calls or something like that. + delete d->m_pendingCallWatcher; + + d->m_errorMessage.truncate( 0 ); + + if ( d->queryInterface ) { + akDebug() << Q_FUNC_INFO; + d->queryInterface->close(); + delete d->queryInterface; + d->queryInterface = 0; + d->m_queryActive = false; + if ( d->loop ) { + d->loop->exit(); + } + } +} + +bool Nepomuk::Query::QueryServiceClient::isListingFinished() const +{ + return !d->m_queryActive; +} + +bool Nepomuk::Query::QueryServiceClient::serviceAvailable() +{ + return QDBusConnection::sessionBus().interface()->isServiceRegistered( QLatin1String( "org.kde.nepomuk.services.nepomukqueryservice" ) ); +} + +QString Nepomuk::Query::QueryServiceClient::errorMessage() const +{ + return d->m_errorMessage; +} + +#include "moc_queryserviceclient.cpp" diff --git a/akonadi/server/src/nepomuk/queryserviceclient.h b/akonadi/server/src/nepomuk/queryserviceclient.h new file mode 100644 index 00000000..46ed84df --- /dev/null +++ b/akonadi/server/src/nepomuk/queryserviceclient.h @@ -0,0 +1,185 @@ +/* + Copyright (c) 2008-2010 Sebastian Trueg + + 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) 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 6 of version 3 of the license. + + This library is distributed in the hope that 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 _NEPOMUK_QUERY_SERVICE_CLIENT_H_ +#define _NEPOMUK_QUERY_SERVICE_CLIENT_H_ + +#include +#include + +class QUrl; + +namespace Nepomuk { + namespace Query { + + class Result; + + /** + * \class QueryServiceClient queryserviceclient.h Nepomuk/Query/QueryServiceClient + * + * \brief Convenience frontend to the %Nepomuk Query DBus Service + * + * The QueryServiceClient provides an easy way to access the %Nepomuk Query Service + * without having to deal with any communication details. By default it monitors + * queries for changes. + * + * Usage is simple: Create an instance of the client for each search you want to + * track. One instance may also be reused for subsequent queries if further updates + * of the persistent query are not necessary. + * + * \author Sebastian Trueg + */ + class QueryServiceClient : public QObject + { + Q_OBJECT + + public: + /** + * Create a new QueryServiceClient instance. + */ + QueryServiceClient( QObject *parent = 0 ); + + /** + * Desctructor. Closes the query. + */ + ~QueryServiceClient(); + + /** + * \brief Check if the Nepomuk query service is running. + * \return \p true if the Nepomuk query service is running and could + * be contacted via DBus, \p false otherwise + */ + static bool serviceAvailable(); + + public Q_SLOTS: + /** + * Start a query using the Nepomuk query service. + * + * Results will be reported via newEntries. All results + * have been reported once finishedListing has been emitted. + * + * \param query the query to perform. + * + * \return \p true if the query service was found and the query + * was started. \p false otherwise. + * + * \sa QueryParser + */ + bool query( const QString &query, const QHash &encodedRps = ( QHash() ) ); + + /** + * Start a query using the Nepomuk query service. + * + * Results will be reported as with query(const QString&) + * but a local event loop will be started to block the method + * call until all results have been listed. + * + * The client will be closed after the initial listing. Thus, + * changes to results will not be reported as it is the case + * with the non-blocking methods. + * + * \param query the query to perform. + * + * \return \p true if the query service was found and the query + * was started. \p false otherwise. + * + * \sa query(const QString&), close() + */ + bool blockingQuery( const QString &query, const QHash &encodedRps = ( QHash() ) ); + + /** + * Close the client, thus stop to monitor the query + * for changes. Without closing the client it will continue + * signalling changes to the results. + * + * This will also make any blockingQuery return immediately. + */ + void close(); + + /** + * \return \p true if all results have been listed (ie. finishedListing() has + * been emitted), close() has been called, or no query was started. + * + * \since 4.6 + */ + bool isListingFinished() const; + + /** + * The last error message which has been emitted via error() or an + * empty string if there was no error. + * + * \since 4.6 + */ + QString errorMessage() const; + + Q_SIGNALS: + /** + * Emitted for new search results. This signal is emitted both + * for the initial listing and for changes to the search. + */ + void newEntries( const QList &entries ); + + /** + * Emitted if the search results changed when monitoring a query. + * \param entries A list of resource URIs identifying the resources + * that dropped out of the query results. + */ + void entriesRemoved( const QList &entries ); + + /** + * Emitted when the initial listing has been finished, ie. if all + * results have been reported via newEntries. If no further updates + * are necessary the client should be closed now. + * + * In case of an error this signal is not emitted. + * + * \sa error() + */ + void finishedListing(); + + /** + * Emitted when an error occurs. This typically happens in case the query + * service is not running or does not respond. No further signals will be + * emitted after this one. + * + * \since 4.6 + */ + void error( const QString &errorMessage ); + + /** + * Emitted when the availability of the query service changes + * + * \since 4.8 + */ + void serviceAvailabilityChanged( bool running ); + + private: + class Private; + Private *const d; + + Q_PRIVATE_SLOT( d, void _k_finishedListing() ) + Q_PRIVATE_SLOT( d, void _k_handleQueryReply( QDBusPendingCallWatcher *) ) + Q_PRIVATE_SLOT( d, void _k_serviceRegistered( const QString &service ) ) + Q_PRIVATE_SLOT( d, void _k_serviceUnregistered( const QString &service ) ) + }; + } +} + +#endif diff --git a/akonadi/server/src/nepomuk/result.cpp b/akonadi/server/src/nepomuk/result.cpp new file mode 100644 index 00000000..e949ee53 --- /dev/null +++ b/akonadi/server/src/nepomuk/result.cpp @@ -0,0 +1,125 @@ +/* + This file is part of the Nepomuk KDE project. + Copyright (C) 2008 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 version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ + +#include "result.h" + +#include +#include + +#include "soprano/node.h" // for qHash( QUrl() ) + +class Nepomuk::Query::Result::Private : public QSharedData +{ +public: + QUrl resource; + double score; + QHash requestProperties; +}; + +Nepomuk::Query::Result::Result() + : d( new Private() ) +{ +} + +Nepomuk::Query::Result::Result( const QUrl &uri, double score ) + : d( new Private() ) +{ + d->resource = uri; + d->score = score; +} + +Nepomuk::Query::Result::Result( const Result &other ) +{ + d = other.d; +} + +Nepomuk::Query::Result::~Result() +{ +} + +Nepomuk::Query::Result &Nepomuk::Query::Result::operator=( const Result &other ) +{ + d = other.d; + return *this; +} + +double Nepomuk::Query::Result::score() const +{ + return d->score; +} + +QUrl Nepomuk::Query::Result::resourceUri() const +{ + return d->resource; +} + +void Nepomuk::Query::Result::setScore( double score ) +{ + d->score = score; +} + +void Nepomuk::Query::Result::addRequestProperty( const QUrl &property, const Soprano::Node &value ) +{ + d->requestProperties[property] = value; +} + +Soprano::Node Nepomuk::Query::Result::operator[]( const QUrl &property ) const +{ + return requestProperty( property ); +} + +Soprano::Node Nepomuk::Query::Result::requestProperty( const QUrl &property ) const +{ + QHash::const_iterator it = d->requestProperties.find( property ); + if ( it != d->requestProperties.end() ) { + return *it; + } else { + return Soprano::Node(); + } +} + +QHash Nepomuk::Query::Result::requestProperties() const +{ + return d->requestProperties; +} + +bool Nepomuk::Query::Result::operator==( const Result &other ) const +{ + if ( d->resource != other.d->resource || + d->score != other.d->score ) { + return false; + } + for ( QHash::const_iterator it = d->requestProperties.constBegin(); + it != d->requestProperties.constEnd(); ++it ) { + QHash::const_iterator it2 = other.d->requestProperties.constFind( it.key() ); + if ( it2 == other.d->requestProperties.constEnd() || + it2.value() != it.value() ) { + return false; + } + } + for ( QHash::const_iterator it = other.d->requestProperties.constBegin(); + it != other.d->requestProperties.constEnd(); ++it ) { + QHash::const_iterator it2 = d->requestProperties.constFind( it.key() ); + if ( it2 == d->requestProperties.constEnd() || + it2.value() != it.value() ) { + return false; + } + } + return true; +} diff --git a/akonadi/server/src/nepomuk/result.h b/akonadi/server/src/nepomuk/result.h new file mode 100644 index 00000000..2422e2b2 --- /dev/null +++ b/akonadi/server/src/nepomuk/result.h @@ -0,0 +1,72 @@ +/* + This file is part of the Nepomuk KDE project. + Copyright (C) 2008 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 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 _NEPOMUK_SEARCH_RESULT_H_ +#define _NEPOMUK_SEARCH_RESULT_H_ + +#include +#include +#include +#include + +#include "soprano/statement.h" + +namespace Nepomuk { + namespace Query { + /** + * \brief A single search result. + * + * A search returns a set of Result object. + * + * \author Sebastian Trueg + */ + class Result + { + public: + Result(); + explicit Result( const QUrl &uri, double score = 0.0 ); + Result( const Result &other ); + ~Result(); + + Result &operator=( const Result &other ); + + double score() const; + QUrl resourceUri() const; + + void setScore( double score ); + + void addRequestProperty( const QUrl &property, const Soprano::Node &value ); + + QHash requestProperties() const; + + Soprano::Node operator[]( const QUrl &property ) const; + Soprano::Node requestProperty( const QUrl &property ) const; + + bool operator==( const Result &other ) const; + + private: + class Private; + QSharedDataPointer d; + }; + } +} + +Q_DECLARE_TYPEINFO( Nepomuk::Query::Result, Q_MOVABLE_TYPE ); + +#endif diff --git a/akonadi/server/src/nepomuksearch.cpp b/akonadi/server/src/nepomuksearch.cpp new file mode 100644 index 00000000..19d56a4b --- /dev/null +++ b/akonadi/server/src/nepomuksearch.cpp @@ -0,0 +1,134 @@ +/* + 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 "nepomuksearch.h" + +#include +#include +#include + +#include "nepomuk/result.h" + +using namespace Akonadi::Server; + +static qint64 resultToId( const Nepomuk::Query::Result &result ) +{ + const Soprano::Node &property = result.requestProperty( QUrl( QLatin1String( "http://akonadi-project.org/ontologies/aneo#akonadiItemId" ) ) ); + if ( !( property.isValid() && property.isLiteral() && property.literal().isString() ) ) { + qWarning() << "Failed to get requested akonadiItemId property"; + qDebug() << "AkonadiItemId missing in query results!" << result.resourceUri() << property.isValid() << property.isLiteral() << property.literal().isString() << property.literal().type() << result.requestProperties().size(); +// qDebug() << result.requestProperties().values().first().toString(); + return -1; + } + return property.literal().toString().toLongLong(); +} + +NepomukSearch::NepomukSearch( QObject *parent ) + : QObject( parent ) + , mSearchService( 0 ) +{ + mSearchService = new Nepomuk::Query::QueryServiceClient( this ); + connect( mSearchService, SIGNAL(newEntries(QList)), + this, SLOT(hitsAdded(QList)) ); +} + +NepomukSearch::~NepomukSearch() +{ + mSearchService->close(); + delete mSearchService; +} + +QStringList NepomukSearch::search( const QString &query ) +{ + //qDebug() << Q_FUNC_INFO << query; + if ( !mSearchService->serviceAvailable() ) { + qWarning() << "Nepomuk search service not available!"; + return QStringList(); + } + + // Insert a property request for the item Id. This should + // be part of the query already and extracted from that, + // but that doesn't seem to work at the moment, so be explicit. + QHash encodedRps; + encodedRps.insert( QString::fromLatin1( "reqProp1" ), QUrl( QString::fromLatin1( "http://akonadi-project.org/ontologies/aneo#akonadiItemId" ) ).toString() ); + + if ( !mSearchService->blockingQuery( query, encodedRps ) ) { + qWarning() << Q_FUNC_INFO << "Calling blockingQuery() failed!" << query; + return QStringList(); + } + + // retrieve IDs for query results without ID: + // retrieving must happen after previous blocking query is finished to avoid that in case + // of multiple retrieved IDs after one result the wrong blocking thread is invoked + if ( !mResultsWithoutIdProperty.isEmpty() ) { + Nepomuk::Query::QueryServiceClient idSearchService( this ); + connect( &idSearchService, SIGNAL(newEntries(QList)), + this, SLOT(idHitsAdded(QList)) ); + + // go fetch the akonadi item id property for those results that don't have them + Q_FOREACH ( const Nepomuk::Query::Result &result, mResultsWithoutIdProperty ) { + qDebug() << "NepomukSearch::hitsAdded() search for IDs for results without ids: " << result.resourceUri().toString(); + QHash encodedRps; + // We do another query to get the akonadiItemId attribute (since it may not have been added to the original query) + encodedRps.insert( QString::fromLatin1( "reqProp1" ), QUrl( QString::fromLatin1( "http://akonadi-project.org/ontologies/aneo#akonadiItemId" ) ).toString() ); + + idSearchService.blockingQuery( QString::fromLatin1( "SELECT DISTINCT ?r ?reqProp1 WHERE { <%1> a ?v2 . <%1> ?reqProp1 . } LIMIT 1" ).arg( result.resourceUri().toString() ), encodedRps ); + } + mResultsWithoutIdProperty.clear(); + } + + return mMatchingUIDs.toList(); +} + +void NepomukSearch::addHit( const Nepomuk::Query::Result &result ) +{ + const qint64 itemId = resultToId( result ); + + if ( itemId == -1 ) { + return; + } + + mMatchingUIDs.insert( QString::number( itemId ) ); +} + +void NepomukSearch::hitsAdded( const QList &entries ) +{ + if ( !mSearchService->serviceAvailable() ) { + qWarning() << "Nepomuk search service not available!"; + return; + } + + Q_FOREACH ( const Nepomuk::Query::Result &result, entries ) { + const Soprano::Node &property = result.requestProperty( QUrl( QLatin1String( "http://akonadi-project.org/ontologies/aneo#akonadiItemId" ) ) ); + if ( !( property.isValid() && property.isLiteral() && property.literal().isString() ) ) { + mResultsWithoutIdProperty << result; + } else { + // for those results that already have a proper akonadi item id property, + // we can add matches right away + addHit( result ); + } + } +} + +void NepomukSearch::idHitsAdded( const QList< Nepomuk::Query::Result > &entries ) +{ + Q_FOREACH ( const Nepomuk::Query::Result &result, entries ) { + addHit( result ); + } +} diff --git a/akonadi/server/src/nepomuksearch.h b/akonadi/server/src/nepomuksearch.h new file mode 100644 index 00000000..3fbf592f --- /dev/null +++ b/akonadi/server/src/nepomuksearch.h @@ -0,0 +1,56 @@ +/* + 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_NEPOMUKSEARCH_H +#define AKONADI_NEPOMUKSEARCH_H + +#include +#include +#include + +#include "nepomuk/queryserviceclient.h" + +namespace Akonadi { +namespace Server { + +class NepomukSearch : public QObject +{ + Q_OBJECT + + public: + NepomukSearch( QObject *parent = 0 ); + ~NepomukSearch(); + + QStringList search( const QString &query ); + + private Q_SLOTS: + void hitsAdded( const QList &entries ); + void idHitsAdded( const QList &entries ); + + private: + void addHit( const Nepomuk::Query::Result &result ); + QSet mMatchingUIDs; + QList mResultsWithoutIdProperty; + Nepomuk::Query::QueryServiceClient *mSearchService; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/notificationmanager.cpp b/akonadi/server/src/notificationmanager.cpp new file mode 100644 index 00000000..ba222bbf --- /dev/null +++ b/akonadi/server/src/notificationmanager.cpp @@ -0,0 +1,234 @@ +/* + 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 +#include "notificationmanageradaptor.h" +#include "notificationsource.h" +#include "tracer.h" +#include "storage/datastore.h" +#include "clientcapabilityaggregator.h" + +#include +#include + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +NotificationManager *NotificationManager::mSelf = 0; + +NotificationManager::NotificationManager() + : QObject( 0 ) +{ + NotificationMessage::registerDBusTypes(); + NotificationMessageV2::registerDBusTypes(); + NotificationMessageV3::registerDBusTypes(); + + new NotificationManagerAdaptor( this ); + QDBusConnection::sessionBus().registerObject( QLatin1String( "/notifications" ), + this, QDBusConnection::ExportAdaptors ); + QDBusConnection::sessionBus().registerObject( QLatin1String( "/notifications/debug" ), + this, QDBusConnection::ExportScriptableSlots ); + + const QString serverConfigFile = AkStandardDirs::serverConfigFile( XdgBaseDirs::ReadWrite ); + QSettings settings( serverConfigFile, QSettings::IniFormat ); + + mTimer.setInterval( settings.value( QLatin1String( "NotificationManager/Interval" ), 50 ).toInt() ); + mTimer.setSingleShot( true ); + connect( &mTimer, SIGNAL(timeout()), SLOT(emitPendingNotifications()) ); +} + +NotificationManager::~NotificationManager() +{ +} + +NotificationManager *NotificationManager::self() +{ + if ( !mSelf ) { + mSelf = new NotificationManager(); + } + + return mSelf; +} + +void NotificationManager::connectNotificationCollector( NotificationCollector *collector ) +{ + connect( collector, SIGNAL(notify(Akonadi::NotificationMessageV3::List)), + SLOT(slotNotify(Akonadi::NotificationMessageV3::List)) ); +} + +void NotificationManager::slotNotify( const Akonadi::NotificationMessageV3::List &msgs ) +{ + //akDebug() << Q_FUNC_INFO << "Appending" << msgs.count() << "notifications to current list of " << mNotifications.count() << "notifications"; + Q_FOREACH ( const NotificationMessageV3 &msg, msgs ) + NotificationMessageV3::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; + } + + NotificationMessage::List legacyNotifications; + Q_FOREACH ( const NotificationMessageV3 ¬ification, mNotifications ) { + Tracer::self()->signal( "NotificationManager::notify", notification.toString() ); + + if ( ClientCapabilityAggregator::minimumNotificationMessageVersion() < 2 ) { + const NotificationMessage::List tmp = notification.toNotificationV1().toList(); + Q_FOREACH ( const NotificationMessage &legacyNotification, tmp ) { + bool appended = false; + NotificationMessage::appendAndCompress( legacyNotifications, legacyNotification, &appended ); + if ( !appended ) { + legacyNotifications << legacyNotification; + } + } + } + } + + if ( !legacyNotifications.isEmpty() ) { + Q_FOREACH ( NotificationSource *src, mNotificationSources ) { + src->emitNotification( legacyNotifications ); + } + } + + + NotificationMessageV2::List v2List; + if ( ClientCapabilityAggregator::maximumNotificationMessageVersion() == 2 ) { + v2List = NotificationMessageV3::toV2List( mNotifications ); + } + + if ( ClientCapabilityAggregator::maximumNotificationMessageVersion() > 1 ) { + Q_FOREACH ( NotificationSource *source, mNotificationSources ) { + if ( !source->isServerSideMonitorEnabled() ) { + if ( ClientCapabilityAggregator::maximumNotificationMessageVersion() == 2 ) { + source->emitNotification( v2List ); + } else { + source->emitNotification( mNotifications ); + } + continue; + } + + NotificationMessageV3::List acceptedNotifications; + Q_FOREACH ( const NotificationMessageV3 ¬ification, mNotifications ) { + if ( source->acceptsNotification( notification ) ) { + acceptedNotifications << notification; + } + } + + if ( !acceptedNotifications.isEmpty() ) { + if ( ClientCapabilityAggregator::maximumNotificationMessageVersion() == 2 ) { + source->emitNotification( NotificationMessageV3::toV2List( acceptedNotifications ) ); + } else { + source->emitNotification( acceptedNotifications ); + } + } + } + } + + // backward compatibility with the old non-subcription interface + // FIXME: Can we drop this already? + if ( !legacyNotifications.isEmpty() ) { + Q_EMIT notify( legacyNotifications ); + } + + mNotifications.clear(); +} + +QDBusObjectPath NotificationManager::subscribeV2( const QString &identifier, bool serverSideMonitor ) +{ + akDebug() << Q_FUNC_INFO << this << identifier << serverSideMonitor; + return subscribeV3( identifier, serverSideMonitor, false ); +} + +QDBusObjectPath NotificationManager::subscribeV3( const QString &identifier, bool serverSideMonitor, bool exclusive ) +{ + akDebug() << Q_FUNC_INFO << this << identifier << serverSideMonitor << 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->setServerSideMonitorEnabled( serverSideMonitor ); + source->setExclusive( exclusive ); + + // The path is /subscriber/escaped_identifier. We want to extract + // the escaped_identifier and emit it in subscribed() instead of the original + // identifier + const QStringList paths = source->dbusPath().path().split( QLatin1Char( '/' ), QString::SkipEmptyParts ); + + // FIXME KF5: Emit the QDBusObjectPath instead of the identifier + Q_EMIT subscribed( paths.at( 1 ) ); + + return source->dbusPath(); +} + +void NotificationManager::registerSource( NotificationSource *source ) +{ + mNotificationSources.insert( source->identifier(), source ); +} + +QDBusObjectPath NotificationManager::subscribe( const QString &identifier ) +{ + akDebug() << Q_FUNC_INFO << this << identifier; + return subscribeV2( identifier, false ); +} + +void NotificationManager::unsubscribe( const QString &identifier ) +{ + NotificationSource *source = mNotificationSources.value( identifier ); + if ( source ) { + unregisterSource( source ); + const QStringList paths = source->dbusPath().path().split( QLatin1Char( '/' ), QString::SkipEmptyParts ); + source->deleteLater(); + Q_EMIT unsubscribed( paths.at( 1 ) ); + } else { + akDebug() << "Attempt to unsubscribe unknown subscriber" << identifier; + } +} + +void NotificationManager::unregisterSource( NotificationSource *source ) +{ + mNotificationSources.remove( source->identifier() ); +} + +QStringList NotificationManager::subscribers() const +{ + QStringList identifiers; + Q_FOREACH ( NotificationSource *source, mNotificationSources ) { + identifiers << source->identifier(); + } + + return identifiers; +} diff --git a/akonadi/server/src/notificationmanager.h b/akonadi/server/src/notificationmanager.h new file mode 100644 index 00000000..3143cb06 --- /dev/null +++ b/akonadi/server/src/notificationmanager.h @@ -0,0 +1,130 @@ +/* + 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 "../libs/notificationmessage_p.h" +#include "../libs/notificationmessagev3_p.h" +#include "storage/entity.h" + +#include +#include +#include +#include + +class NotificationManagerTest; + +namespace Akonadi { +namespace Server { + +class NotificationCollector; +class NotificationSource; + +/** + 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 ); + + public Q_SLOTS: + Q_SCRIPTABLE void emitPendingNotifications(); + + /** + * Subscribe to notifications emitted by this manager. + * + * @param identifier Identifier to use of our subscription. + * @return The path we got assigned. Contains identifier. + */ + Q_SCRIPTABLE QDBusObjectPath subscribe( const QString &identifier ); + + /** + * Subscribe to notifications emitted by this manager. + * + * @param identifier Identifier to use for our subscription. + * @param serverSideMonitor Whether client supports server-side monitoring + * @return The path we got assigned. Contains identifier. + */ + Q_SCRIPTABLE QDBusObjectPath subscribeV2( const QString &identifier, bool serverSideMonitor ); + + /** + * Subscribe to notifications emitted by this manager. + * + * @param identifier Identifier to use for our subscription. + * @param serverSideMonitor Whether client supports server-side monitoring + * @param exclusive Exclusive subscribers also receive notifications on referenced collections + * @return The path we got assigned. Contains identifier. + */ + Q_SCRIPTABLE QDBusObjectPath subscribeV3( const QString &identifier, bool serverSideMonitor, 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. + */ + Q_SCRIPTABLE void unsubscribe( const QString &identifier ); + + /** + * Returns identifiers of currently subscribed sources + */ + Q_SCRIPTABLE QStringList subscribers() const; + + Q_SIGNALS: + Q_SCRIPTABLE void notify( const Akonadi::NotificationMessage::List &msgs ); + + Q_SCRIPTABLE void subscribed( const QString &identifier ); + Q_SCRIPTABLE void unsubscribed( const QString &identifier ); + + private Q_SLOTS: + void slotNotify( const Akonadi::NotificationMessageV3::List &msgs ); + + private: + NotificationManager(); + + private: + void registerSource( NotificationSource *source ); + void unregisterSource( NotificationSource *source ); + + static NotificationManager *mSelf; + NotificationMessageV3::List mNotifications; + QTimer mTimer; + + //! One message source for each subscribed process + QHash mNotificationSources; + + friend class NotificationSource; + friend class ::NotificationManagerTest; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/notificationsource.cpp b/akonadi/server/src/notificationsource.cpp new file mode 100644 index 00000000..c190e319 --- /dev/null +++ b/akonadi/server/src/notificationsource.cpp @@ -0,0 +1,442 @@ +/* + 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" + +using namespace Akonadi; +using namespace Akonadi::Server; + +template +QVector setToVector( const QSet &set ) +{ + QVector v( set.size() ); + std::copy( set.constBegin(), set.constEnd(), v.begin() ); + return v; +} + +NotificationSource::NotificationSource( const QString &identifier, const QString &clientServiceName, NotificationManager *parent ) + : QObject( parent ) + , mManager( parent ) + , mIdentifier( identifier ) + , mDBusIdentifier( identifier ) + , mClientWatcher( 0 ) + , mServerSideMonitorEnabled( false ) + , 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, SIGNAL(serviceUnregistered(QString)), SLOT(serviceUnregistered(QString)) ); +} + +NotificationSource::~NotificationSource() +{ +} + +QDBusObjectPath NotificationSource::dbusPath() const +{ + return QDBusObjectPath( QLatin1String( "/subscriber/" ) + mDBusIdentifier ); +} + +void NotificationSource::emitNotification( const NotificationMessage::List ¬ifications ) +{ + Q_EMIT notify( notifications ); +} + +void NotificationSource::emitNotification( const NotificationMessageV2::List ¬ifications ) +{ + Q_EMIT notifyV2( notifications ); +} + +void NotificationSource::emitNotification( const NotificationMessageV3::List ¬ifications ) +{ + Q_EMIT notifyV3( notifications ); +} + +QString NotificationSource::identifier() const +{ + return mIdentifier; +} + +void NotificationSource::unsubscribe() +{ + mManager->unsubscribe( mIdentifier ); +} + +bool NotificationSource::isServerSideMonitorEnabled() const +{ + return mServerSideMonitorEnabled; +} + +void NotificationSource::setServerSideMonitorEnabled( bool enabled ) +{ + mServerSideMonitorEnabled = enabled; +} + +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 || !isServerSideMonitorEnabled() ) { + 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 || !isServerSideMonitorEnabled() ) { + 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 || !isServerSideMonitorEnabled() ) { + 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 ( !isServerSideMonitorEnabled() ) { + return; + } + + 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() || !isServerSideMonitorEnabled() ) { + 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 ( !isServerSideMonitorEnabled() ) { + return; + } + + if ( allMonitored && !mAllMonitored ) { + mAllMonitored = true; + Q_EMIT isAllMonitoredChanged(); + } else if ( !allMonitored ) { + mAllMonitored = false; + Q_EMIT isAllMonitoredChanged(); + } +} + +bool NotificationSource::isAllMonitored() const +{ + return mAllMonitored; +} + +void NotificationSource::setIgnoredSession( const QByteArray &sessionId, bool ignored ) +{ + if ( !isServerSideMonitorEnabled() ) { + return; + } + + 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 NotificationMessageV3 &msg ) const +{ + if ( msg.operation() != NotificationMessageV2::Move ) { + return false; + } + return mMonitoredResources.contains( msg.destinationResource() ); +} + +void NotificationSource::setMonitoredType( NotificationMessageV2::Type type, bool monitored ) +{ + if ( !isServerSideMonitorEnabled() ) { + return; + } + + 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 NotificationMessageV3 ¬ification ) +{ + // session is ignored + if ( mIgnoredSessions.contains( notification.sessionId() ) ) { + return false; + } + + if ( notification.entities().count() == 0 ) { + return false; + } + + //Only emit notifications for referenced collections if the subscriber is exclusive or monitors the collection + if ( notification.type() == NotificationMessageV2::Collections ) { + Q_FOREACH ( const NotificationMessageV2::Entity &entity, notification.entities() ) { + if ( CollectionReferenceManager::instance()->isReferenced( entity.id ) ) { + return ( mExclusive || isCollectionMonitored( entity.id ) ); + } + } + } else if ( notification.type() == NotificationMessageV2::Items ) { + if ( CollectionReferenceManager::instance()->isReferenced( notification.parentCollection() ) ) { + return ( mExclusive || isCollectionMonitored( notification.parentCollection() ) || isMoveDestinationResourceMonitored( notification ) ); + } + } + + // user requested everything + if ( mAllMonitored && notification.type() != NotificationMessageV2::InvalidType ) { + return true; + } + + switch ( notification.type() ) { + case NotificationMessageV2::InvalidType: + akDebug() << "Received invalid change notification!"; + return false; + + case NotificationMessageV2::Items: + if ( !mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains( NotificationMessageV2::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 NotificationMessageV2::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 NotificationMessageV2::Entity &entity, notification.entities() ) { + if ( mMonitoredItems.contains( entity.id ) ) { + return true; + } + } + + return isCollectionMonitored( notification.parentCollection() ) + || isCollectionMonitored( notification.parentDestCollection() ); + + case NotificationMessageV2::Collections: + if ( !mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains( NotificationMessageV2::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 NotificationMessageV2::Entity &entity, notification.entities() ) { + if ( isCollectionMonitored( entity.id ) ) { + return true; + } + } + + return isCollectionMonitored( notification.parentCollection() ) + || isCollectionMonitored( notification.parentDestCollection() ); + + case NotificationMessageV2::Tags: + if ( !mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains( NotificationMessageV2::Tags ) ) { + return false; + } + + if ( mMonitoredTags.isEmpty() ) { + return true; + } + + Q_FOREACH ( const NotificationMessageV2::Entity &entity, notification.entities() ) { + if ( mMonitoredTags.contains( entity.id ) ) { + return true; + } + } + + return false; + } + + return false; +} diff --git a/akonadi/server/src/notificationsource.h b/akonadi/server/src/notificationsource.h new file mode 100644 index 00000000..bb69e4ec --- /dev/null +++ b/akonadi/server/src/notificationsource.h @@ -0,0 +1,174 @@ +/* + 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 "../libs/notificationmessage_p.h" +#include "../libs/notificationmessagev2_p.h" +#include "../libs/notificationmessagev3_p.h" + +#include +#include +#include + +#include "entities.h" + +namespace Akonadi { +namespace Server { + +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 NotificationMessageV3::List ¬ifications ); + + /** + * Emit the given notifications + * + * @param notifications List of notifications to emit. + */ + void emitNotification( const NotificationMessageV2::List ¬ifications ); + + /** + * Emit the given notifications + * + * @param notifications List of notifications to emit. + */ + void emitNotification( const NotificationMessage::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 ); + + void setServerSideMonitorEnabled( bool enabled ); + bool isServerSideMonitorEnabled() const; + + void setExclusive( bool exclusive ); + bool isExclusive() const; + + bool acceptsNotification( const NotificationMessageV3 ¬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 setIgnoredSession( const QByteArray &sessionId, bool ignored ); + Q_SCRIPTABLE QVector ignoredSessions() const; + Q_SCRIPTABLE void setMonitoredType( NotificationMessageV2::Type type, bool monitored ); + Q_SCRIPTABLE QVector monitoredTypes() const; + + Q_SIGNALS: + + Q_SCRIPTABLE void notify( const Akonadi::NotificationMessage::List &msgs ); + Q_SCRIPTABLE void notifyV2( const Akonadi::NotificationMessageV2::List &msgs ); + Q_SCRIPTABLE void notifyV3( const Akonadi::NotificationMessageV3::List &msgs ); + + 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 NotificationMessageV3 &msg ) const; + + private: + NotificationManager *mManager; + QString mIdentifier; + QString mDBusIdentifier; + QDBusServiceWatcher *mClientWatcher; + + bool mServerSideMonitorEnabled; + bool mAllMonitored; + bool mExclusive; + QSet mMonitoredCollections; + QSet mMonitoredItems; + QSet mMonitoredTags; + // TODO: Make this a bitflag + QSet mMonitoredTypes; + QSet mMonitoredMimeTypes; + QSet mMonitoredResources; + QSet mIgnoredSessions; + +}; // class NotificationSource + +} // namespace Server +} // namespace Akonadi + +#endif // #define AKONADI_NOTIFICATIONSOURCE_H diff --git a/akonadi/server/src/nulltracer.h b/akonadi/server/src/nulltracer.h new file mode 100644 index 00000000..8ba66e8b --- /dev/null +++ b/akonadi/server/src/nulltracer.h @@ -0,0 +1,61 @@ +/*************************************************************************** + * 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/akonadi/server/src/preprocessorinstance.cpp b/akonadi/server/src/preprocessorinstance.cpp new file mode 100644 index 00000000..e087ab06 --- /dev/null +++ b/akonadi/server/src/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 +#include "preprocessorinterface.h" +#include "preprocessormanager.h" + +#include "entities.h" + +#include "agentcontrolinterface.h" +#include "agentmanagerinterface.h" + +#include "tracer.h" + +#include + +#include + +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( + AkDBus::agentServiceName( mId, AkDBus::Preprocessor ), + QLatin1String( "/Preprocessor" ), + QDBusConnection::sessionBus(), + this ); + + if ( !mInterface || !mInterface->isValid() ) { + Tracer::self()->warning( + QLatin1String( "PreprocessorInstance" ), + QString::fromLatin1( "Could not connect to pre-processor instance '%1': %2" ) + .arg( mId ) + .arg( mInterface ? mInterface->lastError().message() : QString() ) ); + delete mInterface; + mInterface = 0; + return false; + } + + QObject::connect( mInterface, SIGNAL(itemProcessed(qlonglong)), this, SLOT(itemProcessed(qlonglong)) ); + + 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( + AkDBus::agentServiceName( mId, AkDBus::Agent ), + QLatin1String( "/" ), + QDBusConnection::sessionBus(), + this ); + + if ( !iface.isValid() ) { + Tracer::self()->warning( + QLatin1String( "PreprocessorInstance" ), + QString::fromLatin1( "Could not connect to pre-processor instance '%1': %2" ) + .arg( mId ) + .arg( 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( + AkDBus::serviceName( AkDBus::Control ), + QLatin1String( "/AgentManager" ), + QDBusConnection::sessionBus(), + this ); + + if ( !iface.isValid() ) { + Tracer::self()->warning( + QLatin1String( "PreprocessorInstance" ), + QString::fromLatin1( "Could not connect to the AgentManager in order to restart pre-processor instance '%1': %2" ) + .arg( mId ) + .arg( 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( + QLatin1String( "PreprocessorInstance" ), + QString::fromLatin1( "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( + QLatin1String( "PreprocessorInstance" ), + QString::fromLatin1( "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/akonadi/server/src/preprocessorinstance.h b/akonadi/server/src/preprocessorinstance.h new file mode 100644 index 00000000..b3b5991c --- /dev/null +++ b/akonadi/server/src/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 responsable 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/akonadi/server/src/preprocessormanager.cpp b/akonadi/server/src/preprocessormanager.cpp new file mode 100644 index 00000000..9d8df35b --- /dev/null +++ b/akonadi/server/src/preprocessormanager.cpp @@ -0,0 +1,489 @@ +/****************************************************************************** + * + * 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 + +#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( + QLatin1String( "/PreprocessorManager" ), + this, + QDBusConnection::ExportAdaptors ); + + mHeartbeatTimer = new QTimer( this ); + + QObject::connect( mHeartbeatTimer, SIGNAL(timeout()), this, SLOT(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( + QLatin1String( "PreprocessorManager" ), + QString::fromLatin1( "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 ); + + Q_FOREACH ( qint64 itemId, *itemList ) { + nextPreprocessor->enqueueItem( itemId ); + } + } else { + // This was the last preprocessor: end handling the items + Q_FOREACH ( 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. + + qWarning() << "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, SIGNAL(destroyed()), this, SLOT(dataStoreDestroyed()) ); + QObject::connect( dataStore, SIGNAL(transactionCommitted()), this, SLOT(dataStoreTransactionCommitted()) ); + QObject::connect( dataStore, SIGNAL(transactionRolledBack()), this, SLOT(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 ) { + qWarning() << "PreprocessorManager::lockedKillWaitQueue(): called for dataStore which has no wait queue"; + return; + } + + mTransactionWaitQueueHash.remove( dataStore ); + + delete waitQueue; + + if ( !disconnectSlots ) { + return; + } + + QObject::disconnect( dataStore, SIGNAL(destroyed()), this, SLOT(dataStoreDestroyed()) ); + QObject::disconnect( dataStore, SIGNAL(transactionCommitted()), this, SLOT(dataStoreTransactionCommitted()) ); + QObject::disconnect( dataStore, SIGNAL(transactionRolledBack()), this, SLOT(dataStoreTransactionRolledBack()) ); + +} + +void PreprocessorManager::dataStoreDestroyed() +{ + QMutexLocker locker( mMutex ); + + akDebug() << "PreprocessorManager::dataStoreDestroyed(): killing the wait queue"; + + const DataStore *dataStore = dynamic_cast< const DataStore *>( sender() ); + if ( !dataStore ) { + qWarning() << "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 ) { + qWarning() << "PreprocessorManager::dataStoreTransactionCommitted(): got the signal from a non DataStore object"; + return; + } + + std::deque< qint64 > *waitQueue = mTransactionWaitQueueHash.value( dataStore, 0 ); + if ( !waitQueue ) { + qWarning() << "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 + Q_FOREACH ( qint64 id, *waitQueue ) { + lockedEndHandleItem( id ); + } + } else { + Q_FOREACH ( 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 ) { + qWarning() << "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( + QLatin1String( "PreprocessorManager" ), + QString::fromLatin1( "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( + QLatin1String( "PreprocessorManager" ), + QString::fromLatin1( "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( + QLatin1String( "PreprocessorManager" ), + QString::fromLatin1( "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( + QLatin1String( "PreprocessorManager" ), + QString::fromLatin1( "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/akonadi/server/src/preprocessormanager.h b/akonadi/server/src/preprocessormanager.h new file mode 100644 index 00000000..ee1ed327 --- /dev/null +++ b/akonadi/server/src/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 ths Akonad 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 succesfull 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 succesfull 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/akonadi/server/src/resourcemanager.cpp b/akonadi/server/src/resourcemanager.cpp new file mode 100644 index 00000000..e7c14f8d --- /dev/null +++ b/akonadi/server/src/resourcemanager.cpp @@ -0,0 +1,90 @@ +/* + 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 "resourcemanageradaptor.h" +#include "libs/capabilities_p.h" + +#include +#include "storage/transaction.h" + +using namespace Akonadi::Server; + +ResourceManager *ResourceManager::mSelf = 0; + +ResourceManager::ResourceManager( QObject *parent ) + : QObject( parent ) +{ + new ResourceManagerAdaptor( this ); + QDBusConnection::sessionBus().registerObject( QLatin1String( "/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", QString::fromLatin1( "Resource '%1' already exists." ).arg( name ) ); + return; // resource already exists + } + + // create the resource + resource.setName( name ); + resource.setIsVirtual( capabilities.contains( QLatin1String( AKONADI_AGENT_CAPABILITY_VIRTUAL ) ) ); + if ( !resource.insert() ) { + Tracer::self()->error( "ResourceManager", QString::fromLatin1( "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; + Q_FOREACH ( const Resource &res, Resource::retrieveAll() ) { + result.append( res.name() ); + } + return result; +} + +ResourceManager *ResourceManager::self() +{ + if ( !mSelf ) { + mSelf = new ResourceManager(); + } + return mSelf; +} diff --git a/akonadi/server/src/resourcemanager.h b/akonadi/server/src/resourcemanager.h new file mode 100644 index 00000000..beddfd90 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/response.cpp b/akonadi/server/src/response.cpp new file mode 100644 index 00000000..07a42763 --- /dev/null +++ b/akonadi/server/src/response.cpp @@ -0,0 +1,104 @@ +/*************************************************************************** + * 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 "response.h" + +#include + +using namespace Akonadi::Server; + +static const char *s_resultCodeStrings[] = { + "OK", "NO", "BAD", "BYE", "" +}; + +Response::Response() + : m_resultCode( OK ) + , m_tag( "*" ) +{ +} + +Response::~Response() +{ +} + +QByteArray Response::asString() const +{ + QByteArray b = m_tag; + if ( m_tag != "*" && m_tag != "+" && m_resultCode != USER ) { + b += ' '; + b += s_resultCodeStrings[m_resultCode]; + } + b += ' '; + b += m_responseString; + return b; +} + +Response::ResultCode Response::resultCode() const +{ + return m_resultCode; +} + +void Response::setSuccess() +{ + m_resultCode = Response::OK; +} + +void Response::setFailure() +{ + m_resultCode = Response::NO; +} + +void Response::setError() +{ + m_resultCode = Response::BAD; +} + +void Response::setTag( const QByteArray &tag ) +{ + m_tag = tag; +} + +void Response::setUntagged() +{ + m_tag = QByteArray( 1, '*' ); +} + +void Response::setContinuation() +{ + m_tag = QByteArray( 1, '+' ); +} + +void Response::setString( const QByteArray &string ) +{ + m_responseString = string; +} + +void Response::setString( const char *string ) +{ + m_responseString = QByteArray( string ); +} + +void Response::setBye() +{ + m_resultCode = Response::BYE; +} + +void Response::setUserDefined() +{ + m_resultCode = Response::USER; +} diff --git a/akonadi/server/src/response.h b/akonadi/server/src/response.h new file mode 100644 index 00000000..fa17c2a6 --- /dev/null +++ b/akonadi/server/src/response.h @@ -0,0 +1,75 @@ +/*************************************************************************** + * 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 AKONADIRESPONSE_H +#define AKONADIRESPONSE_H + +#include +#include + +namespace Akonadi { +namespace Server { + +/** + @brief A command result. + Encapsulates the result of a command, including what to send + back to the client. + */ +class Response{ +public: + enum ResultCode { + OK = 0, + NO = 1, + BAD = 2, + BYE = 3, // not strictly a result code, but easier to handle this way + USER = 4 + }; + + Response(); + + ~Response(); + + ResultCode resultCode() const; + + /** The response string to be sent to the client. */ + QByteArray asString() const; + + void setTag( const QByteArray &tag ); + void setUntagged(); + void setContinuation(); + + void setString( const char *string ); + void setString( const QByteArray &string ); + + void setSuccess(); + void setFailure(); + void setError(); + void setBye(); + void setUserDefined(); +private: + QByteArray m_responseString; + ResultCode m_resultCode; + QByteArray m_tag; +}; + +} // namespace Server +} // namespace Akonadi + +Q_DECLARE_METATYPE( Akonadi::Server::Response ) + +#endif diff --git a/akonadi/server/src/search/abstractsearchengine.h b/akonadi/server/src/search/abstractsearchengine.h new file mode 100644 index 00000000..9be38b95 --- /dev/null +++ b/akonadi/server/src/search/abstractsearchengine.h @@ -0,0 +1,58 @@ +/* + 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/akonadi/server/src/search/abstractsearchplugin.h b/akonadi/server/src/search/abstractsearchplugin.h new file mode 100644 index 00000000..f51bd5d2 --- /dev/null +++ b/akonadi/server/src/search/abstractsearchplugin.h @@ -0,0 +1,64 @@ +/* + 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 + +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/akonadi/server/src/search/agentsearchengine.cpp b/akonadi/server/src/search/agentsearchengine.cpp new file mode 100644 index 00000000..d4327697 --- /dev/null +++ b/akonadi/server/src/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 +#include +#include "entities.h" + +#include "libs/protocol_p.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +void AgentSearchEngine::addSearch( const Collection &collection ) +{ + QDBusInterface agentMgr( AkDBus::serviceName( AkDBus::Control ), + QLatin1String( AKONADI_DBUS_AGENTMANAGER_PATH ), + QLatin1String( "org.freedesktop.Akonadi.AgentManagerInternal" ) ); + if ( agentMgr.isValid() ) { + QList args; + args << collection.queryString() + << QLatin1String("") + << collection.id(); + agentMgr.callWithArgumentList( QDBus::NoBlock, QLatin1String( "addSearch" ), args ); + return; + } + + akError() << "Failed to connect to agent manager: " << agentMgr.lastError().message(); +} + +void AgentSearchEngine::removeSearch( qint64 id ) +{ + QDBusInterface agentMgr( AkDBus::serviceName( AkDBus::Control ), + QLatin1String( AKONADI_DBUS_AGENTMANAGER_PATH ), + QLatin1String( "org.freedesktop.Akonadi.AgentManagerInternal" ) ); + if ( agentMgr.isValid() ) { + QList args; + args << id; + agentMgr.callWithArgumentList( QDBus::NoBlock, QLatin1String( "removeSearch" ), args ); + return; + } + + akError() << "Failed to connect to agent manager: " << agentMgr.lastError().message(); +} diff --git a/akonadi/server/src/search/agentsearchengine.h b/akonadi/server/src/search/agentsearchengine.h new file mode 100644 index 00000000..126dd9ce --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/search/agentsearchinstance.cpp b/akonadi/server/src/search/agentsearchinstance.cpp new file mode 100644 index 00000000..ca6ef87f --- /dev/null +++ b/akonadi/server/src/search/agentsearchinstance.cpp @@ -0,0 +1,85 @@ +/* + 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 "akdbus.h" +#include "dbusconnectionpool.h" + +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( + AkDBus::agentServiceName( mId, AkDBus::Agent ), + QLatin1String( "/Search" ), + DBusConnectionPool::threadConnection() ); + + if ( !mInterface || !mInterface->isValid() ) { + delete mInterface; + mInterface = 0; + return false; + } + + mServiceWatcher = new QDBusServiceWatcher( AkDBus::agentServiceName( mId, AkDBus::Agent ), + DBusConnectionPool::threadConnection(), + QDBusServiceWatcher::WatchForOwnerChange, + this ); + connect( mServiceWatcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)), + this, SLOT(serviceOwnerChanged(QString,QString,QString)) ); + + 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/akonadi/server/src/search/agentsearchinstance.h b/akonadi/server/src/search/agentsearchinstance.h new file mode 100644 index 00000000..78ef5aaa --- /dev/null +++ b/akonadi/server/src/search/agentsearchinstance.h @@ -0,0 +1,58 @@ +/* + 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/akonadi/server/src/search/nepomuksearchengine.cpp b/akonadi/server/src/search/nepomuksearchengine.cpp new file mode 100644 index 00000000..1cd4dab1 --- /dev/null +++ b/akonadi/server/src/search/nepomuksearchengine.cpp @@ -0,0 +1,257 @@ +/* + 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. +*/ + +#include "nepomuksearchengine.h" + +#include +#include "src/nepomuk/result.h" + +#include "entities.h" +#include "storage/notificationcollector.h" +#include "storage/selectquerybuilder.h" +#include "notificationmanager.h" +#include "libs/xdgbasedirs_p.h" +#include "libs/protocol_p.h" + +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +static qint64 resultToId( const Nepomuk::Query::Result &result ) +{ + const Soprano::Node &property = result.requestProperty( QUrl( QLatin1String( "http://akonadi-project.org/ontologies/aneo#akonadiItemId" ) ) ); + if ( !( property.isValid() && property.isLiteral() && property.literal().isString() ) ) { + qWarning() << "Failed to get requested akonadiItemId property"; + qDebug() << "AkonadiItemId missing in query results!" << result.resourceUri() << property.isValid() << property.isLiteral() << property.literal().isString() << property.literal().type() << result.requestProperties().size(); +// qDebug() << result.requestProperties().values().first().toString(); + return -1; + } + return property.literal().toString().toLongLong(); +} + +NepomukSearchEngine::NepomukSearchEngine( QObject *parent ) + : QObject( parent ) + , mCollector( new NotificationCollector( this ) ) + , m_liveSearchEnabled( true ) +{ + NotificationManager::self()->connectNotificationCollector( mCollector ); + + QDBusServiceWatcher *watcher = + new QDBusServiceWatcher( QLatin1String( "org.kde.nepomuk.services.nepomukqueryservice" ), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration, this ); + connect( watcher, SIGNAL(serviceRegistered(QString)), SLOT(reloadSearches()) ); + + if ( Nepomuk::Query::QueryServiceClient::serviceAvailable() ) { + reloadSearches(); + } else { + // FIXME: try to start the nepomuk server + akError() << "Nepomuk Query Server not available"; + } + + const QSettings settings( XdgBaseDirs::akonadiServerConfigFile(), QSettings::IniFormat ); + m_liveSearchEnabled = settings.value( QLatin1String( "Nepomuk/LiveSearchEnabled" ), false ).toBool(); +} + +NepomukSearchEngine::~NepomukSearchEngine() +{ + stopSearches(); +} + +void NepomukSearchEngine::addSearch( const Collection &collection ) +{ + const QStringList queryAttributes = collection.queryAttributes().split( QLatin1Char( ' ' ) ); + const int index = queryAttributes.indexOf( QLatin1String ( AKONADI_PARAM_PERSISTENTSEARCH_QUERYLANG ) ); + if ( index == -1 || queryAttributes.size() < index || queryAttributes.value( index + 1 ) != QLatin1String( "SPARQL" ) ) { + return; + } + + const QString &q = collection.queryString(); + + if ( q.size() >= 32768 ) { + qWarning() << "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."; + return; + } + + //FIXME the requested property must be passed to here from the calling code + //Ideally the Query is passed as object so we can check here for the akonadiItemId property, and add it if missing + if ( !q.contains( QString::fromLatin1( "reqProp1" ) ) || !q.contains( QString::fromLatin1( "http://akonadi-project.org/ontologies/aneo#akonadiItemId" ) ) ) { + qWarning() << "The query MUST contain exactly one required property (http://akonadi-project.org/ontologies/aneo#akonadiItemId), if another property is additionally requested or the akonadiItemId is missing the search will fail (due to this hack)"; + qWarning() << q; + return; + } + + Nepomuk::Query::QueryServiceClient *query = new Nepomuk::Query::QueryServiceClient( this ); + + connect( query, SIGNAL(newEntries(QList)), + this, SLOT(hitsAdded(QList)) ); + connect( query, SIGNAL(entriesRemoved(QList)), + this, SLOT(hitsRemoved(QList)) ); + connect( query, SIGNAL(finishedListing()), + this, SLOT(finishedListing()) ); + + mMutex.lock(); + mQueryMap.insert( query, collection.id() ); + mQueryInvMap.insert( collection.id(), query ); // needed for fast lookup in removeSearch() + mMutex.unlock(); + + QHash encodedRps; + encodedRps.insert( QString::fromLatin1( "reqProp1" ), QUrl( QString::fromLatin1( "http://akonadi-project.org/ontologies/aneo#akonadiItemId" ) ).toString() ); //FIXME hack because the reqProp is not passed to here by the caller + query->query( collection.queryString(), encodedRps ); +} + +void NepomukSearchEngine::removeSearch( qint64 collectionId ) +{ + Nepomuk::Query::QueryServiceClient *query = mQueryInvMap.value( collectionId ); + if ( !query ) { + qWarning() << "Nepomuk QueryServer: Query could not be removed!"; + return; + } + + query->close(); + + // cleanup mappings + mMutex.lock(); + mQueryInvMap.remove( collectionId ); + mQueryMap.remove( query ); + mMutex.unlock(); + + query->deleteLater(); +} + +void NepomukSearchEngine::reloadSearches() +{ + akDebug() << this << sender(); + SelectQueryBuilder qb; + qb.addValueCondition( Collection::queryAttributesFullColumnName(), Query::Like, QLatin1String( "%SPARQL%" ) ); + if ( !qb.exec() ) { + qWarning() << "Nepomuk QueryServer: Unable to execute query!"; + return; + } + + Q_FOREACH ( const Collection &collection, qb.result() ) { + mMutex.lock(); + if ( mQueryInvMap.contains( collection.id() ) ) { + mMutex.unlock(); + akDebug() << "updating search" << collection.name(); + removeSearch( collection.id() ); + } else { + mMutex.unlock(); + akDebug() << "adding search" << collection.name(); + } + addSearch( collection ); + } +} + +void NepomukSearchEngine::stopSearches() +{ + SelectQueryBuilder qb; + qb.addValueCondition( Collection::queryAttributesFullColumnName(), Query::Like, QLatin1String( "%SPARQL%" ) ); + if ( !qb.exec() ) { + qWarning() << "Nepomuk QueryServer: Unable to execute query!"; + return; + } + + Q_FOREACH ( const Collection &collection, qb.result() ) { + akDebug() << "removing search" << collection.name(); + removeSearch( collection.id() ); + } +} + +void NepomukSearchEngine::hitsAdded( const QList &entries ) +{ + Nepomuk::Query::QueryServiceClient *query = qobject_cast( sender() ); + if ( !query ) { + qWarning() << "Nepomuk QueryServer: Got signal from non-existing search query!"; + return; + } + + mMutex.lock(); + qint64 collectionId = mQueryMap.value( query ); + mMutex.unlock(); + const Collection collection = Collection::retrieveById( collectionId ); + + PimItem::List items; + Q_FOREACH ( const Nepomuk::Query::Result &result, entries ) { + const qint64 itemId = resultToId( result ); + + if ( itemId == -1 ) { + continue; + } + + Entity::addToRelation( collectionId, itemId ); + items << PimItem::retrieveById( itemId ); + } + mCollector->itemsLinked( items, collection ); + mCollector->dispatchNotifications(); +} + +void NepomukSearchEngine::hitsRemoved( const QList &entries ) +{ + Nepomuk::Query::QueryServiceClient *query = qobject_cast( sender() ); + if ( !query ) { + qWarning() << "Nepomuk QueryServer: Got signal from non-existing search query!"; + return; + } + + mMutex.lock(); + qint64 collectionId = mQueryMap.value( query ); + mMutex.unlock(); + const Collection collection = Collection::retrieveById( collectionId ); + PimItem::List items; + Q_FOREACH ( const Nepomuk::Query::Result &result, entries ) { + const qint64 itemId = resultToId( result ); + + if ( itemId == -1 ) { + continue; + } + + Entity::removeFromRelation( collectionId, itemId ); + items << PimItem::retrieveById( itemId ); + } + + mCollector->itemsUnlinked( items, collection ); + mCollector->dispatchNotifications(); +} + +void NepomukSearchEngine::finishedListing() +{ + if ( m_liveSearchEnabled ) { + return; + } + + Nepomuk::Query::QueryServiceClient *query = qobject_cast( sender() ); + if ( !query ) { + qWarning() << Q_FUNC_INFO << "Nepomuk QueryServer: Got signal from non-existing search query!"; + return; + } + + mMutex.lock(); + qint64 collectionId = mQueryMap.value( query ); + mMutex.unlock(); + + removeSearch( collectionId ); +} diff --git a/akonadi/server/src/search/nepomuksearchengine.h b/akonadi/server/src/search/nepomuksearchengine.h new file mode 100644 index 00000000..3110446a --- /dev/null +++ b/akonadi/server/src/search/nepomuksearchengine.h @@ -0,0 +1,70 @@ +/* + 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_NEPOMUKSEARCHENGINE_H +#define AKONADI_NEPOMUKSEARCHENGINE_H + +#include "abstractsearchengine.h" + +#include +#include +#include + +#include + +namespace Akonadi { +namespace Server { + +class NotificationCollector; + +class NepomukSearchEngine : public QObject, public AbstractSearchEngine +{ + Q_OBJECT + + public: + NepomukSearchEngine( QObject *parent = 0 ); + ~NepomukSearchEngine(); + + void addSearch( const Collection &collection ); + void removeSearch( qint64 collection ); + + private: + void stopSearches(); + + private Q_SLOTS: + void reloadSearches(); + void hitsAdded( const QList &entries ); + void hitsRemoved( const QList &entries ); + void finishedListing(); + + private: + QMutex mMutex; + NotificationCollector *mCollector; + + QHash mQueryMap; + QHash mQueryInvMap; + + bool m_liveSearchEnabled; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/search/searchmanager.cpp b/akonadi/server/src/search/searchmanager.cpp new file mode 100644 index 00000000..35e76e18 --- /dev/null +++ b/akonadi/server/src/search/searchmanager.cpp @@ -0,0 +1,426 @@ +/* + 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 "searchmanageradaptor.h" + +#include "akdebug.h" +#include "agentsearchengine.h" +#include "nepomuksearchengine.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 "searchhelper.h" +#include "libs/xdgbasedirs_p.h" +#include "libs/protocol_p.h" + + +#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( QWaitCondition* ) + +SearchManagerThread::SearchManagerThread( const QStringList &searchEngines, QObject *parent ) + : QThread( parent ) + , mSearchEngines( searchEngines ) +{ + Q_ASSERT( QThread::currentThread() == QCoreApplication::instance()->thread() ); +} + +SearchManagerThread::~SearchManagerThread() +{ +} + +void SearchManagerThread::run() +{ + SearchManager *manager = new SearchManager(); + manager->init( mSearchEngines ); + exec(); + delete manager; +} + +SearchManager::SearchManager( QObject *parent ) + : QObject( parent ) +{ + qRegisterMetaType< QSet >(); + qRegisterMetaType(); + qRegisterMetaType(); + + Q_ASSERT( sInstance == 0 ); + sInstance = this; + + DataStore::self(); +} + +void SearchManager::init(const QStringList& searchEngines) +{ + mEngines.reserve( searchEngines.size() ); + Q_FOREACH ( const QString &engineName, searchEngines ) { + if ( engineName == QLatin1String( "Nepomuk" ) ) { +#ifdef HAVE_SOPRANO + m_engines.append( new NepomukSearchEngine ); +#endif + } else if ( engineName == QLatin1String( "Agent" ) ) { + mEngines.append( new AgentSearchEngine ); + } else { + akError() << "Unknown search engine type: " << engineName; + } + } + + loadSearchPlugins(); + + new SearchManagerAdaptor( this ); + QDBusConnection::sessionBus().registerObject( + QLatin1String( "/SearchManager" ), + this, + QDBusConnection::ExportAdaptors ); + + + // 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, SIGNAL(timeout()), + this, SLOT(searchUpdateTimeout()) ); +} + +SearchManager::~SearchManager() +{ + qDeleteAll( mEngines ); + DataStore::self()->close(); + 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 desktopFiles = dir.entryList( QStringList() << QLatin1String( "*.desktop" ), QDir::Files ); + qDebug() << "SEARCH MANAGER: searching in " << pluginDir + QLatin1String( "/akonadi" ) << ":" << desktopFiles; + + Q_FOREACH ( const QString &desktopFileName, desktopFiles ) { + QSettings desktop( pluginDir + QLatin1String( "/akonadi/" ) + desktopFileName, QSettings::IniFormat ); + desktop.beginGroup( QLatin1String( "Desktop Entry" ) ); + if ( desktop.value( QLatin1String( "Type" ) ).toString() != QLatin1String( "AkonadiSearchPlugin" ) ) { + continue; + } + + const QString libraryName = desktop.value( QLatin1String( "X-Akonadi-Library" ) ).toString(); + if ( loadedPlugins.contains( libraryName ) ) { + qDebug() << "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 ) { + qDebug() << desktopFileName << "skipped because of AKONADI_OVERRIDE_SEARCHPLUGIN"; + continue; + } + + // When there's no override, only load plugins enabled by default + } else if ( !desktop.value( QLatin1String( "X-Akonadi-LoadByDefault" ), true ).toBool() ) { + continue; + } + + const QString pluginFile = XdgBaseDirs::findPluginFile( libraryName, QStringList() << pluginDir + QLatin1String( "/akonadi") ); + QPluginLoader loader( pluginFile ); + if ( !loader.load() ) { + akError() << "Failed to load search plugin" << libraryName << ":" << loader.errorString(); + continue; + } + + AbstractSearchPlugin *plugin = qobject_cast( loader.instance() ); + if ( !plugin ) { + akError() << libraryName << "is not a valid Akonadi search plugin"; + continue; + } + + qDebug() << "SearchManager: loaded search plugin" << libraryName; + mPlugins << plugin; + loadedPlugins << libraryName; + } + } +} + +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( QWaitCondition*, 0 ) ); +} + +void SearchManager::updateSearch( const Collection &collection ) +{ + QMutex mutex; + QWaitCondition cond; + + mLock.lock(); + if ( mUpdatingCollections.contains( collection.id() ) ) { + mLock.unlock(); + return; + } + mUpdatingCollections.insert( collection.id() ); + mLock.unlock(); + + QMetaObject::invokeMethod( this, "updateSearchImpl", + Qt::QueuedConnection, + Q_ARG( Collection, collection ), + Q_ARG( QWaitCondition*, &cond ) ); + + // Now wait for updateSearchImpl to wake us. + mutex.lock(); + cond.wait( &mutex ); + mutex.unlock(); + + mLock.lock(); + mUpdatingCollections.remove( collection.id() ); + mLock.unlock(); +} + +#define wakeUpCaller(cond) \ + if (cond) { \ + cond->wakeAll(); \ + } + +void SearchManager::updateSearchImpl( const Collection &collection, QWaitCondition *cond ) +{ + if ( collection.queryString().size() >= 32768 ) { + qWarning() << "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( QLatin1String( AKONADI_PARAM_REMOTE ) ); + bool recursive = queryAttributes.contains( QLatin1String( AKONADI_PARAM_RECURSIVE ) ); + + + QStringList queryMimeTypes; + Q_FOREACH ( const MimeType &mt, collection.mimeTypes() ) { + queryMimeTypes << mt.name(); + } + + QVector queryCollections, queryAncestors; + if ( collection.queryCollections().isEmpty() ) { + queryAncestors << 0; + recursive = true; + } else { + Q_FOREACH ( const QString &colId, collection.queryCollections().split( QLatin1Char( ' ' ) ) ) { + queryAncestors << colId.toLongLong(); + } + } + + if ( recursive ) { + queryCollections = SearchHelper::listCollectionsRecursive( queryAncestors, queryMimeTypes ); + } else { + queryCollections = queryAncestors; + } + + //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, SIGNAL(resultsAvailable(QSet)), + this, SLOT(searchUpdateResultsAvailable(QSet)) ); + 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; + } + } + } + + qDebug() << "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; + Q_FOREACH ( qint64 id, newMatches ) { + newMatchesVariant << id; + Collection::addPimItem( collection.id(), id ); + } + + qDebug() << "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/akonadi/server/src/search/searchmanager.h b/akonadi/server/src/search/searchmanager.h new file mode 100644 index 00000000..867c80a0 --- /dev/null +++ b/akonadi/server/src/search/searchmanager.h @@ -0,0 +1,144 @@ +/* + 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 +#include +#include +#include + +#include + +class QWaitCondition; +class QTimer; + +namespace Akonadi { + +class AbstractSearchPlugin; + +namespace Server { + +class NotificationCollector; +class AbstractSearchEngine; +class Collection; + + +class SearchManagerThread : public QThread +{ + public: + SearchManagerThread( const QStringList &searchEngines, QObject *parent = 0 ); + ~SearchManagerThread(); + + void run(); + + private: + QStringList mSearchEngines; +}; + + +/** + * SearchManager creates and deletes persistent searches for all currently + * active search engines. + */ +class SearchManager : public QObject +{ + Q_OBJECT + Q_CLASSINFO( "D-Bus Interface", "org.freedesktop.Akonadi.SearchManager" ) + + friend class SearchManagerThread; + + public: + /** Create a new search manager with the given @p searchEngines. */ + explicit SearchManager( QObject *parent = 0 ); + ~SearchManager(); + + /** + * Returns a global instance of the search manager. + */ + static SearchManager *instance(); + + /** + * 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 ); + + /** + * 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(); + + 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 + * synchrounously, we can pass in a QWaitCondition that the code will wake up + * once the search update is completed. + */ + void updateSearchImpl( const Collection &collection, QWaitCondition *cond ); + + protected: + void init( const QStringList &searchEngines ); + + private: + void loadSearchPlugins(); + + static SearchManager *sInstance; + + QVector mEngines; + QVector mPlugins; + + QTimer *mSearchUpdateTimer; + + QMutex mLock; + QSet mUpdatingCollections; + +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/search/searchrequest.cpp b/akonadi/server/src/search/searchrequest.cpp new file mode 100644 index 00000000..614017ee --- /dev/null +++ b/akonadi/server/src/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 "akdebug.h" +#include "xdgbasedirs_p.h" + +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/akonadi/server/src/search/searchrequest.h b/akonadi/server/src/search/searchrequest.h new file mode 100644 index 00000000..8779e73d --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/search/searchtaskmanager.cpp b/akonadi/server/src/search/searchtaskmanager.cpp new file mode 100644 index 00000000..40eea224 --- /dev/null +++ b/akonadi/server/src/search/searchtaskmanager.cpp @@ -0,0 +1,323 @@ +/* + 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 "akdebug.h" +#include "akdbus.h" +#include "connection.h" +#include "storage/selectquerybuilder.h" +#include "dbusconnectionpool.h" +#include + +#include +#include +#include + +using namespace Akonadi::Server; + +SearchTaskManager *SearchTaskManager::sInstance = 0; + +SearchTaskManager::SearchTaskManager() + : QObject() + , mShouldStop( false ) +{ + sInstance = this; + + QTimer::singleShot(0, this, SLOT(searchLoop()) ); +} + +SearchTaskManager::~SearchTaskManager() +{ + mInstancesLock.lock(); + qDeleteAll( mInstances ); + mInstancesLock.unlock(); +} + +SearchTaskManager* SearchTaskManager::instance() +{ + Q_ASSERT( sInstance ); + return sInstance; +} + +void SearchTaskManager::stop() +{ + QMutexLocker locker( &mLock ); + mShouldStop = true; + mWait.wakeAll(); +} + +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; + 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( AkDBus::serviceName( AkDBus::Control ), QLatin1String( "/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/akonadi/server/src/search/searchtaskmanager.h b/akonadi/server/src/search/searchtaskmanager.h new file mode 100644 index 00000000..9b7972b9 --- /dev/null +++ b/akonadi/server/src/search/searchtaskmanager.h @@ -0,0 +1,119 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include "exception.h" +#include "agentmanagerinterface.h" + +namespace Akonadi { +namespace Server { + +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 QObject +{ + 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; + SearchTaskManager(); + void stop(); + bool mShouldStop; + + TasksMap::Iterator cancelRunningTask( TasksMap::Iterator &iter ); + bool allResourceTasksCompleted( SearchTask* ) const; + + QMap mInstances; + QMutex mInstancesLock; + + QWaitCondition mWait; + QMutex mLock; + + QVector mTasklist; + + QMap mRunningTasks; + QVector mPendingResults; + + friend class SearchTaskManagerThread; +}; + +AKONADI_EXCEPTION_MAKE_INSTANCE( SearchException ); + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SEARCHTASKMANAGER_H diff --git a/akonadi/server/src/search/searchtaskmanagerthread.cpp b/akonadi/server/src/search/searchtaskmanagerthread.cpp new file mode 100644 index 00000000..8cfaa956 --- /dev/null +++ b/akonadi/server/src/search/searchtaskmanagerthread.cpp @@ -0,0 +1,43 @@ +/* + 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 "searchtaskmanagerthread.h" +#include "searchtaskmanager.h" + +#include + +using namespace Akonadi::Server; + +SearchTaskManagerThread::SearchTaskManagerThread( QObject *parent ) + : QThread(parent) +{ + // make sure we are created from the main thread, ie. before all other threads start to potentially use us + Q_ASSERT( QThread::currentThread() == QCoreApplication::instance()->thread() ); +} + +void SearchTaskManagerThread::run() +{ + SearchTaskManager mgr; + exec(); +} + +void SearchTaskManagerThread::stop() +{ + SearchTaskManager::instance()->stop(); +} diff --git a/akonadi/server/src/search/searchtaskmanagerthread.h b/akonadi/server/src/search/searchtaskmanagerthread.h new file mode 100644 index 00000000..37d9cf55 --- /dev/null +++ b/akonadi/server/src/search/searchtaskmanagerthread.h @@ -0,0 +1,43 @@ +/* + 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_SEARCHTASKMANAGERTHREAD_H +#define AKONADI_SEARCHTASKMANAGERTHREAD_H + +#include + +namespace Akonadi { +namespace Server { + +class SearchTaskManagerThread : public QThread +{ + Q_OBJECT + public: + SearchTaskManagerThread( QObject *parent = 0 ); + + void stop(); + + protected: + /* reimpl */ void run(); +}; + +} // namespace Server +} // namespace Akonadi + +#endif // AKONADI_SEARCHTASKMANAGERTHREAD_H diff --git a/akonadi/server/src/storage/CMakeLists.txt b/akonadi/server/src/storage/CMakeLists.txt new file mode 100644 index 00000000..041e98ce --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/akonadi-mysql-client.sh b/akonadi/server/src/storage/akonadi-mysql-client.sh new file mode 100755 index 00000000..ee2b81a3 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/akonadi-mysql-server.sh b/akonadi/server/src/storage/akonadi-mysql-server.sh new file mode 100755 index 00000000..adb4da10 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/akonadidb.qrc b/akonadi/server/src/storage/akonadidb.qrc new file mode 100644 index 00000000..b1ab0612 --- /dev/null +++ b/akonadi/server/src/storage/akonadidb.qrc @@ -0,0 +1,5 @@ + + + dbupdate.xml + + diff --git a/akonadi/server/src/storage/akonadidb.xml b/akonadi/server/src/storage/akonadidb.xml new file mode 100644 index 00000000..4ab487bb --- /dev/null +++ b/akonadi/server/src/storage/akonadidb.xml @@ -0,0 +1,218 @@ + + + + + + + + + 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/akonadi/server/src/storage/akonadidb.xsd b/akonadi/server/src/storage/akonadidb.xsd new file mode 100644 index 00000000..09974db3 --- /dev/null +++ b/akonadi/server/src/storage/akonadidb.xsd @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/server/src/storage/collectionqueryhelper.cpp b/akonadi/server/src/storage/collectionqueryhelper.cpp new file mode 100644 index 00000000..1aaab08a --- /dev/null +++ b/akonadi/server/src/storage/collectionqueryhelper.cpp @@ -0,0 +1,162 @@ +/* + 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 "libs/imapset_p.h" +#include "handler/scope.h" +#include "handler.h" +#include "queryhelper.h" + +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::None || 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.ridChain(), 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 QStringList &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 ) ); + qb.addValueCondition( Collection::resourceIdColumn(), Query::Equals, resId ); + if ( !qb.exec() ) { + throw HandlerException( "Unable to execute query" ); + } + Collection::List results = qb.result(); + 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.scope() == Scope::None ) && scope.uidSet().intervals().count() == 1 ) { + const ImapInterval i = scope.uidSet().intervals().first(); + 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/akonadi/server/src/storage/collectionqueryhelper.h b/akonadi/server/src/storage/collectionqueryhelper.h new file mode 100644 index 00000000..cdaaaf9f --- /dev/null +++ b/akonadi/server/src/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 "handler/scope.h" + +namespace Akonadi { + +class ImapSet; + +namespace Server { + +class Connection; +class QueryBuilder; +class Scope; + +/** + 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 QStringList &ridChain, Resource::Id resId ); + + /** + Returns an existing collection specified by the given scope. If that does not + specify exactly one valid collection, an exception is thrwon. + */ + Collection singleCollectionFromScope( const Scope &scope, Connection *connection ); +} + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/storage/countquerybuilder.h b/akonadi/server/src/storage/countquerybuilder.h new file mode 100644 index 00000000..2269f57a --- /dev/null +++ b/akonadi/server/src/storage/countquerybuilder.h @@ -0,0 +1,87 @@ +/* + 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 "akdebug.h" + +#include +#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( QLatin1String( "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 = QLatin1String( "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/akonadi/server/src/storage/datastore.cpp b/akonadi/server/src/storage/datastore.cpp new file mode 100644 index 00000000..ae78baba --- /dev/null +++ b/akonadi/server/src/storage/datastore.cpp @@ -0,0 +1,1332 @@ +/*************************************************************************** + * 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 "xdgbasedirs_p.h" +#include "akdebug.h" +#include "parthelper.h" +#include "libs/protocol_p.h" +#include "handler.h" +#include "collectionqueryhelper.h" +#include "akonadischema.h" +#include "parttypehelper.h" +#include "querycache.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 ) +{ + open(); + 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, SIGNAL(timeout()), + this, SLOT(sendKeepAliveQuery()) ); + m_keepAliveTimer->start(); + } +} + +DataStore::~DataStore() +{ + 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 ); +} + +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( m_database, &schema ); + if ( !initializer->run() ) { + akError() << initializer->errorMsg(); + return false; + } + s_hasForeignKeyConstraints = initializer->hasForeignKeyConstraints(); + + if ( QFile::exists( QLatin1String( ":dbupdate.xml" ) ) ) { + DbUpdater updater( m_database, QLatin1String( ":dbupdate.xml" ) ); + if ( !updater.run() ) { + return false; + } + } else { + qWarning() << "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 ); + + 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(); +} + +/* --- ItemFlags ----------------------------------------------------- */ + +bool DataStore::setItemsFlags( const PimItem::List &items, const QVector &flags, + bool *flagsChanged, bool silent ) +{ + QSet removedFlags; + QSet addedFlags; + QVariantList insIds; + QVariantList insFlags; + Query::Condition delConds( Query::Or ); + + setBoolPtr( flagsChanged, false ); + + Q_FOREACH ( const PimItem &item, items ) { + Q_FOREACH ( const Flag &flag, item.flags() ) { + 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 ( !item.flags().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 ); + } + + 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; + 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, bool silent ) +{ + QSet removedFlags; + QVariantList itemsIds; + QVariantList flagsIds; + + setBoolPtr( flagsChanged, false ); + + 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 ); + } + } + + 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 ) { + Q_FOREACH ( const Tag &tag, item.tags() ) { + 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 ( !item.tags().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; + 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 ); + + 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; +} + +/* --- ItemParts ----------------------------------------------------- */ + +bool DataStore::removeItemParts( const PimItem &item, const QList &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.toSet() ); + 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() ) { + PartHelper::removeFile( PartHelper::resolveAbsolutePath( qb.query().value( 0 ).value() ) ); + } + } 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( QLatin1String( "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; + QList pimItems; + 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... + QList< QByteArray > parts; + parts << AKONADI_ATTRIBUTE_HIDDEN; + + return removeItemParts( pimItem, parts ); +} + +bool DataStore::unhideAllPimItems() +{ + if ( !m_dbOpened ) { + return false; + } + + akDebug() << "DataStore::unhideAllPimItems()"; + + try { + return PartHelper::remove( Part::partTypeIdFullColumnName(), PartTypeHelper::fromName( "ATR", "HIDDEN" ).id() ); + } catch ( ... ) {} // we can live with this failing + + return false; +} + +bool DataStore::cleanupPimItems( const PimItem::List &items ) +{ + // 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)", + QString::fromLatin1( "%1\nDriver said: %2\nDatabase said:%3" ) + .arg( QString::fromLatin1( actionDescription ) ) + .arg( m_database.lastError().driverText() ) + .arg( 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)", + QString::fromLatin1( "%1: %2" ) + .arg( QString::fromLatin1( actionDescription ) ) + .arg( query.lastError().text() ) ); +} + +// static +QString DataStore::dateTimeFromQDateTime( const QDateTime &dateTime ) +{ + QDateTime utcDateTime = dateTime; + if ( utcDateTime.timeSpec() != Qt::UTC ) { + utcDateTime.toUTC(); + } + return utcDateTime.toString( QLatin1String( "yyyy-MM-dd hh:mm:ss" ) ); +} + +// static +QDateTime DataStore::dateTimeToQDateTime( const QByteArray &dateTime ) +{ + return QDateTime::fromString( QString::fromLatin1( dateTime ), QLatin1String( "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 QVariantList values = query.boundValues().values(); + for (int i = 0; i < values.size(); ++i) { + copiedQuery.bindValue(i, values[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( QLatin1String( "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 ) { + qWarning() << "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 ) { + qWarning() << "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( QLatin1String( "SELECT 1" ) ); + } +} diff --git a/akonadi/server/src/storage/datastore.h b/akonadi/server/src/storage/datastore.h new file mode 100644 index 00000000..395b227c --- /dev/null +++ b/akonadi/server/src/storage/datastore.h @@ -0,0 +1,348 @@ +/*************************************************************************** + * 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 databse + 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 databse 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(); + + /* --- ItemFlags ----------------------------------------------------- */ + virtual bool setItemsFlags( const PimItem::List &items, const QVector &flags, bool *flagsChanged = 0, 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, 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 ); + + /* --- ItemParts ----------------------------------------------------- */ + virtual bool removeItemParts( const PimItem &item, const QList &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 QLatin1String( "/" ); } + + /** + 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. + */ + QSqlDatabase database() const { return m_database; } + + /** + Sets the current session id. + */ + void setSessionId( const QByteArray &sessionId ) { mSessionId = sessionId; } + +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/akonadi/server/src/storage/dbconfig.cpp b/akonadi/server/src/storage/dbconfig.cpp new file mode 100644 index 00000000..892c023b --- /dev/null +++ b/akonadi/server/src/storage/dbconfig.cpp @@ -0,0 +1,129 @@ +/* + 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 = 0; + +DbConfig::DbConfig() +{ + const QString serverConfigFile = AkStandardDirs::serverConfigFile( XdgBaseDirs::ReadWrite ); + QSettings settings( serverConfigFile, QSettings::IniFormat ); + + mSizeThreshold = 4096; + const QVariant value = settings.value( QLatin1String( "General/SizeThreshold" ), mSizeThreshold ); + if ( value.canConvert() ) { + mSizeThreshold = value.value(); + } else { + mSizeThreshold = 0; + } + + if ( mSizeThreshold < 0 ) { + mSizeThreshold = 0; + } +} + +DbConfig::~DbConfig() +{ +} + +DbConfig *DbConfig::configuredDatabase() +{ + if ( !s_DbConfigInstance ) { + const QString serverConfigFile = AkStandardDirs::serverConfigFile( XdgBaseDirs::ReadWrite ); + QSettings settings( serverConfigFile, QSettings::IniFormat ); + + // determine driver to use + QString driverName = settings.value( QLatin1String( "General/Driver" ) ).toString(); + if ( driverName.isEmpty() ) { + driverName = QLatin1String( AKONADI_DATABASE_BACKEND ); + // when using the default, write it explicitly, in case the default changes later + settings.setValue( QLatin1String( "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(); + akFatal(); + } + + s_DbConfigInstance->init( settings ); + } + + return s_DbConfigInstance; +} + +void DbConfig::startInternalServer() +{ + // do nothing +} + +void DbConfig::stopInternalServer() +{ + // do nothing +} + +void DbConfig::setup() +{ + // do nothing +} + +qint64 DbConfig::sizeThreshold() const +{ + return mSizeThreshold; +} + +QString DbConfig::defaultDatabaseName() +{ + if ( !AkApplication::hasInstanceIdentifier() ) { + return QLatin1String( "akonadi" ); + } + return QLatin1Literal( "akonadi_" ) % AkApplication::instanceIdentifier(); +} + +void DbConfig::initSession( const QSqlDatabase &database ) +{ + Q_UNUSED( database ); +} diff --git a/akonadi/server/src/storage/dbconfig.h b/akonadi/server/src/storage/dbconfig.h new file mode 100644 index 00000000..9b75e2b3 --- /dev/null +++ b/akonadi/server/src/storage/dbconfig.h @@ -0,0 +1,121 @@ +/* + 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 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 void 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(); + + private: + qint64 mSizeThreshold; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/storage/dbconfigmysql.cpp b/akonadi/server/src/storage/dbconfigmysql.cpp new file mode 100644 index 00000000..10f99db7 --- /dev/null +++ b/akonadi/server/src/storage/dbconfigmysql.cpp @@ -0,0 +1,428 @@ +/* + 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 + +using namespace Akonadi; +using namespace Akonadi::Server; + +#define MYSQL_MIN_MAJOR 5 +#define MYSQL_MIN_MINOR 1 + +DbConfigMysql::DbConfigMysql() + : mDatabaseProcess( 0 ) +{ +} + +QString DbConfigMysql::driverName() const +{ + return QLatin1String( "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( AkStandardDirs::saveDir( "data", QLatin1String( "db_misc" ) ) ); +#endif + + const bool defaultInternalServer = true; +#ifdef MYSQLD_EXECUTABLE + if ( QFile::exists( QLatin1String( MYSQLD_EXECUTABLE ) ) ) { + defaultServerPath = QLatin1String( MYSQLD_EXECUTABLE ); + } +#endif + const QStringList mysqldSearchPath = QStringList() + << QLatin1String( "/usr/sbin" ) + << QLatin1String( "/usr/local/sbin" ) + << QLatin1String( "/usr/local/libexec" ) + << QLatin1String( "/usr/libexec" ) + << QLatin1String( "/opt/mysql/libexec" ) + << QLatin1String( "/opt/local/lib/mysql5/bin" ) + << QLatin1String( "/opt/mysql/sbin" ); + if ( defaultServerPath.isEmpty() ) { + defaultServerPath = XdgBaseDirs::findExecutableFile( QLatin1String( "mysqld" ), mysqldSearchPath ); + } + + const QString mysqladminPath = XdgBaseDirs::findExecutableFile( QLatin1String( "mysqladmin" ), mysqldSearchPath ); + if ( !mysqladminPath.isEmpty() ) { +#ifndef Q_OS_WIN + defaultCleanShutdownCommand = QString::fromLatin1( "--defaults-file=%1/mysql.conf %2 shutdown --socket=%3/mysql.socket" ) + .arg( AkStandardDirs::saveDir( "data" ) ) + .arg( mysqladminPath ) + .arg( socketDirectory ); +#else + defaultCleanShutdownCommand = QString::fromLatin1( "%1 shutdown --shared-memory" ).arg( mysqladminPath ); +#endif + } + + mMysqlInstallDbPath = XdgBaseDirs::findExecutableFile( QLatin1String( "mysql_install_db" ), mysqldSearchPath ); + akDebug() << "Found mysql_install_db: " << mMysqlInstallDbPath; + + mMysqlCheckPath = XdgBaseDirs::findExecutableFile( QLatin1String( "mysqlcheck" ), mysqldSearchPath ); + akDebug() << "Found mysqlcheck: " << mMysqlCheckPath; + + mInternalServer = settings.value( QLatin1String( "QMYSQL/StartServer" ), defaultInternalServer ).toBool(); +#if !(defined Q_WS_WIN) + if ( mInternalServer ) { + defaultOptions = QString::fromLatin1( "UNIX_SOCKET=%1/mysql.socket" ).arg( socketDirectory ); + } +#endif + + // read settings for current driver + settings.beginGroup( driverName() ); + mDatabaseName = settings.value( QLatin1String( "Name" ), defaultDatabaseName() ).toString(); + mHostName = settings.value( QLatin1String( "Host" ), defaultHostName ).toString(); + mUserName = settings.value( QLatin1String( "User" ) ).toString(); + mPassword = settings.value( QLatin1String( "Password" ) ).toString(); + mConnectionOptions = settings.value( QLatin1String( "Options" ), defaultOptions ).toString(); + mServerPath = settings.value( QLatin1String( "ServerPath" ), defaultServerPath ).toString(); + mCleanServerShutdownCommand = settings.value( QLatin1String( "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 = QLatin1String( "akonadi" ); + } + if ( mInternalServer && ( mServerPath.isEmpty() || !QFile::exists( mServerPath ) ) ) { + mServerPath = defaultServerPath; + } + + // store back the default values + settings.beginGroup( driverName() ); + settings.setValue( QLatin1String( "Name" ), mDatabaseName ); + settings.setValue( QLatin1String( "Host" ), mHostName ); + settings.setValue( QLatin1String( "Options" ), mConnectionOptions ); + if ( !mServerPath.isEmpty() ) { + settings.setValue( QLatin1String( "ServerPath" ), mServerPath ); + } + settings.setValue( QLatin1String( "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; +} + +void DbConfigMysql::startInternalServer() +{ + const QString mysqldPath = mServerPath; + + const QString akDir = AkStandardDirs::saveDir( "data" ); + const QString dataDir = AkStandardDirs::saveDir( "data", QLatin1String( "db_data" ) ); +#ifndef Q_OS_WIN + const QString socketDirectory = Utils::preferredSocketDirectory( AkStandardDirs::saveDir( "data", QLatin1String( "db_misc" ) ) ); +#endif + + // generate config file + const QString globalConfig = XdgBaseDirs::findResourceFile( "config", QLatin1String( "akonadi/mysql-global.conf" ) ); + const QString localConfig = XdgBaseDirs::findResourceFile( "config", QLatin1String( "akonadi/mysql-local.conf" ) ); + const QString actualConfig = AkStandardDirs::saveDir( "data" ) + QLatin1String( "/mysql.conf" ); + if ( globalConfig.isEmpty() ) { + akFatal() << "Did not find MySQL server default configuration (mysql-global.conf)"; + } + +#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 + + 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"; + akFatal() << "or the target file (mysql.conf) could not be written."; + } + } + + // 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() ) { + akFatal() << "Akonadi server was not able to create database data directory"; + } + + if ( akDir.isEmpty() ) { + akFatal() << "Akonadi server was not able to create database log directory"; + } + +#ifndef Q_OS_WIN + if ( socketDirectory.isEmpty() ) { + akFatal() << "Akonadi server was not able to create database misc directory"; + } + + // the socket path must not exceed 103 characters, so check for max dir length right away + if ( socketDirectory.length() >= 90 ) { + akFatal() << "MySQL cannot deal with a socket path this long. Path was: " << socketDirectory; + } +#endif + + // move mysql error log file out of the way + const QFileInfo errorLog( dataDir + QDir::separator() + QString::fromLatin1( "mysql.err" ) ); + if ( errorLog.exists() ) { + QFile logFile( errorLog.absoluteFilePath() ); + QFile oldLogFile( dataDir + QDir::separator() + QString::fromLatin1( "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", QLatin1String( "akonadi/mysql-global.conf" ) ); + if ( QDir( dataDir ).entryList( QDir::NoDotAndDotDot | QDir::AllEntries ).isEmpty() && !mMysqlInstallDbPath.isEmpty() ) { + const QStringList arguments = QStringList() << QString::fromLatin1( "--force" ) << QString::fromLatin1( "--defaults-file=%1" ).arg( confFile ) << QString::fromLatin1( "--datadir=%1/" ).arg( dataDir ); + QProcess::execute( mMysqlInstallDbPath, arguments ); + } + + // clear mysql ib_logfile's in case innodb_log_file_size option changed in last confUpdate + if ( confUpdate ) { + QFile( dataDir + QDir::separator() + QString::fromLatin1( "ib_logfile0" ) ).remove(); + QFile( dataDir + QDir::separator() + QString::fromLatin1( "ib_logfile1" ) ).remove(); + } + + // synthesize the mysqld command + QStringList arguments; + arguments << QString::fromLatin1( "--defaults-file=%1/mysql.conf" ).arg( akDir ); + arguments << QString::fromLatin1( "--datadir=%1/" ).arg( dataDir ); +#ifndef Q_WS_WIN + arguments << QString::fromLatin1( "--socket=%1/mysql.socket" ).arg( socketDirectory ); +#else + arguments << QString::fromLatin1( "--shared-memory" ); +#endif + + if ( mysqldPath.isEmpty() ) { + akError() << "mysqld not found. Please verify your installation"; + return; + } + mDatabaseProcess = new QProcess; + mDatabaseProcess->start( mysqldPath, arguments ); + if ( !mDatabaseProcess->waitForStarted() ) { + akError() << "Could not start database server!"; + akError() << "executable:" << mysqldPath; + akError() << "arguments:" << arguments; + akFatal() << "process error:" << mDatabaseProcess->errorString(); + } + + const QLatin1String initCon( "initConnection" ); + { + QSqlDatabase db = QSqlDatabase::addDatabase( QLatin1String( "QMYSQL" ), initCon ); + apply( db ); + + db.setDatabaseName( QString() ); // might not exist yet, then connecting to the actual db will fail + if ( !db.isValid() ) { + akFatal() << "Invalid database object during database server startup"; + } + + 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:" << mysqldPath; + akError() << "arguments:" << arguments; + akError() << "stdout:" << mDatabaseProcess->readAllStandardOutput(); + akError() << "stderr:" << mDatabaseProcess->readAllStandardError(); + akError() << "exit code:" << mDatabaseProcess->exitCode(); + akFatal() << "process error:" << mDatabaseProcess->errorString(); + } + } + + if ( opened ) { + if ( !mMysqlCheckPath.isEmpty() ) { + const QStringList arguments = QStringList() << QString::fromLatin1( "--defaults-file=%1/mysql.conf" ).arg( akDir ) + << QLatin1String( "--check-upgrade" ) + << QLatin1String( "--all-databases" ) + << QLatin1String( "--auto-repair" ) +#ifndef Q_OS_WIN + << QString::fromLatin1( "--socket=%1/mysql.socket" ).arg( socketDirectory ) +#endif + ; + QProcess::execute( mMysqlCheckPath, arguments ); + } + + // Verify MySQL version + { + QSqlQuery query( db ); + if ( !query.exec( QString::fromLatin1( "SELECT VERSION()" ) ) || !query.first() ) { + akError() << "Failed to verify database server version"; + akError() << "Query error:" << query.lastError().text(); + akFatal() << "Database error:" << db.lastError().text(); + } + + const QString version = query.value( 0 ).toString(); + const QStringList versions = version.split( QLatin1Char( '.' ), QString::SkipEmptyParts ); + if ( versions.count() < 3 ) { + akFatal() << "Invalid database server version: " << version; + } + + 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:" << QString::fromLatin1( "%1.%2" ).arg( versions[0], versions[1] ); + akError() << "Minimum required version:" << QString::fromLatin1( "%1.%2" ).arg( MYSQL_MIN_MAJOR ).arg( MYSQL_MIN_MINOR ); + akFatal() << "Please update your MySQL database server"; + } else { + akDebug() << "MySQL version OK" + << "(required" << QString::fromLatin1( "%1.%2" ).arg( MYSQL_MIN_MAJOR ).arg( MYSQL_MIN_MINOR ) + << ", available" << QString::fromLatin1( "%1.%2" ).arg( versions[0], versions[1] ) << ")"; + } + } + + { + QSqlQuery query( db ); + if ( !query.exec( QString::fromLatin1( "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( QLatin1String( "CREATE DATABASE akonadi" ) ) ) { + akError() << "Failed to create database"; + akError() << "Query error:" << query.lastError().text(); + akFatal() << "Database error:" << db.lastError().text(); + } + } + } // make sure query is destroyed before we close the db + db.close(); + } + } + + QSqlDatabase::removeDatabase( initCon ); +} + +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( QLatin1String( "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED" ) ); +} diff --git a/akonadi/server/src/storage/dbconfigmysql.h b/akonadi/server/src/storage/dbconfigmysql.h new file mode 100644 index 00000000..77b3cc19 --- /dev/null +++ b/akonadi/server/src/storage/dbconfigmysql.h @@ -0,0 +1,95 @@ +/* + 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 void startInternalServer(); + + /** + * This method is called to stop the external server. + */ + virtual void stopInternalServer(); + + /// reimpl + virtual void initSession( const QSqlDatabase &database ); + + private: + QString mDatabaseName; + QString mHostName; + QString mUserName; + QString mPassword; + QString mConnectionOptions; + QString mServerPath; + QString mCleanServerShutdownCommand; + QString mMysqlInstallDbPath; + QString mMysqlCheckPath; + bool mInternalServer; + QProcess *mDatabaseProcess; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/storage/dbconfigpostgresql.cpp b/akonadi/server/src/storage/dbconfigpostgresql.cpp new file mode 100644 index 00000000..ebad22cd --- /dev/null +++ b/akonadi/server/src/storage/dbconfigpostgresql.cpp @@ -0,0 +1,363 @@ +/* + 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 +#include +#include + +#include +#include +#include +#include +#include +#include + + +using namespace Akonadi::Server; + +DbConfigPostgresql::DbConfigPostgresql() +{ +} + +QString DbConfigPostgresql::driverName() const +{ + return QLatin1String( "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( QLatin1String( "QPSQL/StartServer" ), defaultInternalServer ).toBool(); + if ( mInternalServer ) { + QStringList postgresSearchPath; + +#ifdef POSTGRES_PATH + const QString dir( QLatin1String( POSTGRES_PATH ) ); + if ( QDir( dir ).exists() ) { + postgresSearchPath << QLatin1String( POSTGRES_PATH ); + } +#endif + postgresSearchPath << QLatin1String( "/usr/sbin" ) + << QLatin1String( "/usr/local/sbin" ) + << QLatin1String( "/usr/lib/postgresql/8.4/bin" ) + << QLatin1String( "/usr/lib/postgresql/9.0/bin" ) + << QLatin1String( "/usr/lib/postgresql/9.1/bin" ) + << QLatin1String( "/usr/lib/postgresql/9.2/bin" ) + << QLatin1String( "/usr/lib/postgresql/9.3/bin" ); + + defaultServerPath = XdgBaseDirs::findExecutableFile( QLatin1String( "pg_ctl" ), postgresSearchPath ); + defaultInitDbPath = XdgBaseDirs::findExecutableFile( QLatin1String( "initdb" ), postgresSearchPath ); + defaultHostName = Utils::preferredSocketDirectory( AkStandardDirs::saveDir( "data", QLatin1String( "db_misc" ) ) ); + defaultPgData = AkStandardDirs::saveDir( "data", QLatin1String( "db_data" ) ); + } + + // read settings for current driver + settings.beginGroup( driverName() ); + mDatabaseName = settings.value( QLatin1String( "Name" ), defaultDatabaseName() ).toString(); + if ( mDatabaseName.isEmpty() ) { + mDatabaseName = defaultDatabaseName(); + } + mHostName = settings.value( QLatin1String( "Host" ), defaultHostName ).toString(); + if ( mHostName.isEmpty() ) { + mHostName = defaultHostName; + } + // User, password and Options can be empty and still valid, so don't override them + mUserName = settings.value( QLatin1String( "User" ) ).toString(); + mPassword = settings.value( QLatin1String( "Password" ) ).toString(); + mConnectionOptions = settings.value( QLatin1String( "Options" ), defaultOptions ).toString(); + mServerPath = settings.value( QLatin1String( "ServerPath" ), defaultServerPath ).toString(); + if ( mInternalServer && mServerPath.isEmpty() ) { + mServerPath = defaultServerPath; + } + mInitDbPath = settings.value( QLatin1String( "InitDbPath" ), defaultInitDbPath ).toString(); + if ( mInternalServer && mInitDbPath.isEmpty() ) { + mInitDbPath = defaultInitDbPath; + } + mPgData = settings.value( QLatin1String( "PgData" ), defaultPgData ).toString(); + if ( mPgData.isEmpty() ) { + mPgData = defaultPgData; + } + settings.endGroup(); + + // store back the default values + settings.beginGroup( driverName() ); + settings.setValue( QLatin1String( "Name" ), mDatabaseName ); + settings.setValue( QLatin1String( "Host" ), mHostName ); + settings.setValue( QLatin1String( "Options" ), mConnectionOptions ); + settings.setValue( QLatin1String( "ServerPath" ), mServerPath ); + settings.setValue( QLatin1String( "InitDbPath" ), mInitDbPath ); + settings.setValue( QLatin1String( "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 ( !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; +} + +void DbConfigPostgresql::startInternalServer() +{ + // We defined the mHostName to the socket directory, during init + const QString socketDir = mHostName; + + // 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( QString::fromLatin1( "%1/postmaster.pid" ).arg( mPgData ) ); + if ( postmaster.exists() && postmaster.open( QIODevice::ReadOnly ) ) { + qDebug() << "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 + qWarning() << "PostgreSQL for Akonadi is already running, trying to connect to it."; + return; + } + } + proc.close(); + } + + qDebug() << "No postgres process with specified PID is running. Removing the pidfile and starting a new Postgres instance..."; + postmaster.close(); + postmaster.remove(); + } +#endif + + if ( !QFile::exists( QString::fromLatin1( "%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 + + // postgres data directory not initialized yet, so call initdb on it + + // call 'initdb --pgdata=/home/user/.local/share/akonadi/data_db' + const QString command = QString::fromLatin1( "%1" ).arg( mInitDbPath ); + QStringList arguments; + arguments << QString::fromLatin1( "--pgdata=%2" ).arg( mPgData ) + // TODO check locale + << QString::fromLatin1( "--locale=en_US.UTF-8" ); + QProcess::execute( command, arguments ); + } + + // synthesize the postgres command + QStringList arguments; + arguments << QString::fromLatin1( "start") + << QString::fromLatin1( "-w" ) + << QString::fromLatin1( "--timeout=10" ) // default is 60 seconds. + << QString::fromLatin1( "--pgdata=%1" ).arg( mPgData ) + // These options are passed to postgres + // -k - directory for unix domain socket communication + // -h - disable listening for TCP/IP + << QString::fromLatin1( "-o \"-k%1\" -h ''" ).arg( socketDir ); + + QProcess pgCtl; + pgCtl.start( mServerPath, arguments ); + if ( !pgCtl.waitForStarted() ) { + akError() << "Could not start database server!"; + akError() << "executable:" << mServerPath; + akError() << "arguments:" << arguments; + akFatal() << "process error:" << pgCtl.errorString(); + } + + const QLatin1String initCon( "initConnection" ); + { + QSqlDatabase db = QSqlDatabase::addDatabase( QLatin1String( "QPSQL" ), initCon ); + apply( db ); + + // use the default database that is always available + db.setDatabaseName( QLatin1String( "postgres" ) ); + + if ( !db.isValid() ) { + akFatal() << "Invalid database object during database server startup"; + } + + 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(); + akFatal() << "process error:" << pgCtl.errorString(); + } + } + + if ( opened ) { + { + QSqlQuery query( db ); + + // check if the 'akonadi' database already exists + query.exec( QString::fromLatin1( "SELECT 1 FROM pg_catalog.pg_database WHERE datname = '%1'" ).arg( mDatabaseName ) ); + + // if not, create it + if ( !query.first() ) { + if ( !query.exec( QString::fromLatin1( "CREATE DATABASE %1" ).arg( mDatabaseName ) ) ) { + akError() << "Failed to create database"; + akError() << "Query error:" << query.lastError().text(); + akFatal() << "Database error:" << db.lastError().text(); + } + } + } // make sure query is destroyed before we close the db + db.close(); + } + } + + QSqlDatabase::removeDatabase( initCon ); +} + +void DbConfigPostgresql::stopInternalServer() +{ + if ( !checkServerIsRunning() ) { + akDebug() << "Database is no longer running"; + return; + } + + const QString command = QString::fromLatin1( "%1" ).arg( mServerPath ); + + // first, try a FAST shutdown + QStringList arguments; + arguments << QString::fromLatin1( "stop" ) + << QString::fromLatin1( "--pgdata=%1" ).arg( mPgData ) + << QString::fromLatin1( "--mode=fast" ); + QProcess::execute( command, arguments ); + if ( !checkServerIsRunning() ) { + return; + } + + // second, try an IMMEDIATE shutdown + arguments.clear(); + arguments << QString::fromLatin1( "stop" ) + << QString::fromLatin1( "--pgdata=%1" ).arg( mPgData ) + << QString::fromLatin1( "--mode=immediate" ); + QProcess::execute( command, arguments ); + 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 = QString::fromLatin1( "%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."; + + arguments.clear(); + arguments << QString::fromLatin1( "kill" ) + << QString::fromLatin1( "ABRT" ) + << QString::fromLatin1( "%1" ).arg( postmasterPid ); + QProcess::execute( command, arguments ); + } +} + +bool DbConfigPostgresql::checkServerIsRunning() +{ + const QString command = QString::fromLatin1( "%1" ).arg( mServerPath ); + QStringList arguments; + arguments << QString::fromLatin1( "status" ) + << QString::fromLatin1( "--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/akonadi/server/src/storage/dbconfigpostgresql.h b/akonadi/server/src/storage/dbconfigpostgresql.h new file mode 100644 index 00000000..cb3d6182 --- /dev/null +++ b/akonadi/server/src/storage/dbconfigpostgresql.h @@ -0,0 +1,92 @@ +/* + 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 void startInternalServer(); + + /** + * This method is called to stop the external server. + */ + virtual void stopInternalServer(); + + private: + bool checkServerIsRunning(); + + QString mDatabaseName; + QString mHostName; + QString mUserName; + QString mPassword; + QString mConnectionOptions; + QString mServerPath; + QString mInitDbPath; + QString mPgData; + bool mInternalServer; +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/akonadi/server/src/storage/dbconfigsqlite.cpp b/akonadi/server/src/storage/dbconfigsqlite.cpp new file mode 100644 index 00000000..39b52b89 --- /dev/null +++ b/akonadi/server/src/storage/dbconfigsqlite.cpp @@ -0,0 +1,244 @@ +/* + 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 = AkStandardDirs::saveDir( "data" ); + if ( akonadiHomeDir.isEmpty() ) { + akFatal() << "Unable to create directory 'akonadi' in " << XdgBaseDirs::homePath( "data" ) + << "during database initialization"; + } + + akonadiHomeDir += QDir::separator(); + + return akonadiHomeDir; +} + +static QString sqliteDataFile() +{ + const QString akonadiPath = dataDir() + QLatin1String( "akonadi.db" ); + if ( !QFile::exists( akonadiPath ) ) { + QFile file( akonadiPath ); + if ( !file.open( QIODevice::WriteOnly ) ) { + akFatal() << "Unable to create file" << akonadiPath << "during database initialization."; + } + file.close(); + } + + return akonadiPath; +} + +DbConfigSqlite::DbConfigSqlite( Version driverVersion ) + : mDriverVersion( driverVersion ) +{ +} + +QString DbConfigSqlite::driverName() const +{ + if ( mDriverVersion == Default ) { + return QLatin1String( "QSQLITE" ); + } else { + return QLatin1String( "QSQLITE3" ); + } +} + +QString DbConfigSqlite::databaseName() const +{ + return mDatabaseName; +} + +bool DbConfigSqlite::init( QSettings &settings ) +{ + // determine default settings depending on the driver + QString defaultDbName; + + defaultDbName = sqliteDataFile(); + + // read settings for current driver + settings.beginGroup( driverName() ); + mDatabaseName = settings.value( QLatin1String( "Name" ), defaultDbName ).toString(); + mHostName = settings.value( QLatin1String( "Host" ) ).toString(); + mUserName = settings.value( QLatin1String( "User" ) ).toString(); + mPassword = settings.value( QLatin1String( "Password" ) ).toString(); + mConnectionOptions = settings.value( QLatin1String( "Options" ) ).toString(); + settings.endGroup(); + + // store back the default values + settings.beginGroup( driverName() ); + settings.setValue( QLatin1String( "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 ); + } + + 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( QLatin1String( "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( QLatin1String( "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( QLatin1String( "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/akonadi/server/src/storage/dbconfigsqlite.h b/akonadi/server/src/storage/dbconfigsqlite.h new file mode 100644 index 00000000..0070e975 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/dbexception.cpp b/akonadi/server/src/storage/dbexception.cpp new file mode 100644 index 00000000..95df6b92 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/dbexception.h b/akonadi/server/src/storage/dbexception.h new file mode 100644 index 00000000..586ae44e --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/dbinitializer.cpp b/akonadi/server/src/storage/dbinitializer.cpp new file mode 100644 index 00000000..f319cb23 --- /dev/null +++ b/akonadi/server/src/storage/dbinitializer.cpp @@ -0,0 +1,425 @@ +/*************************************************************************** + * 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 ), 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 = QString::fromLatin1( "%1_%2" ).arg( tableDescription.name ).arg( 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 ) +{ + const QString relationTableName = relationDescription.firstTable + + relationDescription.secondTable + + QLatin1String( "Relation" ); + + // translate into a regular table and let checkTable() handle it + TableDescription table; + table.name = relationTableName; + + ColumnDescription column; + column.type = QLatin1String( "qint64" ); + column.allowNull = false; + column.isPrimaryKey = true; + column.onUpdate = ColumnDescription::Cascade; + column.onDelete = ColumnDescription::Cascade; + column.name = relationDescription.firstTable + QLatin1Char( '_' ) + relationDescription.firstColumn; + column.refTable = relationDescription.firstTable; + column.refColumn = relationDescription.firstColumn; + table.columns.push_back( column ); + + column.name = relationDescription.secondTable + QLatin1Char( '_' ) + relationDescription.secondColumn; + column.refTable = relationDescription.secondTable; + column.refColumn = relationDescription.secondColumn; + table.columns.push_back( column ); + + return checkTable( table ); +} + +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 ); + } + + 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 QLatin1String( "INTEGER" ); + } + if ( type == QLatin1String( "qint64" ) ) { + return QLatin1String( "BIGINT" ); + } + if ( type == QLatin1String( "QString" ) ) { + return QLatin1String( "TEXT" ); + } + if ( type == QLatin1String( "QByteArray" ) ) { + return QLatin1String( "LONGBLOB" ); + } + if ( type == QLatin1String( "QDateTime" ) ) { + return QLatin1String( "TIMESTAMP" ); + } + if ( type == QLatin1String( "bool" ) ) { + return QLatin1String( "BOOL" ); + } + if ( type == QLatin1String( "Tristate" ) ) { + return QLatin1String( "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 QLatin1String( "CURRENT_TIMESTAMP" ); + } else if ( type == QLatin1String( "Tristate" ) ) { + if ( value == QLatin1String( "False" ) ) { + return QString::number( Akonadi::Server::Tristate::False ); + } else if ( value == QLatin1String( "True" ) ) { + return QString::number( Akonadi::Server::Tristate::True ); + } else { + return QString::number( Akonadi::Server::Tristate::Undefined ); + } + } + + return value; +} + +QString DbInitializer::buildAddColumnStatement( const TableDescription &tableDescription, const ColumnDescription &columnDescription ) const +{ + return QString::fromLatin1( "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 = QString::fromLatin1( "%1_%2" ).arg( tableDescription.name ).arg( indexDescription.name ); + return QString::fromLatin1( "CREATE %1 INDEX %2 ON %3 (%4)" ) + .arg( indexDescription.isUnique ? QLatin1String( "UNIQUE" ) : QString() ) + .arg( indexName ) + .arg( tableDescription.name ) + .arg( indexDescription.columns.join( QLatin1String( "," ) ) ); +} + +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 QLatin1String( "CASCADE" ); + case ColumnDescription::Restrict: + return QLatin1String( "RESTRICT" ); + case ColumnDescription::SetNull: + return QLatin1String( "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( QLatin1String( ", " ) ) + 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/akonadi/server/src/storage/dbinitializer.h b/akonadi/server/src/storage/dbinitializer.h new file mode 100644 index 00000000..c5b85562 --- /dev/null +++ b/akonadi/server/src/storage/dbinitializer.h @@ -0,0 +1,190 @@ +/*************************************************************************** + * 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 boost::shared_ptr 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/akonadi/server/src/storage/dbinitializer_p.cpp b/akonadi/server/src/storage/dbinitializer_p.cpp new file mode 100644 index 00000000..a214061b --- /dev/null +++ b/akonadi/server/src/storage/dbinitializer_p.cpp @@ -0,0 +1,299 @@ +/*************************************************************************** + * 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 << QString::fromLatin1( "FOREIGN KEY (%1) REFERENCES %2Table(%3) " ) + .arg( columnDescription.name ) + .arg( columnDescription.refTable ) + .arg( columnDescription.refColumn ) + + buildReferentialAction( columnDescription.onUpdate, columnDescription.onDelete ); + } + } + + if ( tableDescription.primaryKeyColumnCount() > 1 ) { + columns.push_back( buildPrimaryKeyStatement( tableDescription ) ); + } + columns << references; + + const QString tableProperties = QLatin1String( " COLLATE=utf8_general_ci DEFAULT CHARSET=utf8" ); + + return QString::fromLatin1( "CREATE TABLE %1 (%2) %3" ).arg( tableDescription.name, columns.join( QLatin1String( ", " ) ), 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 += QString::fromLatin1( " DEFAULT %1" ).arg( defaultValue ); + } + } + + return column; +} + +QString DbInitializerMySql::buildInsertValuesStatement( const TableDescription &tableDescription, const DataDescription &dataDescription ) const +{ + QHash data = dataDescription.data; + QMutableHashIterator it( data ); + while ( it.hasNext() ) { + it.next(); + it.value().replace( QLatin1String( "\\" ), QLatin1String( "\\\\" ) ); + } + + return QString::fromLatin1( "INSERT INTO %1 (%2) VALUES (%3)" ) + .arg( tableDescription.name ) + .arg( QStringList( data.keys() ).join( QLatin1String( "," ) ) ) + .arg( QStringList( data.values() ).join( QLatin1String( "," ) ) ); +} + +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 QString::fromLatin1( "CREATE TABLE %1 (%2)" ).arg( tableDescription.name, columns.join( QLatin1String( ", " ) ) ); +} + +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 += QString::fromLatin1( " DEFAULT %1" ).arg( defaultValue ); + } + } + + return column; +} + +QString DbInitializerSqlite::buildInsertValuesStatement( const TableDescription &tableDescription, const DataDescription &dataDescription ) const +{ + QHash data = dataDescription.data; + QMutableHashIterator it( data ); + while ( it.hasNext() ) { + it.next(); + it.value().replace( QLatin1String( "true" ), QLatin1String( "1" ) ); + it.value().replace( QLatin1String( "false" ), QLatin1String( "0" ) ); + } + + return QString::fromLatin1( "INSERT INTO %1 (%2) VALUES (%3)" ) + .arg( tableDescription.name ) + .arg( QStringList( data.keys() ).join( QLatin1String( "," ) ) ) + .arg( QStringList( data.values() ).join( QLatin1String( "," ) ) ); +} + +//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 QLatin1String( "int8" ); + } + if ( type == QLatin1String( "QByteArray" ) ) { + return QLatin1String( "BYTEA" ); + } + if ( type == QLatin1String( "Tristate" ) ) { + return QLatin1String( "SMALLINT" ); + } + + return DbInitializer::sqlType( type, size ); +} + +QString DbInitializerPostgreSql::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 QString::fromLatin1( "CREATE TABLE %1 (%2)" ).arg( tableDescription.name, columns.join( QLatin1String( ", " ) ) ); +} + +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 += QString::fromLatin1( " DEFAULT %1" ).arg( defaultValue ); + } + } + + return column; +} + +QString DbInitializerPostgreSql::buildInsertValuesStatement( const TableDescription &tableDescription, const DataDescription &dataDescription ) const +{ + QHash data = dataDescription.data; + + return QString::fromLatin1( "INSERT INTO %1 (%2) VALUES (%3)" ) + .arg( tableDescription.name ) + .arg( QStringList( data.keys() ).join( QLatin1String( "," ) ) ) + .arg( QStringList( data.values() ).join( QLatin1String( "," ) ) ); +} + +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/akonadi/server/src/storage/dbinitializer_p.h b/akonadi/server/src/storage/dbinitializer_p.h new file mode 100644 index 00000000..4af93775 --- /dev/null +++ b/akonadi/server/src/storage/dbinitializer_p.h @@ -0,0 +1,70 @@ +/*************************************************************************** + * 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; +}; + +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/akonadi/server/src/storage/dbintrospector.cpp b/akonadi/server/src/storage/dbintrospector.cpp new file mode 100644 index 00000000..3b9de014 --- /dev/null +++ b/akonadi/server/src/storage/dbintrospector.cpp @@ -0,0 +1,120 @@ +/* + 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 ); + for ( int i = 0; i < table.count(); ++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( QLatin1String( "*" ) ); + 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/akonadi/server/src/storage/dbintrospector.h b/akonadi/server/src/storage/dbintrospector.h new file mode 100644 index 00000000..49ef3ed3 --- /dev/null +++ b/akonadi/server/src/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 implemention 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 reimplment 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/akonadi/server/src/storage/dbintrospector_impl.cpp b/akonadi/server/src/storage/dbintrospector_impl.cpp new file mode 100644 index 00000000..ba724cb9 --- /dev/null +++ b/akonadi/server/src/storage/dbintrospector_impl.cpp @@ -0,0 +1,183 @@ +/* + 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 +#include + +using namespace Akonadi::Server; + +//BEGIN MySql + +DbIntrospectorMySql::DbIntrospectorMySql( const QSqlDatabase &database ) + : DbIntrospector( database ) +{ +} + +QString DbIntrospectorMySql::hasIndexQuery( const QString &tableName, const QString &indexName ) +{ + return QString::fromLatin1( "SHOW INDEXES FROM %1 WHERE `Key_name` = '%2'" ) + .arg( tableName ).arg( indexName ); +} + +QVector< DbIntrospector::ForeignKey > DbIntrospectorMySql::foreignKeyConstraints( const QString &tableName ) +{ + QueryBuilder qb( QLatin1String( "information_schema.REFERENTIAL_CONSTRAINTS" ), QueryBuilder::Select ); + qb.addJoin( QueryBuilder::InnerJoin, QLatin1String( "information_schema.KEY_COLUMN_USAGE" ), + QLatin1String( "information_schema.REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME" ), + QLatin1String( "information_schema.KEY_COLUMN_USAGE.CONSTRAINT_NAME" ) ); + qb.addColumn( QLatin1String( "information_schema.REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME" ) ); + qb.addColumn( QLatin1String( "information_schema.KEY_COLUMN_USAGE.COLUMN_NAME" ) ); + qb.addColumn( QLatin1String( "information_schema.KEY_COLUMN_USAGE.REFERENCED_TABLE_NAME" ) ); + qb.addColumn( QLatin1String( "information_schema.KEY_COLUMN_USAGE.REFERENCED_COLUMN_NAME" ) ); + qb.addColumn( QLatin1String( "information_schema.REFERENTIAL_CONSTRAINTS.UPDATE_RULE" ) ); + qb.addColumn( QLatin1String( "information_schema.REFERENTIAL_CONSTRAINTS.DELETE_RULE" ) ); + + qb.addValueCondition( QLatin1String( "information_schema.KEY_COLUMN_USAGE.TABLE_SCHEMA" ), Query::Equals, m_database.databaseName() ); + qb.addValueCondition( QLatin1String( "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 QString::fromLatin1( "SELECT * FROM sqlite_master WHERE type='index' AND tbl_name='%1' AND name='%2';" ) + .arg( tableName ).arg( 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( QLatin1String( TABLE_CONSTRAINTS ".constraint_catalog" ), Query::Equals, + QLatin1String( KEY_COLUMN_USAGE ".constraint_catalog" ) ); + keyColumnUsageCondition.addColumnCondition( QLatin1String( TABLE_CONSTRAINTS ".constraint_schema" ), Query::Equals, + QLatin1String( KEY_COLUMN_USAGE ".constraint_schema" ) ); + keyColumnUsageCondition.addColumnCondition( QLatin1String( TABLE_CONSTRAINTS ".constraint_name" ), Query::Equals, + QLatin1String( KEY_COLUMN_USAGE ".constraint_name" ) ); + + Query::Condition referentialConstraintsCondition( Query::And ); + referentialConstraintsCondition.addColumnCondition( QLatin1String( TABLE_CONSTRAINTS ".constraint_catalog" ), Query::Equals, + QLatin1String( REFERENTIAL_CONSTRAINTS ".constraint_catalog" ) ); + referentialConstraintsCondition.addColumnCondition( QLatin1String( TABLE_CONSTRAINTS ".constraint_schema" ), Query::Equals, + QLatin1String( REFERENTIAL_CONSTRAINTS ".constraint_schema" ) ); + referentialConstraintsCondition.addColumnCondition( QLatin1String( TABLE_CONSTRAINTS ".constraint_name" ), Query::Equals, + QLatin1String( REFERENTIAL_CONSTRAINTS ".constraint_name" ) ); + + Query::Condition constraintColumnUsageCondition( Query::And ); + constraintColumnUsageCondition.addColumnCondition( QLatin1String( REFERENTIAL_CONSTRAINTS ".unique_constraint_catalog" ), Query::Equals, + QLatin1String( CONSTRAINT_COLUMN_USAGE ".constraint_catalog" ) ); + constraintColumnUsageCondition.addColumnCondition( QLatin1String( REFERENTIAL_CONSTRAINTS ".unique_constraint_schema" ), Query::Equals, + QLatin1String( CONSTRAINT_COLUMN_USAGE ".constraint_schema" ) ); + constraintColumnUsageCondition.addColumnCondition( QLatin1String( REFERENTIAL_CONSTRAINTS ".unique_constraint_name" ), Query::Equals, + QLatin1String( CONSTRAINT_COLUMN_USAGE ".constraint_name" ) ); + + QueryBuilder qb( QLatin1String( TABLE_CONSTRAINTS ), QueryBuilder::Select ); + qb.addColumn( QLatin1String( TABLE_CONSTRAINTS ".constraint_name" ) ); + qb.addColumn( QLatin1String( KEY_COLUMN_USAGE ".column_name" ) ); + qb.addColumn( QLatin1String( CONSTRAINT_COLUMN_USAGE ".table_name AS referenced_table" ) ); + qb.addColumn( QLatin1String( CONSTRAINT_COLUMN_USAGE ".column_name AS referenced_column" ) ); + qb.addColumn( QLatin1String( REFERENTIAL_CONSTRAINTS ".update_rule" ) ); + qb.addColumn( QLatin1String( REFERENTIAL_CONSTRAINTS ".delete_rule" ) ); + qb.addJoin( QueryBuilder::LeftJoin, QLatin1String( KEY_COLUMN_USAGE ), keyColumnUsageCondition ); + qb.addJoin( QueryBuilder::LeftJoin, QLatin1String( REFERENTIAL_CONSTRAINTS ), referentialConstraintsCondition ); + qb.addJoin( QueryBuilder::LeftJoin, QLatin1String( CONSTRAINT_COLUMN_USAGE ), constraintColumnUsageCondition ); + qb.addValueCondition( QLatin1String( TABLE_CONSTRAINTS ".constraint_type" ), + Query::Equals, QLatin1String( "FOREIGN KEY" ) ); + qb.addValueCondition( QLatin1String( 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 = QLatin1String( "SELECT indexname FROM pg_catalog.pg_indexes" ); + query += QString::fromLatin1( " WHERE tablename ilike '%1'" ).arg( tableName ); + query += QString::fromLatin1( " AND indexname ilike '%1'" ).arg( indexName ); + query += QString::fromLatin1( " UNION SELECT conname FROM pg_catalog.pg_constraint " ); + query += QString::fromLatin1( " WHERE conname ilike '%1'" ).arg( indexName ); + return query; +} + +//END PostgreSql + diff --git a/akonadi/server/src/storage/dbintrospector_impl.h b/akonadi/server/src/storage/dbintrospector_impl.h new file mode 100644 index 00000000..d076b3de --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/dbtype.cpp b/akonadi/server/src/storage/dbtype.cpp new file mode 100644 index 00000000..7df2fb16 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/dbtype.h b/akonadi/server/src/storage/dbtype.h new file mode 100644 index 00000000..3595604e --- /dev/null +++ b/akonadi/server/src/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 databse 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/akonadi/server/src/storage/dbupdate.xml b/akonadi/server/src/storage/dbupdate.xml new file mode 100644 index 00000000..ddc32c56 --- /dev/null +++ b/akonadi/server/src/storage/dbupdate.xml @@ -0,0 +1,207 @@ + + + + + + + + + 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; + + + diff --git a/akonadi/server/src/storage/dbupdate.xsd b/akonadi/server/src/storage/dbupdate.xsd new file mode 100644 index 00000000..86fdba75 --- /dev/null +++ b/akonadi/server/src/storage/dbupdate.xsd @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akonadi/server/src/storage/dbupdater.cpp b/akonadi/server/src/storage/dbupdater.cpp new file mode 100644 index 00000000..d6368b56 --- /dev/null +++ b/akonadi/server/src/storage/dbupdater.cpp @@ -0,0 +1,462 @@ +/* + 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 "akdebug.h" +#include "akdbus.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 + +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( AkDBus::serviceName( AkDBus::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 = QString::fromLatin1( "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( AkDBus::serviceName( AkDBus::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( QLatin1String( "version" ), QLatin1String( "-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( QLatin1String( "abortOnFailure" ) ) == QLatin1String( "true" ) ); + + QDomElement childElement = updateElement.firstChildElement(); + while ( !childElement.isNull() ) { + if ( childElement.tagName() == QLatin1String( "raw-sql" ) ) { + if ( updateApplicable( childElement.attribute( QLatin1String( "backends" ) ) ) ) { + updateSet.statements << buildRawSqlStatement( childElement ); + } + } else if ( childElement.tagName() == QLatin1String( "complex-update" ) ) { + if ( updateApplicable( childElement.attribute( QLatin1String( "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 = QLatin1String( "mysql" ); + break; + case DbType::PostgreSQL: + currentBackend = QLatin1String( "psql" ); + break; + case DbType::Sqlite: + currentBackend = QLatin1String( "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( QLatin1String( "ALTER TABLE PartTable_old RENAME TO PartTable" ) ); + } + + { + QSqlQuery query( DataStore::self()->database() ); + query.exec( QLatin1String( "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( QLatin1String( "DELETE FROM PartTypeTable" ) ); + } else { // MySQL, PostgreSQL + query.exec( QLatin1String( "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( QLatin1String( "PartTable" ), QueryBuilder::Delete ); + qb.addValueCondition( QLatin1String( "PartTable.name" ), Query::Equals, QLatin1String( "GID" ) ); + qb.exec(); + } + + akDebug() << "Creating a PartTable_new"; + { + TableDescription description; + description.name = QLatin1String( "PartTable_new" ); + + ColumnDescription idColumn; + idColumn.name = QLatin1String( "id" ); + idColumn.type = QLatin1String( "qint64" ); + idColumn.isAutoIncrement = true; + idColumn.isPrimaryKey = true; + description.columns << idColumn; + + ColumnDescription pimItemIdColumn; + pimItemIdColumn.name = QLatin1String( "pimItemId" ); + pimItemIdColumn.type = QLatin1String( "qint64" ); + pimItemIdColumn.allowNull = false; + description.columns << pimItemIdColumn; + + ColumnDescription partTypeIdColumn; + partTypeIdColumn.name = QLatin1String( "partTypeId" ); + partTypeIdColumn.type = QLatin1String( "qint64" ); + partTypeIdColumn.allowNull = false; + description.columns << partTypeIdColumn; + + ColumnDescription dataColumn; + dataColumn.name = QLatin1String( "data" ); + dataColumn.type = QLatin1String( "QByteArray" ); + description.columns << dataColumn; + + ColumnDescription dataSizeColumn; + dataSizeColumn.name = QLatin1String( "datasize" ); + dataSizeColumn.type = QLatin1String( "qint64" ); + dataSizeColumn.allowNull = false; + description.columns << dataSizeColumn; + + ColumnDescription versionColumn; + versionColumn.name = QLatin1String( "version" ); + versionColumn.type = QLatin1String( "int" ); + versionColumn.defaultValue = QLatin1String( "0" ); + description.columns << versionColumn; + + ColumnDescription externalColumn; + externalColumn.name = QLatin1String( "external" ); + externalColumn.type = QLatin1String( "bool" ); + externalColumn.defaultValue = QLatin1String( "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( QLatin1String( "PartTable" ), QueryBuilder::Select ); + qb.setDistinct( true ); + qb.addColumn( QLatin1String( "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( QLatin1String( "PartTypeTable" ), QueryBuilder::Insert ); + qb.setColumnValue( QLatin1String( "ns" ), ns ); + qb.setColumnValue( QLatin1String( "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 = QLatin1String( "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 = QLatin1String( "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 = QLatin1String( "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( QLatin1String( "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( QLatin1String( "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( QLatin1String( "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( QLatin1String( "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( QLatin1String( "ALTER TABLE PartTable RENAME CONSTRAINT parttable_new_pkey TO parttable_pkey" ) ); + query.exec( QLatin1String( "ALTER SEQUENCE parttable_new_id_seq RENAME TO parttable_id_seq" ) ); + query.exec( QLatin1String( "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( QLatin1String( "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/akonadi/server/src/storage/dbupdater.h b/akonadi/server/src/storage/dbupdater.h new file mode 100644 index 00000000..0e0dabaf --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/doxygen-preprocess-entities.sh b/akonadi/server/src/storage/doxygen-preprocess-entities.sh new file mode 100755 index 00000000..e163105d --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/entities-dox.xsl b/akonadi/server/src/storage/entities-dox.xsl new file mode 100644 index 00000000..686b5394 --- /dev/null +++ b/akonadi/server/src/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/akonadi/server/src/storage/entities-header.xsl b/akonadi/server/src/storage/entities-header.xsl new file mode 100644 index 00000000..49669664 --- /dev/null +++ b/akonadi/server/src/storage/entities-header.xsl @@ -0,0 +1,293 @@ + + + + + + + + + +/** + 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 ); + + + /** 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/akonadi/server/src/storage/entities-source.xsl b/akonadi/server/src/storage/entities-source.xsl new file mode 100644 index 00000000..174cf4f7 --- /dev/null +++ b/akonadi/server/src/storage/entities-source.xsl @@ -0,0 +1,649 @@ + + + + + + + +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 bool cacheEnabled; + static QMutex cacheMutex; + + static QHash<qint64, > idCache; + + + static QHash<, > nameCache; + +}; + + +// static members +bool ::Private::cacheEnabled = false; +QMutex ::Private::cacheMutex; + +QHash<qint64, > ::Private::idCache; + + +QHash<, > ::Private::nameCache; + + + +void ::Private::addToCache( const & entry ) +{ + Q_ASSERT( cacheEnabled ); + Q_UNUSED( entry ); + cacheMutex.lock(); + + idCache.insert( entry.id(), entry ); + + + nameCache.insert( entry.name(), entry ); + + cacheMutex.unlock(); +} + + +// 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 + + ::() const +{ + return d->; +} + +void :: +{ + d-> = ; + d->_changed = true; +} + + + +// SQL table information +QString ::tableName() +{ + return QLatin1String( "" ); +} + +QStringList ::columnNames() +{ + QStringList rv; + + rv.append( QLatin1String( "" ) ); + + return rv; +} + +QStringList ::fullColumnNames() +{ + QStringList rv; + + rv.append( QLatin1String( "." ) ); + + return rv; +} + + +QString ::Column() +{ + return QLatin1String( "" ); +} + +QString ::FullColumnName() +{ + return tableName() + QLatin1String( "." ); +} + + + +// count records +int ::count( const QString &column, const QVariant &value ) +{ + return Entity::count<>( column, value ); +} + +// check existence + +bool ::exists( qint64 id ) +{ + if ( Private::cacheEnabled ) { + Private::cacheMutex.lock(); + if ( Private::idCache.contains( id ) ) { + Private::cacheMutex.unlock(); + return true; + } + Private::cacheMutex.unlock(); + } + return count( idColumn(), id ) > 0; +} + + +bool ::exists( const &name ) +{ + if ( Private::cacheEnabled ) { + Private::cacheMutex.lock(); + if ( Private::nameCache.contains( name ) ) { + Private::cacheMutex.unlock(); + return true; + } + Private::cacheMutex.unlock(); + } + return count( nameColumn(), name ) > 0; +} + + + +// result extraction +QVector< > ::extractResult( QSqlQuery & query ) +{ + QVector<> rv; + 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 + +} + + +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 + + +Table + +// data retrieval for n:m relations +QVector<> ::s() const +{ + QSqlDatabase db = DataStore::self()->database(); + if ( !db.isOpen() ) + return QVector<>(); + + QueryBuilder qb( QLatin1String(""), QueryBuilder::Select ); + + qb.addColumn( QLatin1String("." ) ); + + qb.addJoin( QueryBuilder::InnerJoin, QLatin1String(""), + QLatin1String("._"), + QLatin1String(".") ); + qb.addValueCondition( QLatin1String("._"), 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 << "[: " + + << " = " << 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(), 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(), this->() ); + } + + + + qb.addValueCondition( QLatin1String("id"), 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 ) { + Private::cacheMutex.lock(); + + Private::idCache.remove( id() ); + + + Private::nameCache.remove( name() ); + + Private::cacheMutex.unlock(); + } +} + +void ::invalidateCompleteCache() +{ + if ( Private::cacheEnabled ) { + Private::cacheMutex.lock(); + + Private::idCache.clear(); + + + Private::nameCache.clear(); + + Private::cacheMutex.unlock(); + } +} + +void ::enableCache( bool enable ) +{ + Private::cacheEnabled = enable; +} + + + + + + +Relation +Relation + +// SQL table information +QString ::tableName() +{ + return QLatin1String( "" ); +} + +QString ::leftColumn() +{ + return QLatin1String( "_" ); +} + +QString ::leftFullColumnName() +{ + return tableName() + QLatin1String( "." ) + leftColumn(); +} + +QString ::rightColumn() +{ + return QLatin1String( "_" ); +} + +QString ::rightFullColumnName() +{ + return tableName() + QLatin1String( "." ) + rightColumn(); +} + + + diff --git a/akonadi/server/src/storage/entities.xsl b/akonadi/server/src/storage/entities.xsl new file mode 100644 index 00000000..033e2929 --- /dev/null +++ b/akonadi/server/src/storage/entities.xsl @@ -0,0 +1,233 @@ + + + + + + + + + +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 <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 <QLatin1String> +#include <qsqlquery.h> +#include <qsqlerror.h> +#include <qvariant.h> +#include <QtCore/QHash> +#include <QtCore/QMutex> + +using namespace Akonadi::Server; + + + + + + + + + +QVector<QString> Akonadi::Server::allDatabaseTables() +{ + static QVector<QString> allTables = QVector<QString>() + + << QLatin1String( "Table" ) + + + << QLatin1String( "Relation" ) + + ; + return allTables; +} + + + + + + + + + + + + + + + + + const + & + + + + + + + +set( ) + + + + + + + , + + + + + + + + + + if ( Private::cacheEnabled ) { + Private::cacheMutex.lock(); + if ( Private::.contains( ) ) { + const tmp = Private::.value( ); + Private::cacheMutex.unlock(); + return tmp; + } + Private::cacheMutex.unlock(); + } + + QSqlDatabase db = DataStore::self()->database(); + if ( !db.isOpen() ) + return (); + + QueryBuilder qb( tableName(), QueryBuilder::Select ); + qb.addColumns( columnNames() ); + qb.addValueCondition( QLatin1String(""), 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 (); + } + + rv( + + (qb.query().isNull()) ? + () : + + + Utils::variantToString( qb.query().value( ) ) + + + static_cast<Tristate>(qb.query().value( ).value<int>()) + + + qb.query().value( ).value<>() + + + , + + ); + if ( Private::cacheEnabled ) { + Private::addToCache( rv ); + } + return rv; + + + + + + + + + + + + + + + + diff --git a/akonadi/server/src/storage/entity.cpp b/akonadi/server/src/storage/entity.cpp new file mode 100644 index 00000000..91c816d6 --- /dev/null +++ b/akonadi/server/src/storage/entity.cpp @@ -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. * + ***************************************************************************/ + +#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/akonadi/server/src/storage/entity.h b/akonadi/server/src/storage/entity.h new file mode 100644 index 00000000..eb180e45 --- /dev/null +++ b/akonadi/server/src/storage/entity.h @@ -0,0 +1,197 @@ +/*************************************************************************** + * 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 { + + +enum Tristate { + False = 0, + True = 1, + Undefined = 2 +}; + +/** + 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