diff --git a/CMakeLists.txt b/CMakeLists.txt index ba2c411c..54ab9eaa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,154 +1,154 @@ cmake_minimum_required(VERSION 3.0) cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0028 NEW) project(sink VERSION 0.7) option(BUILD_MAILDIR "BUILD_MAILDIR" ON) option(BUILD_DAV "BUILD_DAV" ON) option(CATCH_ERRORS "CATCH_ERRORS" OFF) option(ENABLE_MEMCHECK "Build valgrind tests" OFF) option(ENABLE_ASAN "Enable the address sanitizer" OFF) option(ENABLE_TSAN "Enable the thread sanitizer" OFF) # ECM setup find_package(ECM 1.0.0 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_CURRENT_SOURCE_DIR}/tests ${CMAKE_MODULE_PATH}) include(FeatureSummary) include(GenerateExportHeader) include(CMakePackageConfigHelpers) include(ECMSetupVersion) include(KDEInstallDirs) #Avoid building appstreamtest set(KDE_SKIP_TEST_SETTINGS true) #Pick up rpath settings include(KDECMakeSettings NO_POLICY_SCOPE) #We only have console applications here set(CMAKE_MACOSX_BUNDLE OFF) set(CMAKE_WIN32_EXECUTABLE OFF) ecm_setup_version(PROJECT SOVERSION sink_VERSION_MAJOR VERSION_HEADER sink_version.h ) find_package(Qt5 COMPONENTS REQUIRED Core Concurrent Network Gui Test) -find_package(KF5 COMPONENTS REQUIRED Mime Contacts) +find_package(KF5 COMPONENTS REQUIRED Mime Contacts CalendarCore) find_package(FlatBuffers REQUIRED 1.4.0) find_package(KAsync REQUIRED 0.1.2) find_package(LMDB REQUIRED 0.9) find_package(Xapian REQUIRED 1.4) if (${ENABLE_MEMCHECK}) message("Enabled memcheck") find_program(MEMORYCHECK_COMMAND valgrind) if(NOT MEMORYCHECK_COMMAND) message(FATAL_ERROR "valgrind not found!") endif() set(MEMORYCHECK_COMMAND_OPTIONS "--trace-children=yes --leak-check=full") endif() if (${ENABLE_ASAN}) message("Enabled ASAN") set(SINK_ASAN_FLAG "-fsanitize=address -fPIE -fno-omit-frame-pointer -O1 ") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SINK_ASAN_FLAG}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SINK_ASAN_FLAG}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${SINK_ASAN_FLAG}") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${SINK_ASAN_FLAG}") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${SINK_ASAN_FLAG}") endif() if (${ENABLE_TSAN}) message("Enabled TSAN") set(SINK_TSAN_FLAG "-fsanitize=thread -fPIE -fno-omit-frame-pointer -O1 ") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SINK_TSAN_FLAG}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SINK_TSAN_FLAG}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${SINK_TSAN_FLAG}") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${SINK_TSAN_FLAG}") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${SINK_TSAN_FLAG}") endif() #Clang-format support add_custom_command( OUTPUT format.dummy WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND clang-format -i ${CMAKE_SOURCE_DIR}/**.{cpp,h} ) add_custom_target(format DEPENDS format.dummy) function(generate_flatbuffers _target) foreach(fbs ${ARGN}) #Necessary because we can get relative paths as name, e.g. commands/create_entity get_filename_component(filename ${fbs} NAME) #We first generate into a temporary directory to avoid changing the timestamp of the actual dependency unnecessarily. #Otherwise we'd end up unnecessarily rebuilding the target. add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${fbs}_generated.h COMMAND ${FLATBUFFERS_FLATC_EXECUTABLE} -c -b -o ${CMAKE_CURRENT_BINARY_DIR}/flatbufferstmp ${CMAKE_CURRENT_SOURCE_DIR}/${fbs}.fbs COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/flatbufferstmp/${filename}_generated.h ${CMAKE_CURRENT_BINARY_DIR}/${filename}_generated.h DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${fbs}.fbs ) target_sources(${_target} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/${fbs}_generated.h) set_property(SOURCE ${fbs}_generated.h PROPERTY SKIP_AUTOMOC ON) endforeach(fbs) endfunction(generate_flatbuffers) #Clang-analyze support add_custom_target(analyze) function(add_clang_static_analysis target) get_target_property(SRCs ${target} SOURCES) get_target_property(INCLUDEs ${target} INCLUDE_DIRECTORIES) add_library(${target}_analyze OBJECT EXCLUDE_FROM_ALL ${SRCs}) set_target_properties(${target}_analyze PROPERTIES COMPILE_OPTIONS "--analyze" EXCLUDE_FROM_DEFAULT_BUILD true INCLUDE_DIRECTORIES "${INCLUDEs};${KDE_INSTALL_FULL_INCLUDEDIR}/KF5/" # Had to hardcode include directory to find KAsync includes #COMPILE_FLAGS is deprecated, but the only way that -Xanalyzer isn't erronously deduplicated COMPILE_FLAGS "-Xanalyzer -analyzer-eagerly-assume -Xanalyzer -analyzer-opt-analyze-nested-blocks" ) target_compile_options(${target}_analyze PRIVATE ${Qt5Core_EXECUTABLE_COMPILE_FLAGS})# Necessary to get options such as fPIC add_dependencies(analyze ${target}_analyze) endfunction() set(CMAKE_AUTOMOC ON) if (${CATCH_ERRORS}) add_definitions("-Werror -Wall -Weverything -Wno-unused-function -Wno-cast-align -Wno-used-but-marked-unused -Wno-shadow -Wno-weak-vtables -Wno-global-constructors -Wno-deprecated -Wno-weak-template-vtables -Wno-exit-time-destructors -Wno-covered-switch-default -Wno-shorten-64-to-32 -Wno-documentation -Wno-old-style-cast -Wno-extra-semi -Wno-unused-parameter -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-padded -Wno-missing-noreturn -Wno-missing-prototypes -Wno-documentation-unknown-command -Wno-sign-conversion -Wno-gnu-zero-variadic-macro-arguments -Wno-disabled-macro-expansion -Wno-vla-extension -Wno-vla -Wno-undefined-func-template -Wno-#warnings -Wno-unused-template -Wno-inconsistent-missing-destructor-override -Wno-zero-as-null-pointer-constant -Wno-unused-lambda-capture") endif() set(CMAKE_CXX_STANDARD 14) include_directories(${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} ${FLATBUFFERS_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/common 3rdparty) include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/common ${CMAKE_SOURCE_DIR}/common/domain) configure_file(hawd.conf hawd.conf) enable_testing() set(SINK_RESOURCE_PLUGINS_PATH ${QT_PLUGIN_INSTALL_DIR}/sink/resources) # common, eventually a lib but right now just the command buffers add_subdirectory(common) # the synchronizer add_subdirectory(synchronizer) # example implementations add_subdirectory(examples) # some tests add_subdirectory(tests) # cli add_subdirectory(sinksh) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 9c4d4f17..51145fd6 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -1,157 +1,159 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR}) include_directories(domain) ecm_setup_version(${sink_VERSION} VARIABLE_PREFIX Sink VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/sink_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/SinkConfigVersion.cmake" SOVERSION 0 ) ########### CMake Config Files ########### set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/Sink") configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/SinkConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/SinkConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/SinkConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/SinkConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) add_definitions("-fvisibility=hidden") install(EXPORT SinkTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE SinkTargets.cmake) set(storage_SRCS storage_lmdb.cpp) set(command_SRCS store.cpp secretstore.cpp notifier.cpp resourcecontrol.cpp modelresult.cpp definitions.cpp log.cpp entitybuffer.cpp facadefactory.cpp commands.cpp facade.cpp pipeline.cpp propertymapper.cpp domainadaptor.cpp resource.cpp genericresource.cpp resourceaccess.cpp queryrunner.cpp listener.cpp storage_common.cpp threadboundary.cpp messagequeue.cpp index.cpp typeindex.cpp resourcefacade.cpp resourceconfig.cpp configstore.cpp resultset.cpp domain/propertyregistry.cpp domain/applicationdomaintype.cpp domain/typeimplementations.cpp test.cpp query.cpp changereplay.cpp adaptorfactoryregistry.cpp synchronizer.cpp synchronizerstore.cpp contactpreprocessor.cpp mailpreprocessor.cpp + eventpreprocessor.cpp specialpurposepreprocessor.cpp datastorequery.cpp storage/entitystore.cpp indexer.cpp mail/threadindexer.cpp mail/fulltextindexer.cpp notification.cpp commandprocessor.cpp inspector.cpp propertyparser.cpp utils.cpp fulltextindex.cpp ${storage_SRCS}) add_library(${PROJECT_NAME} SHARED ${command_SRCS}) generate_flatbuffers( ${PROJECT_NAME} commands/commandcompletion commands/createentity commands/deleteentity commands/handshake commands/modifyentity commands/revisionupdate commands/synchronize commands/notification commands/revisionreplayed commands/inspection commands/flush commands/secret domain/contact domain/addressbook domain/event domain/calendar domain/mail domain/folder domain/dummy entity metadata queuedcommand ) generate_export_header(${PROJECT_NAME} BASE_NAME Sink EXPORT_FILE_NAME sink_export.h) SET_TARGET_PROPERTIES(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX VERSION ${Sink_VERSION} SOVERSION ${Sink_SOVERSION} EXPORT_NAME ${PROJECT_NAME} ) target_link_libraries(${PROJECT_NAME} PUBLIC KAsync Qt5::Network PRIVATE ${LMDB_LIBRARIES} Qt5::Gui KF5::Mime KF5::Contacts + KF5::CalendarCore ${XAPIAN_LIBRARIES} ) install(TARGETS ${PROJECT_NAME} EXPORT SinkTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ${LIBRARY_NAMELINK} ) add_clang_static_analysis(${PROJECT_NAME}) install(FILES store.h notifier.h resourcecontrol.h domain/applicationdomaintype.h query.h standardqueries.h inspection.h notification.h bufferadaptor.h test.h log.h flush.h secretstore.h ${CMAKE_CURRENT_BINARY_DIR}/sink_export.h DESTINATION ${INCLUDE_INSTALL_DIR}/${PROJECT_NAME} COMPONENT Devel ) diff --git a/common/domain/applicationdomaintype.h b/common/domain/applicationdomaintype.h index 43d385c5..f3c954ae 100644 --- a/common/domain/applicationdomaintype.h +++ b/common/domain/applicationdomaintype.h @@ -1,585 +1,585 @@ /* * Copyright (C) 2014 Christian Mollekopf * * 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 . */ #pragma once #include "sink_export.h" #include #include #include #include #include #include "bufferadaptor.h" #define SINK_ENTITY(TYPE, LOWERCASENAME) \ static constexpr const char *name = #LOWERCASENAME; \ typedef QSharedPointer Ptr; \ using Entity::Entity; \ TYPE() = default; \ TYPE(const ApplicationDomainType &o) : Entity(o) {} \ virtual ~TYPE() = default; \ static TYPE create(const QByteArray &resource) { return createEntity(resource); }; \ #define SINK_PROPERTY(TYPE, NAME, LOWERCASENAME) \ struct NAME { \ static constexpr const char *name = #LOWERCASENAME; \ typedef TYPE Type; \ }; \ void set##NAME(const TYPE &value) { setProperty(NAME::name, QVariant::fromValue(value)); } \ TYPE get##NAME() const { return getProperty(NAME::name).value(); } \ #define SINK_EXTRACTED_PROPERTY(TYPE, NAME, LOWERCASENAME) \ struct NAME { \ static constexpr const char *name = #LOWERCASENAME; \ typedef TYPE Type; \ }; \ void setExtracted##NAME(const TYPE &value) { setProperty(NAME::name, QVariant::fromValue(value)); } \ TYPE get##NAME() const { return getProperty(NAME::name).value(); } \ #define SINK_STATUS_PROPERTY(TYPE, NAME, LOWERCASENAME) \ struct NAME { \ static constexpr const char *name = #LOWERCASENAME; \ typedef TYPE Type; \ }; \ void setStatus##NAME(const TYPE &value) { setProperty(NAME::name, QVariant::fromValue(value)); } \ TYPE get##NAME() const { return getProperty(NAME::name).value(); } \ #define SINK_REFERENCE_PROPERTY(TYPE, NAME, LOWERCASENAME) \ struct NAME { \ static constexpr const char *name = #LOWERCASENAME; \ typedef Reference Type; \ typedef ApplicationDomain::TYPE ReferenceType; \ }; \ void set##NAME(const ApplicationDomain::TYPE &value) { setProperty(NAME::name, value); } \ void set##NAME(const QByteArray &value) { setProperty(NAME::name, QVariant::fromValue(Reference{value})); } \ QByteArray get##NAME() const { return getProperty(NAME::name).value().value; } \ #define SINK_INDEX_PROPERTY(TYPE, NAME, LOWERCASENAME) \ struct NAME { \ static constexpr const char *name = #LOWERCASENAME; \ typedef TYPE Type; \ }; \ namespace Sink { namespace ApplicationDomain { enum SINK_EXPORT ErrorCode { NoError = 0, UnknownError, NoServerError, ConnectionError, LoginError, ConfigurationError, TransmissionError, ConnectionLostError, MissingCredentialsError, ResourceCrashedError }; enum SINK_EXPORT SuccessCode { TransmissionSuccess }; enum SINK_EXPORT SyncStatus { NoSyncStatus, SyncInProgress, SyncError, SyncSuccess, NewContentAvailable }; /** * The status of an account or resource. * * It is set as follows: * * By default the status is no status. * * If a connection to the server failed the status is Offline. * * If a connection to the server could be established the status is Connected. * * If an error occurred that keeps the resource from operating (so non transient), the resource enters the error state. * * If a long running operation is started the resource goes to the busy state (and return to the previous state after that). */ enum SINK_EXPORT Status { NoStatus, OfflineStatus, ConnectedStatus, BusyStatus, ErrorStatus }; struct SINK_EXPORT Error { }; struct SINK_EXPORT Progress { }; /** * Internal type. * * Represents a reference to another entity in the same resource. */ struct Reference { Reference() = default; Reference(const Reference &) = default; Reference(const QByteArray &id) : value(id) {}; Reference(const char *id) : value(id) {}; ~Reference() = default; bool operator==(const Reference &other) const { return value == other.value; } operator QByteArray() const { return value; } QByteArray value; }; void copyBuffer(Sink::ApplicationDomain::BufferAdaptor &buffer, Sink::ApplicationDomain::BufferAdaptor &memoryAdaptor, const QList &properties = QList(), bool pruneReferences = false); /** * The domain type interface has two purposes: * * provide a unified interface to read buffers (for zero-copy reading) * * record changes to generate changesets for modifications * * ApplicationDomainTypes don't adhere to any standard and are meant to be extended frequently (hence the non-typesafe interface). */ class SINK_EXPORT ApplicationDomainType { public: typedef QSharedPointer Ptr; ApplicationDomainType(); explicit ApplicationDomainType(const QByteArray &resourceInstanceIdentifier); explicit ApplicationDomainType(const QByteArray &resourceInstanceIdentifier, const QByteArray &identifier, qint64 revision, const QSharedPointer &adaptor); ApplicationDomainType(const ApplicationDomainType &other); ApplicationDomainType& operator=(const ApplicationDomainType &other); inline bool operator==(const ApplicationDomainType &other) { return other.identifier() == identifier(); } inline bool operator!=(const ApplicationDomainType &other) { return !(*this == other); } template DomainType cast() { static_assert(std::is_base_of::value, "You can only cast to base classes of ApplicationDomainType."); DomainType t = *this; t.mChangeSet = mChangeSet; return t; } /** * Returns an in memory representation of the same entity. */ template static typename DomainType::Ptr getInMemoryRepresentation(const ApplicationDomainType &domainType, const QList properties = QList()) { auto memoryAdaptor = QSharedPointer::create(); copyBuffer(*(domainType.mAdaptor), *memoryAdaptor, properties, false); //mIdentifier internally still refers to the memory-mapped memory, we need to copy the memory or it will become invalid return QSharedPointer::create(domainType.mResourceInstanceIdentifier, QByteArray(domainType.mIdentifier.constData(), domainType.mIdentifier.size()), domainType.mRevision, memoryAdaptor); } /** * Returns an in memory copy without id and resource set. */ template static typename DomainType::Ptr getInMemoryCopy(const ApplicationDomainType &domainType, const QList properties = QList()) { auto memoryAdaptor = QSharedPointer::create(); Q_ASSERT(domainType.mAdaptor); copyBuffer(*(domainType.mAdaptor), *memoryAdaptor, properties, true); return QSharedPointer::create(QByteArray{}, QByteArray{}, 0, memoryAdaptor); } static QByteArray generateUid(); template static DomainType createEntity() { DomainType object; object.mIdentifier = generateUid(); return object; } template static DomainType createEntity(const QByteArray &resourceInstanceIdentifier) { DomainType object(resourceInstanceIdentifier); object.mIdentifier = generateUid(); return object; } template static DomainType createEntity(const QByteArray &resourceInstanceIdentifier, const QByteArray &identifier) { if (identifier.isEmpty()) { return createEntity(resourceInstanceIdentifier); } DomainType object(resourceInstanceIdentifier); object.mIdentifier = identifier; return object; } template static DomainType createCopy(const QByteArray &identifier, const DomainType &original) { DomainType object(original); object.mIdentifier = identifier; //This shouldn't be necessary but is to avoid corrupt id's from Store::modify calls. object.mIdentifier.detach(); return object; } virtual ~ApplicationDomainType(); bool hasProperty(const QByteArray &key) const; QVariant getProperty(const QByteArray &key) const; QVariantList getCollectedProperty(const QByteArray &key) const; template QVariantList getCollectedProperty() const { return getCollectedProperty(Property::name); } /** * Set a property and record a changed property * * If the propery is available and did not change the call will be ignored. */ void setProperty(const QByteArray &key, const QVariant &value); /** * Convenience method to set a reference property. */ void setProperty(const QByteArray &key, const ApplicationDomainType &value); void setChangedProperties(const QSet &changeset); QByteArrayList changedProperties() const; QByteArrayList availableProperties() const; qint64 revision() const; QByteArray resourceInstanceIdentifier() const; void setResource(const QByteArray &identifier); QByteArray identifier() const; bool isAggregate() const; QVector aggregatedIds() const; QVector &aggregatedIds(); int count() const; private: friend QDebug operator<<(QDebug, const ApplicationDomainType &); QSharedPointer mAdaptor; QSharedPointer> mChangeSet; /* * Each domain object needs to store the resource, identifier, revision triple so we can link back to the storage location. */ QByteArray mResourceInstanceIdentifier; QByteArray mIdentifier; qint64 mRevision; QVector mAggreatedIds; }; /* * Should this be specific to the synclistresultset, in other cases we may want to take revision and resource into account. */ inline bool operator==(const ApplicationDomainType& lhs, const ApplicationDomainType& rhs) { return lhs.identifier() == rhs.identifier() && lhs.resourceInstanceIdentifier() == rhs.resourceInstanceIdentifier(); } SINK_EXPORT QDebug operator<< (QDebug d, const ApplicationDomainType &type); SINK_EXPORT QDebug operator<< (QDebug d, const Reference &ref); struct SINK_EXPORT SinkAccount : public ApplicationDomainType { static constexpr const char *name = "account"; typedef QSharedPointer Ptr; explicit SinkAccount(const QByteArray &resourceInstanceIdentifier, const QByteArray &identifier, qint64 revision, const QSharedPointer &adaptor); explicit SinkAccount(const QByteArray &identifier); SinkAccount(); virtual ~SinkAccount(); SINK_PROPERTY(QString, Name, name); SINK_PROPERTY(QString, Icon, icon); SINK_PROPERTY(QString, AccountType, type); SINK_STATUS_PROPERTY(int, Status, status); }; /** * Represents an sink resource. * * This type is used for configuration of resources, * and for creating and removing resource instances. */ struct SINK_EXPORT SinkResource : public ApplicationDomainType { static constexpr const char *name = "resource"; typedef QSharedPointer Ptr; explicit SinkResource(const QByteArray &resourceInstanceIdentifier, const QByteArray &identifier, qint64 revision, const QSharedPointer &adaptor); explicit SinkResource(const QByteArray &identifier); SinkResource(); virtual ~SinkResource(); SINK_REFERENCE_PROPERTY(SinkAccount, Account, account); SINK_PROPERTY(QByteArray, ResourceType, type); SINK_PROPERTY(QByteArrayList, Capabilities, capabilities); SINK_STATUS_PROPERTY(int, Status, status); }; struct SINK_EXPORT Entity : public ApplicationDomainType { typedef QSharedPointer Ptr; using ApplicationDomainType::ApplicationDomainType; Entity() = default; Entity(const ApplicationDomainType &other) : ApplicationDomainType(other) {} virtual ~Entity() = default; }; struct SINK_EXPORT Addressbook : public Entity { SINK_ENTITY(Addressbook, addressbook); SINK_REFERENCE_PROPERTY(Addressbook, Parent, parent); SINK_PROPERTY(QString, Name, name); SINK_EXTRACTED_PROPERTY(QDateTime, LastUpdated, lastUpdated); }; struct SINK_EXPORT Contact : public Entity { struct SINK_EXPORT Email { enum Type { Undefined, Work, Home }; Type type; QString email; }; SINK_ENTITY(Contact, contact); SINK_PROPERTY(QString, Uid, uid); SINK_PROPERTY(QString, Fn, fn); SINK_PROPERTY(QString, Firstname, firstname); SINK_PROPERTY(QString, Lastname, lastname); SINK_PROPERTY(QList, Emails, emails); SINK_PROPERTY(QByteArray, Vcard, vcard); SINK_PROPERTY(QByteArray, Photo, photo); SINK_REFERENCE_PROPERTY(Addressbook, Addressbook, addressbook); }; struct SINK_EXPORT Calendar : public Entity { SINK_ENTITY(Calendar, calendar); SINK_PROPERTY(QString, Name, name); }; struct SINK_EXPORT Event : public Entity { SINK_ENTITY(Event, event); - SINK_PROPERTY(QString, Uid, uid); - SINK_PROPERTY(QString, Summary, summary); - SINK_PROPERTY(QString, Description, description); - SINK_PROPERTY(QDateTime, StartTime, startTime); - SINK_PROPERTY(QDateTime, EndTime, endTime); + SINK_EXTRACTED_PROPERTY(QString, Uid, uid); + SINK_EXTRACTED_PROPERTY(QString, Summary, summary); + SINK_EXTRACTED_PROPERTY(QString, Description, description); + SINK_EXTRACTED_PROPERTY(QDateTime, StartTime, startTime); + SINK_EXTRACTED_PROPERTY(QDateTime, EndTime, endTime); SINK_PROPERTY(QByteArray, Ical, ical); SINK_REFERENCE_PROPERTY(Calendar, Calendar, calendar); }; struct SINK_EXPORT Todo : public Entity { SINK_ENTITY(Todo, todo); }; struct SINK_EXPORT Folder : public Entity { SINK_ENTITY(Folder, folder); SINK_REFERENCE_PROPERTY(Folder, Parent, parent); SINK_PROPERTY(QString, Name, name); SINK_PROPERTY(QByteArray, Icon, icon); SINK_PROPERTY(QByteArrayList, SpecialPurpose, specialpurpose); SINK_PROPERTY(bool, Enabled, enabled); SINK_EXTRACTED_PROPERTY(QDateTime, LastUpdated, lastUpdated); SINK_EXTRACTED_PROPERTY(int, Count, count); SINK_EXTRACTED_PROPERTY(bool, FullContentAvailable, fullContentAvailable); }; struct SINK_EXPORT Mail : public Entity { struct SINK_EXPORT Contact { QString name; QString emailAddress; }; SINK_ENTITY(Mail, mail); SINK_EXTRACTED_PROPERTY(Contact, Sender, sender); SINK_EXTRACTED_PROPERTY(QList, To, to); SINK_EXTRACTED_PROPERTY(QList, Cc, cc); SINK_EXTRACTED_PROPERTY(QList, Bcc, bcc); SINK_EXTRACTED_PROPERTY(QString, Subject, subject); SINK_EXTRACTED_PROPERTY(QDateTime, Date, date); SINK_PROPERTY(bool, Unread, unread); SINK_PROPERTY(bool, Important, important); SINK_REFERENCE_PROPERTY(Folder, Folder, folder); SINK_PROPERTY(QByteArray, MimeMessage, mimeMessage); SINK_EXTRACTED_PROPERTY(bool, FullPayloadAvailable, fullPayloadAvailable); SINK_PROPERTY(bool, Draft, draft); SINK_PROPERTY(bool, Trash, trash); SINK_PROPERTY(bool, Sent, sent); SINK_EXTRACTED_PROPERTY(QByteArray, MessageId, messageId); SINK_EXTRACTED_PROPERTY(QByteArray, ParentMessageId, parentMessageId); SINK_INDEX_PROPERTY(QByteArray, ThreadId, threadId); }; SINK_EXPORT QDebug operator<< (QDebug d, const Mail::Contact &c); struct SINK_EXPORT Identity : public ApplicationDomainType { static constexpr const char *name = "identity"; typedef QSharedPointer Ptr; explicit Identity(const QByteArray &resourceInstanceIdentifier, const QByteArray &identifier, qint64 revision, const QSharedPointer &adaptor); explicit Identity(const QByteArray &identifier); Identity(); virtual ~Identity(); SINK_REFERENCE_PROPERTY(SinkAccount, Account, account); SINK_PROPERTY(QString, Name, name); SINK_PROPERTY(QString, Address, address); }; struct SINK_EXPORT DummyResource { static SinkResource create(const QByteArray &account); }; struct SINK_EXPORT MaildirResource { static SinkResource create(const QByteArray &account); }; struct SINK_EXPORT MailtransportResource { static SinkResource create(const QByteArray &account); }; struct SINK_EXPORT ImapResource { static SinkResource create(const QByteArray &account); }; struct SINK_EXPORT CardDavResource { static SinkResource create(const QByteArray &account); }; struct SINK_EXPORT CalDavResource { static SinkResource create(const QByteArray &account); }; namespace ResourceCapabilities { namespace Mail { static constexpr const char *mail = "mail"; static constexpr const char *folder = "folder"; static constexpr const char *storage = "mail.storage"; static constexpr const char *drafts = "mail.drafts"; static constexpr const char *sent = "mail.sent"; static constexpr const char *trash = "mail.trash"; static constexpr const char *transport = "mail.transport"; static constexpr const char *folderhierarchy = "mail.folderhierarchy"; }; namespace Contact { static constexpr const char *contact = "contact"; static constexpr const char *addressbook = "addressbook"; static constexpr const char *storage = "contact.storage"; }; namespace Event { static constexpr const char *event = "event"; static constexpr const char *calendar = "calendar"; static constexpr const char *storage = "event.storage"; }; }; namespace SpecialPurpose { namespace Mail { static constexpr const char *inbox = "inbox"; static constexpr const char *drafts = "drafts"; static constexpr const char *trash = "trash"; static constexpr const char *sent = "sent"; }; }; /** * All types need to be registered here an MUST return a different name. * * Do not store these types to disk, they may change over time. */ template QByteArray SINK_EXPORT getTypeName() { return DomainType::name; } QByteArrayList SINK_EXPORT getTypeNames(); bool SINK_EXPORT isGlobalType(const QByteArray &type); /** * Type implementation. * * Needs to be implemented for every application domain type. * Contains all non-resource specific, but type-specific code. */ template class SINK_EXPORT TypeImplementation; } } #undef SINK_ENTITY #undef SINK_PROPERTY #undef SINK_EXTRACTED_PROPERTY #undef SINK_REFERENCE_PROPERTY #undef SINK_INDEX_PROPERTY /** * This macro can be used to instantiate templates for all domain types. */ #define SINK_REGISTER_TYPES() \ REGISTER_TYPE(Sink::ApplicationDomain::Contact) \ REGISTER_TYPE(Sink::ApplicationDomain::Addressbook) \ REGISTER_TYPE(Sink::ApplicationDomain::Event) \ REGISTER_TYPE(Sink::ApplicationDomain::Calendar) \ REGISTER_TYPE(Sink::ApplicationDomain::Mail) \ REGISTER_TYPE(Sink::ApplicationDomain::Folder) \ REGISTER_TYPE(Sink::ApplicationDomain::SinkResource) \ REGISTER_TYPE(Sink::ApplicationDomain::SinkAccount) \ REGISTER_TYPE(Sink::ApplicationDomain::Identity) \ SINK_EXPORT QDataStream &operator<<(QDataStream &out, const Sink::ApplicationDomain::Reference &reference); SINK_EXPORT QDataStream &operator>>(QDataStream &in, Sink::ApplicationDomain::Reference &reference); #define REGISTER_TYPE(TYPE) \ Q_DECLARE_METATYPE(TYPE) \ Q_DECLARE_METATYPE(TYPE::Ptr) SINK_REGISTER_TYPES() #undef REGISTER_TYPE Q_DECLARE_METATYPE(Sink::ApplicationDomain::ApplicationDomainType) Q_DECLARE_METATYPE(Sink::ApplicationDomain::ApplicationDomainType::Ptr) Q_DECLARE_METATYPE(Sink::ApplicationDomain::Entity) Q_DECLARE_METATYPE(Sink::ApplicationDomain::Entity::Ptr) Q_DECLARE_METATYPE(Sink::ApplicationDomain::Mail::Contact) Q_DECLARE_METATYPE(Sink::ApplicationDomain::Contact::Email) Q_DECLARE_METATYPE(Sink::ApplicationDomain::Error) Q_DECLARE_METATYPE(Sink::ApplicationDomain::Progress) Q_DECLARE_METATYPE(Sink::ApplicationDomain::Reference) diff --git a/common/eventpreprocessor.cpp b/common/eventpreprocessor.cpp new file mode 100644 index 00000000..e0875632 --- /dev/null +++ b/common/eventpreprocessor.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 Christian Mollekopf + * Copyright (C) 2018 Rémi Nicole + * + * 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 "eventpreprocessor.h" + +#include + +void EventPropertyExtractor::updatedIndexedProperties(Event &event, const QByteArray &rawIcal) +{ + auto incidence = KCalCore::ICalFormat().readIncidence(rawIcal); + + if(!incidence) { + SinkWarning() << "Invalid ICal to process, ignoring..."; + return; + } + + if(incidence->type() != KCalCore::IncidenceBase::IncidenceType::TypeEvent) { + SinkWarning() << "ICal to process is not of type `Event`, ignoring..."; + return; + } + + auto icalEvent = dynamic_cast(incidence.data()); + // Should be guaranteed by the incidence->type() condition above. + Q_ASSERT(icalEvent); + + SinkTrace() << "Extracting properties for event:" << icalEvent->summary(); + + event.setExtractedUid(icalEvent->uid()); + event.setExtractedSummary(icalEvent->summary()); + event.setExtractedDescription(icalEvent->description()); + event.setExtractedStartTime(icalEvent->dtStart()); + event.setExtractedEndTime(icalEvent->dtEnd()); +} + +void EventPropertyExtractor::newEntity(Event &event) +{ + updatedIndexedProperties(event, event.getIcal()); +} + +void EventPropertyExtractor::modifiedEntity(const Event &oldEvent, Event &newEvent) +{ + updatedIndexedProperties(newEvent, newEvent.getIcal()); +} diff --git a/common/eventpreprocessor.h b/common/eventpreprocessor.h new file mode 100644 index 00000000..3fde8b27 --- /dev/null +++ b/common/eventpreprocessor.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 Christian Mollekopf + * Copyright (C) 2018 Rémi Nicole + * + * 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 "pipeline.h" +#include "sink_export.h" + +class SINK_EXPORT EventPropertyExtractor : public Sink::EntityPreprocessor +{ + using Event = Sink::ApplicationDomain::Event; + +public: + virtual ~EventPropertyExtractor() {} + virtual void newEntity(Event &event) Q_DECL_OVERRIDE; + virtual void modifiedEntity(const Event &oldEvent, Event &newEvent) Q_DECL_OVERRIDE; + +private: + static void updatedIndexedProperties(Event &event, const QByteArray &rawIcal); +}; diff --git a/examples/caldavresource/caldavresource.cpp b/examples/caldavresource/caldavresource.cpp index 57f030b2..6bf1a27b 100644 --- a/examples/caldavresource/caldavresource.cpp +++ b/examples/caldavresource/caldavresource.cpp @@ -1,158 +1,154 @@ /* * Copyright (C) 2018 Christian Mollekopf * * 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 "caldavresource.h" #include "../webdavcommon/webdav.h" #include "adaptorfactoryregistry.h" #include "applicationdomaintype.h" #include "domainadaptor.h" +#include "eventpreprocessor.h" #include "facade.h" #include "facadefactory.h" #include #define ENTITY_TYPE_EVENT "event" #define ENTITY_TYPE_CALENDAR "calendar" using Sink::ApplicationDomain::getTypeName; class EventSynchronizer : public WebDavSynchronizer { using Event = Sink::ApplicationDomain::Event; using Calendar = Sink::ApplicationDomain::Calendar; public: explicit EventSynchronizer(const Sink::ResourceContext &context) : WebDavSynchronizer(context, KDAV2::CalDav, getTypeName(), getTypeName()) {} protected: void updateLocalCollections(KDAV2::DavCollection::List calendarList) Q_DECL_OVERRIDE { SinkLog() << "Found" << calendarList.size() << "calendar(s)"; QVector ridList; for (const auto &remoteCalendar : calendarList) { const auto &rid = resourceID(remoteCalendar); SinkLog() << "Found calendar:" << remoteCalendar.displayName() << "[" << rid << "]"; Calendar localCalendar; localCalendar.setName(remoteCalendar.displayName()); createOrModify(ENTITY_TYPE_CALENDAR, rid, localCalendar, /* mergeCriteria = */ QHash{}); } } void updateLocalItem(KDAV2::DavItem remoteItem, const QByteArray &calendarLocalId) Q_DECL_OVERRIDE { const auto &rid = resourceID(remoteItem); auto ical = remoteItem.data(); auto incidence = KCalCore::ICalFormat().fromString(ical); using Type = KCalCore::IncidenceBase::IncidenceType; switch (incidence->type()) { case Type::TypeEvent: { auto remoteEvent = dynamic_cast(*incidence); Event localEvent; - localEvent.setUid(remoteEvent.uid()); - localEvent.setSummary(remoteEvent.summary()); - localEvent.setDescription(remoteEvent.description()); - localEvent.setStartTime(remoteEvent.dtStart()); - localEvent.setEndTime(remoteEvent.dtEnd()); localEvent.setIcal(ical); localEvent.setCalendar(calendarLocalId); SinkTrace() << "Found an event:" << localEvent.getSummary() << "with id:" << rid; createOrModify(ENTITY_TYPE_EVENT, rid, localEvent, /* mergeCriteria = */ QHash{}); break; } case Type::TypeTodo: SinkWarning() << "Unimplemented add of a 'Todo' item in the Store"; break; case Type::TypeJournal: SinkWarning() << "Unimplemented add of a 'Journal' item in the Store"; break; case Type::TypeFreeBusy: SinkWarning() << "Unimplemented add of a 'FreeBusy' item in the Store"; break; case Type::TypeUnknown: SinkWarning() << "Trying to add a 'Unknown' item"; break; default: break; } } QByteArray collectionLocalResourceID(const KDAV2::DavCollection &calendar) Q_DECL_OVERRIDE { return syncStore().resolveRemoteId(ENTITY_TYPE_CALENDAR, resourceID(calendar)); } }; CalDavResource::CalDavResource(const Sink::ResourceContext &context) : Sink::GenericResource(context) { auto synchronizer = QSharedPointer::create(context); setupSynchronizer(synchronizer); - // setupPreprocessors(ENTITY_TYPE_EVENT, QVector() << new EventPropertyExtractor); + setupPreprocessors(ENTITY_TYPE_EVENT, QVector() << new EventPropertyExtractor); } CalDavResourceFactory::CalDavResourceFactory(QObject *parent) : Sink::ResourceFactory(parent, { Sink::ApplicationDomain::ResourceCapabilities::Event::event, Sink::ApplicationDomain::ResourceCapabilities::Event::calendar, Sink::ApplicationDomain::ResourceCapabilities::Event::storage, }) {} Sink::Resource *CalDavResourceFactory::createResource(const Sink::ResourceContext &context) { return new CalDavResource(context); } using Sink::ApplicationDomain::Calendar; using Sink::ApplicationDomain::Event; void CalDavResourceFactory::registerFacades(const QByteArray &resourceName, Sink::FacadeFactory &factory) { factory.registerFacade>(resourceName); factory.registerFacade>(resourceName); } void CalDavResourceFactory::registerAdaptorFactories( const QByteArray &resourceName, Sink::AdaptorFactoryRegistry ®istry) { registry.registerFactory>(resourceName); registry.registerFactory>(resourceName); } void CalDavResourceFactory::removeDataFromDisk(const QByteArray &instanceIdentifier) { CalDavResource::removeFromDisk(instanceIdentifier); } diff --git a/examples/dummyresource/resourcefactory.cpp b/examples/dummyresource/resourcefactory.cpp index cfce6e43..597bd957 100644 --- a/examples/dummyresource/resourcefactory.cpp +++ b/examples/dummyresource/resourcefactory.cpp @@ -1,209 +1,212 @@ /* * Copyright (C) 2014 Aaron Seigo * Copyright (C) 2015 Christian Mollekopf * * 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 "resourcefactory.h" #include "facade.h" #include "entitybuffer.h" #include "pipeline.h" #include "dummycalendar_generated.h" #include "mail_generated.h" #include "domainadaptor.h" #include "log.h" #include "dummystore.h" #include "definitions.h" #include "facadefactory.h" #include "adaptorfactoryregistry.h" #include "synchronizer.h" #include "inspector.h" #include "mailpreprocessor.h" #include "specialpurposepreprocessor.h" #include //This is the resources entity type, and not the domain type #define ENTITY_TYPE_EVENT "event" #define ENTITY_TYPE_MAIL "mail" #define ENTITY_TYPE_FOLDER "folder" using namespace Sink; class DummySynchronizer : public Sink::Synchronizer { public: DummySynchronizer(const Sink::ResourceContext &context) : Sink::Synchronizer(context) { setSecret("dummy"); } Sink::ApplicationDomain::Event::Ptr createEvent(const QByteArray &ridBuffer, const QMap &data) { auto event = Sink::ApplicationDomain::Event::Ptr::create(); - event->setSummary(data.value("summary").toString()); + event->setExtractedUid(data.value("uid").toString()); + event->setExtractedSummary(data.value("summary").toString()); + event->setExtractedDescription(data.value("description").toString()); + event->setExtractedStartTime(data.value("starttime").toDateTime()); + event->setExtractedEndTime(data.value("endtime").toDateTime()); event->setProperty("remoteId", ridBuffer); - event->setDescription(data.value("description").toString()); return event; } Sink::ApplicationDomain::Mail::Ptr createMail(const QByteArray &ridBuffer, const QMap &data) { auto mail = Sink::ApplicationDomain::Mail::Ptr::create(); mail->setExtractedMessageId(ridBuffer); mail->setExtractedSubject(data.value("subject").toString()); mail->setExtractedSender(Sink::ApplicationDomain::Mail::Contact{data.value("senderName").toString(), data.value("senderEmail").toString()}); mail->setExtractedDate(data.value("date").toDateTime()); mail->setFolder(syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, data.value("parentFolder").toByteArray())); mail->setUnread(data.value("unread").toBool()); mail->setImportant(data.value("important").toBool()); return mail; } Sink::ApplicationDomain::Folder::Ptr createFolder(const QByteArray &ridBuffer, const QMap &data) { auto folder = Sink::ApplicationDomain::Folder::Ptr::create(); folder->setName(data.value("name").toString()); folder->setIcon(data.value("icon").toByteArray()); if (!data.value("parent").toString().isEmpty()) { auto sinkId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, data.value("parent").toByteArray()); folder->setParent(sinkId); } return folder; } void synchronize(const QByteArray &bufferType, const QMap > &data, std::function &data)> createEntity) { auto time = QSharedPointer::create(); time->start(); //TODO find items to remove int count = 0; for (auto it = data.constBegin(); it != data.constEnd(); it++) { count++; const auto remoteId = it.key().toUtf8(); auto entity = createEntity(remoteId, it.value()); createOrModify(bufferType, remoteId, *entity); } SinkTrace() << "Sync of " << count << " entities of type " << bufferType << " done." << Sink::Log::TraceTime(time->elapsed()); } KAsync::Job synchronizeWithSource(const Sink::QueryBase &) Q_DECL_OVERRIDE { SinkLog() << " Synchronizing with the source"; SinkTrace() << "Synchronize with source and sending a notification about it"; Sink::Notification n; n.id = "connected"; n.type = Sink::Notification::Status; n.message = "We're connected"; n.code = Sink::ApplicationDomain::ConnectedStatus; emit notify(n); return KAsync::start([this]() { synchronize(ENTITY_TYPE_EVENT, DummyStore::instance().events(), [this](const QByteArray &ridBuffer, const QMap &data) { return createEvent(ridBuffer, data); }); synchronize(ENTITY_TYPE_MAIL, DummyStore::instance().mails(), [this](const QByteArray &ridBuffer, const QMap &data) { return createMail(ridBuffer, data); }); synchronize(ENTITY_TYPE_FOLDER, DummyStore::instance().folders(), [this](const QByteArray &ridBuffer, const QMap &data) { return createFolder(ridBuffer, data); }); }); } bool canReplay(const QByteArray &type, const QByteArray &key, const QByteArray &value) Q_DECL_OVERRIDE { return false; } }; class DummyInspector : public Sink::Inspector { public: DummyInspector(const Sink::ResourceContext &resourceContext) : Sink::Inspector(resourceContext) { } protected: KAsync::Job inspect(int inspectionType, const QByteArray &inspectionId, const QByteArray &domainType, const QByteArray &entityId, const QByteArray &property, const QVariant &expectedValue) Q_DECL_OVERRIDE { SinkTrace() << "Inspecting " << inspectionType << domainType << entityId << property << expectedValue; if (property == "testInspection") { if (expectedValue.toBool()) { //Success return KAsync::null(); } else { //Failure return KAsync::error(1, "Failed."); } } return KAsync::null(); } }; DummyResource::DummyResource(const Sink::ResourceContext &resourceContext, const QSharedPointer &pipeline) : Sink::GenericResource(resourceContext, pipeline) { setupSynchronizer(QSharedPointer::create(resourceContext)); setupInspector(QSharedPointer::create(resourceContext)); setupPreprocessors(ENTITY_TYPE_MAIL, QVector() << new MailPropertyExtractor << new SpecialPurposeProcessor); setupPreprocessors(ENTITY_TYPE_FOLDER, QVector()); setupPreprocessors(ENTITY_TYPE_EVENT, QVector()); } DummyResource::~DummyResource() { } DummyResourceFactory::DummyResourceFactory(QObject *parent) : Sink::ResourceFactory(parent, {Sink::ApplicationDomain::ResourceCapabilities::Mail::mail, "event", Sink::ApplicationDomain::ResourceCapabilities::Mail::folder, Sink::ApplicationDomain::ResourceCapabilities::Mail::storage, "-folder.rename", Sink::ApplicationDomain::ResourceCapabilities::Mail::sent} ) { } Sink::Resource *DummyResourceFactory::createResource(const Sink::ResourceContext &resourceContext) { return new DummyResource(resourceContext); } void DummyResourceFactory::registerFacades(const QByteArray &resourceName, Sink::FacadeFactory &factory) { factory.registerFacade>(resourceName); factory.registerFacade>(resourceName); factory.registerFacade>(resourceName); } void DummyResourceFactory::registerAdaptorFactories(const QByteArray &resourceName, Sink::AdaptorFactoryRegistry ®istry) { registry.registerFactory(resourceName); registry.registerFactory(resourceName); registry.registerFactory(resourceName); } void DummyResourceFactory::removeDataFromDisk(const QByteArray &instanceIdentifier) { DummyResource::removeFromDisk(instanceIdentifier); } diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index e2f3543e..aa03fc31 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -1,457 +1,457 @@ #include #include #include #include "store.h" #include "facade.h" #include "resourceconfig.h" #include "modelresult.h" #include "resultprovider.h" #include "facadefactory.h" #include "test.h" #include "asyncutils.h" template struct Result { bool fetchedAll; }; template class TestDummyResourceFacade : public Sink::StoreFacade { public: static std::shared_ptr> registerFacade(const QByteArray &instanceIdentifier = QByteArray()) { static QMap>> map; auto facade = std::make_shared>(); map.insert(instanceIdentifier, facade); bool alwaysReturnFacade = instanceIdentifier.isEmpty(); Sink::FacadeFactory::instance().registerFacade>("dummyresource", [alwaysReturnFacade](const Sink::ResourceContext &context) { if (alwaysReturnFacade) { Q_ASSERT(map.contains(QByteArray())); return map.value(QByteArray()); } Q_ASSERT(map.contains(context.instanceId())); return map.value(context.instanceId()); }); return facade; } ~TestDummyResourceFacade(){}; KAsync::Job create(const T &domainObject) Q_DECL_OVERRIDE { SinkLogCtx(Sink::Log::Context{"test"}) << "Create: " << domainObject; creations << domainObject; return KAsync::null(); }; KAsync::Job modify(const T &domainObject) Q_DECL_OVERRIDE { SinkLogCtx(Sink::Log::Context{"test"}) << "Modify: " << domainObject; modifications << domainObject; return KAsync::null(); }; KAsync::Job move(const T &domainObject, const QByteArray &) Q_DECL_OVERRIDE { return KAsync::null(); }; KAsync::Job copy(const T &domainObject, const QByteArray &) Q_DECL_OVERRIDE { return KAsync::null(); }; KAsync::Job remove(const T &domainObject) Q_DECL_OVERRIDE { SinkLogCtx(Sink::Log::Context{"test"}) << "Remove: " << domainObject; removals << domainObject; return KAsync::null(); }; QPair, typename Sink::ResultEmitter::Ptr> load(const Sink::Query &query, const Sink::Log::Context &ctx) Q_DECL_OVERRIDE { auto resultProvider = QSharedPointer>::create(); resultProvider->onDone([resultProvider,ctx]() { SinkTraceCtx(ctx) << "Result provider is done"; }); // We have to do it this way, otherwise we're not setting the fetcher right auto emitter = resultProvider->emitter(); resultProvider->setFetcher([query, resultProvider, this, ctx]() { async::run>([=] { SinkTraceCtx(ctx) << "Running the fetcher."; SinkTraceCtx(ctx) << "-------------------------."; int count = 0; for (int i = offset; i < results.size(); i++) { const auto res = results.at(i); count++; resultProvider->add(res); if (query.limit()) { if (count >= query.limit()) { SinkTraceCtx(ctx) << "Aborting early after " << count << "results."; offset = i + 1; bool fetchedAll = (i + 1 >= results.size()); return Result{fetchedAll}; } } } return Result{true}; }, runAsync) .then([=] (const Result &r) { resultProvider->initialResultSetComplete(r.fetchedAll); }) .exec(); }); auto job = KAsync::start([query, resultProvider]() {}); mResultProvider = resultProvider.data(); return qMakePair(job, emitter); } QList results; Sink::ResultProviderInterface *mResultProvider; bool runAsync = false; int offset = 0; QList creations; QList modifications; QList removals; }; /** * Test of the client api implementation. * * This test works with injected dummy facades and thus doesn't write to storage. */ class ClientAPITest : public QObject { Q_OBJECT template std::shared_ptr > setupFacade(const QByteArray &identifier) { auto facade = TestDummyResourceFacade::registerFacade(identifier); ResourceConfig::addResource(identifier, "dummyresource"); QMap config = ResourceConfig::getConfiguration(identifier); config.insert(Sink::ApplicationDomain::SinkResource::Capabilities::name, QVariant::fromValue(QByteArrayList() << Sink::ApplicationDomain::getTypeName())); ResourceConfig::configureResource(identifier, config); return facade; } private slots: void initTestCase() { Sink::Test::initTest(); Sink::FacadeFactory::instance().resetFactory(); ResourceConfig::clear(); } void testLoad() { auto facade = setupFacade("dummyresource.instance1"); facade->results << QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); Sink::Query query; query.resourceFilter("dummyresource.instance1"); auto model = Sink::Store::loadModel(query); QTRY_VERIFY(model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()); QCOMPARE(model->rowCount(QModelIndex()), 1); } void testLoadWithoutResource() { Sink::Query query; query.resourceFilter("nonexisting.resource"); auto model = Sink::Store::loadModel(query); QTRY_VERIFY(model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()); } void testModelSingle() { auto facade = setupFacade("dummyresource.instance1"); facade->results << QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); Sink::Query query; query.resourceFilter("dummyresource.instance1"); auto model = Sink::Store::loadModel(query); QTRY_COMPARE(model->rowCount(), 1); } void testModelSignals() { auto facade = setupFacade("dummyresource.instance1"); facade->runAsync = true; auto folder = QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); auto subfolder = QSharedPointer::create("resource", "subId", 0, QSharedPointer::create()); subfolder->setParent("id"); facade->results << folder << subfolder; // Test Sink::Query query; query.resourceFilter("dummyresource.instance1"); query.requestTree("parent"); auto model = Sink::Store::loadModel(query); QSignalSpy spy(model.data(), SIGNAL(rowsInserted(const QModelIndex &, int, int))); QVERIFY(spy.isValid()); QTRY_VERIFY(spy.count() == 2); } void testModelNested() { auto facade = setupFacade("dummyresource.instance1"); facade->runAsync = true; auto folder = QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); auto subfolder = QSharedPointer::create("resource", "subId", 0, QSharedPointer::create()); subfolder->setParent("id"); facade->results << folder << subfolder; // Test Sink::Query query; query.resourceFilter("dummyresource.instance1"); query.requestTree("parent"); auto model = Sink::Store::loadModel(query); QObject::connect(model.data(), &QAbstractItemModel::rowsInserted, [&] (const QModelIndex &parent, int first, int last) { for (int row = first; row <= last; row++) { auto index = model->index(row, 0, parent); QVERIFY(index.isValid()); QVERIFY(index.data(Sink::Store::DomainObjectRole).value()); } }); QTRY_COMPARE(model->rowCount(), 1); QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); } void testModelNestedReverse() { auto facade = setupFacade("dummyresource.instance1"); facade->runAsync = true; auto folder = QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); auto subfolder = QSharedPointer::create("resource", "subId", 0, QSharedPointer::create()); subfolder->setParent(folder->identifier()); auto subsubfolder = QSharedPointer::create("resource", "subsubId", 0, QSharedPointer::create()); subsubfolder->setParent(subfolder->identifier()); facade->results << subsubfolder << subfolder << folder; // Test Sink::Query query; query.resourceFilter("dummyresource.instance1"); query.requestTree("parent"); auto model = Sink::Store::loadModel(query); QObject::connect(model.data(), &QAbstractItemModel::rowsInserted, [&] (const QModelIndex &parent, int first, int last) { for (int row = first; row <= last; row++) { auto index = model->index(row, 0, parent); QVERIFY(index.isValid()); QVERIFY(index.data(Sink::Store::DomainObjectRole).value()); } }); QTRY_COMPARE(model->rowCount(), 1); QVERIFY(model->index(0, 0).data(Sink::Store::DomainObjectRole).value()); QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); } void testModelNestedLive() { auto facade = setupFacade("dummyresource.instance1"); auto folder = QSharedPointer::create("dummyresource.instance1", "id", 0, QSharedPointer::create()); auto subfolder = QSharedPointer::create("dummyresource.instance1", "subId", 0, QSharedPointer::create()); subfolder->setParent("id"); facade->results << folder << subfolder; // Test Sink::Query query; query.resourceFilter("dummyresource.instance1"); query.setFlags(Sink::Query::LiveQuery); query.requestTree("parent"); auto model = Sink::Store::loadModel(query); QTRY_COMPARE(model->rowCount(), 1); model->fetchMore(model->index(0, 0)); QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); auto resultProvider = facade->mResultProvider; // Test new toplevel folder { QSignalSpy rowsInsertedSpy(model.data(), SIGNAL(rowsInserted(const QModelIndex &, int, int))); auto folder2 = QSharedPointer::create("resource", "id2", 0, QSharedPointer::create()); resultProvider->add(folder2); QTRY_COMPARE(model->rowCount(), 2); QTRY_COMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0).value(), QModelIndex()); } // Test changed name { QSignalSpy dataChanged(model.data(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &))); folder->setProperty("subject", "modifiedSubject"); resultProvider->modify(folder); QTRY_COMPARE(model->rowCount(), 2); QTRY_COMPARE(dataChanged.count(), 1); } // Test removal { QSignalSpy rowsRemovedSpy(model.data(), SIGNAL(rowsRemoved(const QModelIndex &, int, int))); folder->setProperty("subject", "modifiedSubject"); resultProvider->remove(subfolder); QTRY_COMPARE(model->rowCount(model->index(0, 0)), 0); QTRY_COMPARE(rowsRemovedSpy.count(), 1); } // TODO: A modification can also be a move } void testLoadMultiResource() { auto facade1 = setupFacade("dummyresource.instance1"); facade1->results << QSharedPointer::create("resource1", "id", 0, QSharedPointer::create()); auto facade2 = setupFacade("dummyresource.instance2"); facade2->results << QSharedPointer::create("resource2", "id", 0, QSharedPointer::create()); Sink::Query query; int childrenFetchedCount = 0; auto model = Sink::Store::loadModel(query); QObject::connect(model.data(), &QAbstractItemModel::dataChanged, [&childrenFetchedCount](const QModelIndex &, const QModelIndex &, const QVector &roles) { if (roles.contains(Sink::Store::ChildrenFetchedRole)) { childrenFetchedCount++; } }); QTRY_VERIFY(model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()); QCOMPARE(model->rowCount(QModelIndex()), 2); // Ensure children fetched is only emitted once (when all resources are done) QTest::qWait(50); QVERIFY(childrenFetchedCount <= 1); } void testImperativeLoad() { auto facade = setupFacade("dummyresource.instance1"); facade->results << QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); Sink::Query query; query.resourceFilter("dummyresource.instance1"); bool gotValue = false; auto result = Sink::Store::fetchOne(query) .then([&gotValue](const Sink::ApplicationDomain::Event &event) { gotValue = true; }) .exec(); result.waitForFinished(); QVERIFY(!result.errorCode()); QVERIFY(gotValue); } void testMultiresourceIncrementalLoad() { auto facade1 = setupFacade("dummyresource.instance1"); for (int i = 0; i < 4; i++) { facade1->results << QSharedPointer::create("resource1", "id" + QByteArray::number(i), 0, QSharedPointer::create()); } auto facade2 = setupFacade("dummyresource.instance2"); for (int i = 0; i < 6; i++) { facade2->results << QSharedPointer::create("resource2", "id" + QByteArray::number(i), 0, QSharedPointer::create()); } Sink::Query query; query.limit(2); auto model = Sink::Store::loadModel(query); QTRY_VERIFY(model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()); QCOMPARE(model->rowCount(QModelIndex()), 4); //Try to fetch another round QVERIFY(model->canFetchMore(QModelIndex())); model->fetchMore(QModelIndex()); QTRY_VERIFY(model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()); QCOMPARE(model->rowCount(QModelIndex()), 8); //Try to fetch the last round QVERIFY(model->canFetchMore(QModelIndex())); model->fetchMore(QModelIndex()); QTRY_VERIFY(model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()); QCOMPARE(model->rowCount(QModelIndex()), 10); QVERIFY(!model->canFetchMore(QModelIndex())); } void testCreateModifyDelete() { auto facade = setupFacade("dummyresource.instance1"); auto event = Sink::ApplicationDomain::Event::createEntity("dummyresource.instance1"); Sink::Store::create(event).exec().waitForFinished(); QCOMPARE(facade->creations.size(), 1); //Modify something so the mdofication won't be dropped - event.setSummary("foobar"); + event.setExtractedSummary("foobar"); Sink::Store::modify(event).exec().waitForFinished(); QCOMPARE(facade->modifications.size(), 1); Sink::Store::remove(event).exec().waitForFinished(); QCOMPARE(facade->removals.size(), 1); } void testMultiModify() { auto facade = setupFacade("dummyresource.instance1"); facade->results << QSharedPointer::create("dummyresource.instance1", "id1", 0, QSharedPointer::create()); facade->results << QSharedPointer::create("dummyresource.instance1", "id2", 0, QSharedPointer::create()); Sink::Query query; query.resourceFilter("dummyresource.instance1"); auto event = Sink::ApplicationDomain::Event::createEntity("dummyresource.instance1"); - event.setUid("modifiedUid"); + event.setExtractedUid("modifiedUid"); Sink::Store::modify(query, event).exec().waitForFinished(); QCOMPARE(facade->modifications.size(), 2); for (const auto &m : facade->modifications) { QCOMPARE(m.getUid(), QString("modifiedUid")); } } void testAggregateModify() { auto facade = setupFacade("dummyresource.instance1"); facade->results << QSharedPointer::create("dummyresource.instance1", "id1", 0, QSharedPointer::create()); facade->results << QSharedPointer::create("dummyresource.instance1", "id2", 0, QSharedPointer::create()); Sink::ApplicationDomain::Event modification("dummyresource.instance1", "id1", 0, QSharedPointer::create()); modification.aggregatedIds() << "id1" << "id2"; - modification.setUid("modifiedUid2"); + modification.setExtractedUid("modifiedUid2"); Sink::Store::modify(modification).exec().waitForFinished(); QCOMPARE(facade->modifications.size(), 2); for (const auto &m : facade->modifications) { QCOMPARE(m.getUid(), {"modifiedUid2"}); } Sink::Store::remove(modification).exec().waitForFinished(); QCOMPARE(facade->removals.size(), 2); } void testModelStress() { auto facade = setupFacade("dummyresource.instance1"); facade->runAsync = true; for (int i = 0; i < 100; i++) { facade->results << QSharedPointer::create("resource", "id" + QByteArray::number(i), 0, QSharedPointer::create()); } Sink::Query query; query.resourceFilter("dummyresource.instance1"); for (int i = 0; i < 100; i++) { auto model = Sink::Store::loadModel(query); model->fetchMore(QModelIndex()); QTest::qWait(1); } QTest::qWait(100); } }; QTEST_MAIN(ClientAPITest) #include "clientapitest.moc"