rewrite of Akonadi backend for SyncEvolution

This patch adds the necessary boiler-plate to compile
Sascha's Akonadi backend as part of SyncEvolution.

It changes the change tracking so that it is now based on the revision
number maintained by Akonadi. This removes the need to keep the sync
process running all the time to record changes.

Finding local databases (= collections) is implemented inside the
backend, with several TODOs in the code to make this nicer.

Tests were added as part of copying the boiler-plate code from the
Evolution backend. However, this depends on being able to open local
databases following the name pattern <prefix>_<type>_[12], with
<prefix> from CLIENT_TEST_EVOLUTION_PREFIX and <type> one of
ical20/vcard30/itodo20/text. This does not work at the moment.

Because the implementation of isEmpty() always returns "false", the
logic which checks whether a slow sync is acceptable will err on the
side of caution and reject slow sync, even if the local side has no
data.
This commit is contained in:
Patrick Ohly 2010-01-10 18:54:33 +01:00
parent 6c48f78743
commit e84e385bb8
6 changed files with 714 additions and 344 deletions

View File

@ -0,0 +1,376 @@
/*
* Copyright (C) 2009 Intel Corporation
*
* 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.
*
* This library is distributed in the hope that 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 "akonadisyncsource.h"
#include "test.h"
#include <boost/algorithm/string.hpp>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
static SyncSource *createSource(const SyncSourceParams &params)
{
SourceType sourceType = SyncSource::getSourceType(params.m_nodes);
bool isMe;
isMe = sourceType.m_backend == "KDE Address Book";
if (isMe || sourceType.m_backend == "addressbook") {
if (sourceType.m_format == "" || sourceType.m_format == "text/vcard"
|| sourceType.m_format == "text/x-vcard") {
return
#ifdef ENABLE_AKONADI
new AkonadiContactSource(params)
#else
isMe ? RegisterSyncSource::InactiveSource : NULL
#endif
;
} else {
return NULL;
}
}
isMe = sourceType.m_backend == "KDE Task List";
if (isMe || sourceType.m_backend == "todo") {
if (sourceType.m_format == "" || sourceType.m_format == "text/calendar"
|| sourceType.m_format == "text/x-vcalendar") {
return
#ifdef ENABLE_AKONADI
new AkonadiTaskSource(params)
#else
isMe ? RegisterSyncSource::InactiveSource : NULL
#endif
;
} else {
return NULL;
}
}
isMe = sourceType.m_backend == "KDE Memos";
if (isMe || sourceType.m_backend == "memo") {
if (sourceType.m_format == "" || sourceType.m_format == "text/plain") {
return
#ifdef ENABLE_AKONADI
new AkonadiMemoSource(params)
#else
isMe ? RegisterSyncSource::InactiveSource : NULL
#endif
;
} else {
return NULL;
}
}
isMe = sourceType.m_backend == "KDE Calendar";
if (isMe || sourceType.m_backend == "calendar") {
if (sourceType.m_format == "" || sourceType.m_format == "text/calendar" ||
sourceType.m_format == "text/x-vcalendar" /* this is for backwards compatibility with broken configs */ ) {
return
#ifdef ENABLE_AKONADI
new AkonadiCalendarSource(params)
#else
isMe ? RegisterSyncSource::InactiveSource : NULL
#endif
;
} else {
return NULL;
}
}
return NULL;
}
static RegisterSyncSource registerMe("KDE Contact/Calendar/Task List/Memos",
#ifdef ENABLE_AKONADI
true,
#else
false,
#endif
createSource,
"KDE Address Book = KDE Contacts = addressbook = contacts = kde-contacts\n"
" vCard 2.1 (default) = text/x-vcard\n"
" vCard 3.0 = text/vcard\n"
" The later is the internal format of KDE and preferred with\n"
" servers that support it. One such server is ScheduleWorld\n"
" together with the \"card3\" uri.\n"
"KDE Calendar = calendar = events = kde-events\n"
" iCalendar 2.0 (default) = text/calendar\n"
" vCalendar 1.0 = text/x-calendar\n"
"KDE Task List = KDE Tasks = todo = tasks = kde-tasks\n"
" iCalendar 2.0 (default) = text/calendar\n"
" vCalendar 1.0 = text/x-calendar\n"
"KDE Memos = memo = memos = kde-memos\n"
" plain text in UTF-8 (default) = text/plain\n",
Values() +
(Aliases("KDE Address Book") + "KDE Contacts" + "kde-contacts") +
(Aliases("KDE Calendar") + "kde-calendar") +
(Aliases("KDE Task List") + "KDE Tasks" + "kde-tasks") +
(Aliases("KDE Memos") + "kde-memos"));
#ifdef ENABLE_AKONADI
#ifdef ENABLE_UNIT_TESTS
class AkonadiTest : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(AkonadiTest);
CPPUNIT_TEST(testInstantiate);
CPPUNIT_TEST(testOpenDefaultCalendar);
CPPUNIT_TEST(testOpenDefaultTodo);
CPPUNIT_TEST(testOpenDefaultMemo);
CPPUNIT_TEST(testTimezones);
CPPUNIT_TEST_SUITE_END();
protected:
static string addItem(boost::shared_ptr<TestingSyncSource> source,
string &data) {
SyncSourceRaw::InsertItemResult res = source->insertItemRaw("", data);
return res.m_luid;
}
void testInstantiate() {
boost::shared_ptr<SyncSource> source;
source.reset(SyncSource::createTestingSource("addressbook", "addressbook", true));
source.reset(SyncSource::createTestingSource("addressbook", "contacts", true));
source.reset(SyncSource::createTestingSource("addressbook", "kde-contacts", true));
source.reset(SyncSource::createTestingSource("addressbook", "KDE Contacts", true));
source.reset(SyncSource::createTestingSource("addressbook", "KDE Address Book:text/x-vcard", true));
source.reset(SyncSource::createTestingSource("addressbook", "KDE Address Book:text/vcard", true));
source.reset(SyncSource::createTestingSource("calendar", "calendar", true));
source.reset(SyncSource::createTestingSource("calendar", "kde-calendar", true));
source.reset(SyncSource::createTestingSource("calendar", "KDE Calendar:text/calendar", true));
source.reset(SyncSource::createTestingSource("calendar", "tasks", true));
source.reset(SyncSource::createTestingSource("calendar", "kde-tasks", true));
source.reset(SyncSource::createTestingSource("calendar", "KDE Tasks", true));
source.reset(SyncSource::createTestingSource("calendar", "KDE Task List:text/calendar", true));
source.reset(SyncSource::createTestingSource("calendar", "memos", true));
source.reset(SyncSource::createTestingSource("calendar", "kde-memos", true));
source.reset(SyncSource::createTestingSource("calendar", "KDE Memos:text/plain", true));
source.reset(SyncSource::createTestingSource("calendar", "KDE Memos:text/calendar", true));
}
void testOpenDefaultAddressBook() {
boost::shared_ptr<TestingSyncSource> source;
source.reset((TestingSyncSource *)SyncSource::createTestingSource("contacts", "kde-contacts", true, NULL));
CPPUNIT_ASSERT_NO_THROW(source->open());
}
void testOpenDefaultCalendar() {
boost::shared_ptr<TestingSyncSource> source;
source.reset((TestingSyncSource *)SyncSource::createTestingSource("calendar", "kde-calendar", true, NULL));
CPPUNIT_ASSERT_NO_THROW(source->open());
}
void testOpenDefaultTodo() {
boost::shared_ptr<TestingSyncSource> source;
source.reset((TestingSyncSource *)SyncSource::createTestingSource("calendar", "kde-tasks", true, NULL));
CPPUNIT_ASSERT_NO_THROW(source->open());
}
void testOpenDefaultMemo() {
boost::shared_ptr<TestingSyncSource> source;
source.reset((TestingSyncSource *)SyncSource::createTestingSource("calendar", "kde-memos", true, NULL));
CPPUNIT_ASSERT_NO_THROW(source->open());
}
void testTimezones() {
const char *prefix = getenv("CLIENT_TEST_EVOLUTION_PREFIX");
if (!prefix) {
prefix = "SyncEvolution_Test_";
}
boost::shared_ptr<TestingSyncSource> source;
source.reset((TestingSyncSource *)SyncSource::createTestingSource("ical20", "kde-calendar", true, prefix));
CPPUNIT_ASSERT_NO_THROW(source->open());
string newyork =
"BEGIN:VCALENDAR\n"
"PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
"VERSION:2.0\n"
"BEGIN:VTIMEZONE\n"
"TZID:America/New_York\n"
"BEGIN:STANDARD\n"
"TZOFFSETFROM:-0400\n"
"TZOFFSETTO:-0500\n"
"TZNAME:EST\n"
"DTSTART:19701025T020000\n"
"RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10\n"
"END:STANDARD\n"
"BEGIN:DAYLIGHT\n"
"TZOFFSETFROM:-0500\n"
"TZOFFSETTO:-0400\n"
"TZNAME:EDT\n"
"DTSTART:19700405T020000\n"
"RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=4\n"
"END:DAYLIGHT\n"
"END:VTIMEZONE\n"
"BEGIN:VEVENT\n"
"UID:artificial\n"
"DTSTAMP:20060416T205224Z\n"
"DTSTART;TZID=America/New_York:20060406T140000\n"
"DTEND;TZID=America/New_York:20060406T143000\n"
"TRANSP:OPAQUE\n"
"SEQUENCE:2\n"
"SUMMARY:timezone New York with custom definition\n"
"DESCRIPTION:timezone New York with custom definition\n"
"CLASS:PUBLIC\n"
"CREATED:20060416T205301Z\n"
"LAST-MODIFIED:20060416T205301Z\n"
"END:VEVENT\n"
"END:VCALENDAR\n";
string luid;
CPPUNIT_ASSERT_NO_THROW(luid = addItem(source, newyork));
string newyork_suffix = newyork;
boost::replace_first(newyork_suffix,
"UID:artificial",
"UID:artificial-2");
boost::replace_all(newyork_suffix,
"TZID:America/New_York",
"TZID://FOOBAR/America/New_York-SUFFIX");
CPPUNIT_ASSERT_NO_THROW(luid = addItem(source, newyork_suffix));
string notimezone =
"BEGIN:VCALENDAR\n"
"PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
"VERSION:2.0\n"
"BEGIN:VEVENT\n"
"UID:artificial-3\n"
"DTSTAMP:20060416T205224Z\n"
"DTSTART;TZID=America/New_York:20060406T140000\n"
"DTEND;TZID=America/New_York:20060406T143000\n"
"TRANSP:OPAQUE\n"
"SEQUENCE:2\n"
"SUMMARY:timezone New York without custom definition\n"
"DESCRIPTION:timezone New York without custom definition\n"
"CLASS:PUBLIC\n"
"CREATED:20060416T205301Z\n"
"LAST-MODIFIED:20060416T205301Z\n"
"END:VEVENT\n"
"END:VCALENDAR\n";
CPPUNIT_ASSERT_NO_THROW(luid = addItem(source, notimezone));
// fake VTIMEZONE where daylight saving starts on first Sunday in March
string fake_march =
"BEGIN:VCALENDAR\n"
"PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
"VERSION:2.0\n"
"BEGIN:VTIMEZONE\n"
"TZID:FAKE\n"
"BEGIN:STANDARD\n"
"TZOFFSETFROM:-0400\n"
"TZOFFSETTO:-0500\n"
"TZNAME:EST MARCH\n"
"DTSTART:19701025T020000\n"
"RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10\n"
"END:STANDARD\n"
"BEGIN:DAYLIGHT\n"
"TZOFFSETFROM:-0500\n"
"TZOFFSETTO:-0400\n"
"TZNAME:EDT\n"
"DTSTART:19700405T020000\n"
"RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=3\n"
"END:DAYLIGHT\n"
"END:VTIMEZONE\n"
"BEGIN:VEVENT\n"
"UID:artificial-4\n"
"DTSTAMP:20060416T205224Z\n"
"DTSTART;TZID=FAKE:20060406T140000\n"
"DTEND;TZID=FAKE:20060406T143000\n"
"TRANSP:OPAQUE\n"
"SEQUENCE:2\n"
"SUMMARY:fake timezone with daylight starting in March\n"
"CLASS:PUBLIC\n"
"CREATED:20060416T205301Z\n"
"LAST-MODIFIED:20060416T205301Z\n"
"END:VEVENT\n"
"END:VCALENDAR\n";
CPPUNIT_ASSERT_NO_THROW(luid = addItem(source, fake_march));
string fake_may = fake_march;
boost::replace_first(fake_may,
"UID:artificial-4",
"UID:artificial-5");
boost::replace_first(fake_may,
"RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=3",
"RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=5");
boost::replace_first(fake_may,
"starting in March",
"starting in May");
boost::replace_first(fake_may,
"TZNAME:EST MARCH",
"TZNAME:EST MAY");
CPPUNIT_ASSERT_NO_THROW(luid = addItem(source, fake_may));
// insert again, shouldn't re-add timezone
CPPUNIT_ASSERT_NO_THROW(luid = addItem(source, fake_may));
}
};
SYNCEVOLUTION_TEST_SUITE_REGISTRATION(AkonadiTest);
#endif // ENABLE_UNIT_TESTS
#ifdef ENABLE_INTEGRATION_TESTS
namespace {
#if 0
}
#endif
static class iCal20Test : public RegisterSyncSourceTest {
public:
iCal20Test() : RegisterSyncSourceTest("ical20", "ical20") {}
virtual void updateConfig(ClientTestConfig &config) const
{
config.type = "kde-calendar";
}
} iCal20Test;
static class iTodo20Test : public RegisterSyncSourceTest {
public:
iTodo20Test() : RegisterSyncSourceTest("itodo20", "itodo20") {}
virtual void updateConfig(ClientTestConfig &config) const
{
config.type = "kde-tasks";
}
} iTodo20Test;
static class MemoTest : public RegisterSyncSourceTest {
public:
MemoTest() : RegisterSyncSourceTest("text", "text") {}
virtual void updateConfig(ClientTestConfig &config) const
{
config.type = "KDE Memos"; // use an alias here to test that
}
} memoTest;
}
#endif // ENABLE_INTEGRATION_TESTS
#endif // ENABLE_AKONADI
SE_END_CXX

View File

@ -0,0 +1,20 @@
AM_CPPFLAGS = -I$(srcdir)/../../ -I$(top_srcdir)/test $(BACKEND_CPPFLAGS)
EXTRA_DIST = configure-sub.in
SYNCSOURCES = syncakonadi.la
MOSTLYCLEANFILES = $(SYNCSOURCES)
if ENABLE_MODULES
backend_LTLIBRARIES = $(SYNCSOURCES)
else
noinst_LTLIBRARIES = $(SYNCSOURCES)
endif
MAINTAINERCLEANFILES = Makefile.in
syncakonadi_la_SOURCES = \
akonadisyncsource.h \
akonadisyncsource.cpp
syncakonadi_la_LIBADD = $(KDEPIM_LIBS) ../../syncevo/libsyncevolution.la
syncakonadi_la_LDFLAGS = -module -avoid-version
syncakonadi_la_CXXFLAGS = $(SYNCEVOLUTION_CXXFLAGS)

View File

@ -0,0 +1,41 @@
Getting started with Akonadi on Debian testing:
aptitude install libakonadi-dev akonadi-server \
libqt4-dev kdepim-runtime \
kdepimlibs5-dev
Controlling Akonadi server:
akonadictl start/stop/restart
Debugging Akonadi:
akonadiconsole (from kdepim-runtime)
Configuring without Evolution and with Akonadi:
<path>/syncevolution/configure --with-synthesis-src=<path>/libsynthesis \
CFLAGS="-g -Wall -Werror -Wno-unknown-pragmas" \
CXXFLAGS="-g -Wall -Werror -Wno-unknown-pragmas" \
--disable-shared --enable-static \
--enable-libcurl \
--enable-unit-tests --enable-integration-tests \
--disable-ecal --disable-ebook --disable-libsoup
This creates src/syncevolution and src/client-test which can be run under
a debugger directly.
Query databases:
syncevolution
Configuring syncevolution for contacts with Akonadi as backend:
syncevolution --source-property sync=none \
--sync-property username=... \
--sync-property password=... \
scheduleworld
syncevolution --source-property sync=two-way \
--source-property type=kde-contacts \
--source-property evolutionsource=akonadi:?... \
scheduleworld addressbook
Initial run:
syncevolution --sync slow scheduleworld addressbook
All following runs:
syncevolution scheduleworld

View File

@ -18,269 +18,177 @@
*/
#include "akonadisyncsource.h"
#include "timetrackingobserver.h"
#ifdef ENABLE_AKONADI
#include <Akonadi/ItemCreateJob>
#include <Akonadi/ItemDeleteJob>
#include <Akonadi/ItemFetchJob>
#include <Akonadi/ItemFetchScope>
#include <Akonadi/ItemModifyJob>
#include <Akonadi/CollectionFetchJob>
#include <Akonadi/Control>
#include <kurl.h>
#include <QtCore/QCoreApplication>
SE_BEGIN_CXX
using namespace Akonadi;
AkonadiSyncSource::AkonadiSyncSource(TimeTrackingObserver *observer,
AkonadiSyncSourceConfig *config,
SyncManagerConfig *managerConfig)
: SyncSource(config->getName(), config)
, m_observer(observer)
AkonadiSyncSource::AkonadiSyncSource(const char *submime,
const SyncSourceParams &params) :
TrackingSyncSource(params),
m_subMime(submime)
{
managerConfig->setSyncSourceConfig(*config);
}
AkonadiSyncSource::~AkonadiSyncSource()
{
}
int AkonadiSyncSource::beginSync()
bool AkonadiSyncSource::isEmpty(){return false;}
void AkonadiSyncSource::start()
{
// Fetch all item sets from the time-tracking observer that
// correspond to this SyncSource's corresponding Akonadi collection
m_allItems = m_observer->allItems(m_lastSyncTime, m_collectionId);
m_newItems = m_observer->addedItems(m_lastSyncTime, m_collectionId);
m_updatedItems = m_observer->changedItems(m_lastSyncTime, m_collectionId);
m_deletedItems = m_observer->removedItems(m_lastSyncTime, m_collectionId);
m_currentTime = QDateTime::currentDateTime().toUTC();
kDebug() << "Begin sync at" << m_currentTime;
return 0;
}
int AkonadiSyncSource::endSync()
{
m_lastSyncTime = m_currentTime;
kDebug() << "End sync at" << m_lastSyncTime;
return 0;
}
int AkonadiSyncSource::addItem(SyncItem& syncItem)
{
kDebug() << "Remote wants us to add" << syncItemToString(syncItem);
Item item;
item.setMimeType(syncItem.getDataType());
item.setPayloadFromData(QByteArray((char *)syncItem.getData()));
ItemCreateJob *createJob = new ItemCreateJob(item, Collection(m_collectionId));
if (createJob->exec()) {
item = createJob->item();
kDebug() << "Created new item" << item.id() << "with mimetype" << item.mimeType()
<< "and added it to collection" << m_collectionId;
syncItem.setKey(QByteArray::number(item.id()));
//TODO: Read-only datastores may not have actually added something here!
return 200; // Ok, the SyncML command completed successfully
} else {
kDebug() << "Unable to create item" << item.id() << "in Akonadi datastore";
return 211; // Failed, the recipient encountered an error
int argc = 1;
static const char *prog = "syncevolution";
static char *argv[] = { (char *)&prog, NULL };
if (!qApp) {
new QCoreApplication(argc, argv);
}
}
int AkonadiSyncSource::updateItem(SyncItem& syncItem)
SyncSource::Databases AkonadiSyncSource::getDatabases()
{
kDebug() << "Remote wants us to update" << syncItemToString(syncItem);
start();
Entity::Id syncItemId = QByteArray(syncItem.getKey()).toLongLong();
Databases res;
// TODO: insert databases which match the "type"
// of the source, including a user-visible description
// and a database IDs. Exactly one of the databases
// should be marked as the default one used by the
// source.
// res.push_back("Contacts", "some-KDE-specific-ID", isDefault);
// Fetch item which shall be modified
ItemFetchJob *fetchJob = new ItemFetchJob(Item(syncItemId));
if (fetchJob->exec()) {
Item item = fetchJob->items().first();
CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection::root(),
CollectionFetchJob::Recursive);
// fetchJob->setMimeTypeFilter(m_subMime.c_str());
if (!fetchJob->exec()) {
throwError("cannot list collections");
}
// Modify item, e.g. set new payload data
QByteArray data((char *)syncItem.getData());
item.setPayloadFromData(data);
// Store back modified item
ItemModifyJob *modifyJob = new Akonadi::ItemModifyJob(item);
if (modifyJob->exec()) {
kDebug() << "Item" << item.id() << "modified successfully";
return 200; // Ok, the SyncML command completed successfully
} else {
return 211; // Failed, the recipient encountered an error
// the first collection of the right type is the default
// TODO: is there a better way to choose the default?
bool isFirst = true;
Collection::List collections = fetchJob->collections();
foreach(const Collection &collection, collections) {
// TODO: filter out collections which contain no items
// of the type we sync (m_subMime)
if (true) {
res.push_back(Database(collection.name().toUtf8().constData(),
collection.url().url().toUtf8().constData(),
isFirst));
isFirst = false;
}
} else {
kDebug() << "Unable to find item with id" << syncItemId;
return 211; // Failed, the recipient encountered an error
}
return res;
}
void AkonadiSyncSource::open()
{
start();
// the "evolutionsource" property, empty for default,
// otherwise the collection URL or a name
string id = getDatabaseID();
// TODO: support selection by name and empty ID for default
// TODO: check for invalid URL?!
m_collection = Collection::fromUrl(KUrl(id.c_str()));
}
void AkonadiSyncSource::listAllItems(SyncSourceRevisions::RevisionMap_t &revisions)
{
// copy all local IDs and the corresponding revision
ItemFetchJob *fetchJob = new ItemFetchJob(m_collection);
if (!fetchJob->exec()) {
throwError("listing items");
}
BOOST_FOREACH(const Item &item, fetchJob->items()) {
// TODO: filter out items which don't have the right type
// (for example, VTODO when syncing events)
// if (... == m_subMime)
revisions[QByteArray::number(item.id()).constData()] =
QByteArray::number(item.revision()).constData();
}
}
int AkonadiSyncSource::deleteItem(SyncItem& syncItem)
void AkonadiSyncSource::close()
{
kDebug() << "Remote wants us to delete" << syncItemToString(syncItem);
// TODO: close collection!?
}
Entity::Id syncItemId = QByteArray(syncItem.getKey()).toLongLong();
TrackingSyncSource::InsertItemResult
AkonadiSyncSource::insertItem(const std::string &luid, const std::string &data, bool raw)
{
Item item;
if (luid.empty()) {
item.setMimeType(m_subMime.c_str());
item.setPayloadFromData(QByteArray(data.c_str()));
ItemCreateJob *createJob = new ItemCreateJob(item, m_collection);
if (!createJob->exec()) {
throwError(string("storing new item ") + luid);
}
item = createJob->item();
} else {
Entity::Id syncItemId = QByteArray(luid.c_str()).toLongLong();
ItemFetchJob *fetchJob = new ItemFetchJob(Item(syncItemId));
if (!fetchJob->exec()) {
throwError(string("checking item ") + luid);
}
ItemModifyJob *modifyJob = new ItemModifyJob(item);
// TODO: SyncEvolution must pass the known revision that
// we are updating.
// TODO: check that the item has not been updated in the meantime
if (!modifyJob->exec()) {
throwError(string("updating item ") + luid);
}
item = modifyJob->item();
}
// TODO: Read-only datastores may not have actually added something here!
return InsertItemResult(QByteArray::number(item.id()).constData(),
QByteArray::number(item.revision()).constData(),
false);
}
void AkonadiSyncSource::removeItem(const string &luid)
{
Entity::Id syncItemId = QByteArray(luid.c_str()).toLongLong();
// Delete the item from our collection
// TODO: check that the revision is right (need revision from SyncEvolution)
ItemDeleteJob *deleteJob = new ItemDeleteJob(Item(syncItemId));
if (deleteJob->exec()) {
return 200; // Ok, the SyncML command completed successfully
if (!deleteJob->exec()) {
throwError(string("deleting item " ) + luid);
}
}
void AkonadiSyncSource::readItem(const std::string &luid, std::string &data, bool raw)
{
Entity::Id syncItemId = QByteArray(luid.c_str()).toLongLong();
ItemFetchJob *fetchJob = new ItemFetchJob(Item(syncItemId));
fetchJob->fetchScope().fetchFullPayload();
if (fetchJob->exec()) {
QByteArray payload = fetchJob->items().first().payloadData();
data.assign(payload.constData(),
payload.size());
} else {
return 211; // Failed, the recipient encountered an error
throwError(string("extracting item " ) + luid);
}
}
int AkonadiSyncSource::removeAllItems()
{
kDebug() << "Remote wants us to remove all items";
// Remove all items from our collection
ItemDeleteJob *deleteJob = new ItemDeleteJob(Collection(m_collectionId));
if (deleteJob->exec()) {
return 200; // Ok, the SyncML command completed successfully
} else {
return 211; // Failed, the recipient encountered an error
}
}
SyncItem *AkonadiSyncSource::first(ItemSet set, bool withData)
{
SyncState state = SYNC_STATE_NONE;
Akonadi::Item item;
switch (set) {
case AllItems:
m_allItemsIndex = 0;
if (m_allItemsIndex < m_allItems.size()) {
kDebug() << "Fetch first item from 'all items' set";
item = m_allItems[m_allItemsIndex];
}
break;
case NewItems:
m_newItemsIndex = 0;
if (m_newItemsIndex < m_newItems.size()) {
kDebug() << "Fetch first item from 'new items' set";
state = SYNC_STATE_NEW;
item = m_newItems[m_newItemsIndex];
}
break;
case UpdatedItems:
m_updatedItemsIndex = 0;
if (m_updatedItemsIndex < m_updatedItems.size()) {
kDebug() << "Fetch first item from 'updated items' set";
state = SYNC_STATE_UPDATED;
item = m_updatedItems[m_updatedItemsIndex];
}
break;
case DeletedItems:
m_deletedItemsIndex = 0;
if (m_deletedItemsIndex < m_deletedItems.size()) {
kDebug() << "Fetch first item from 'next items' set";
state = SYNC_STATE_DELETED;
item = m_deletedItems[m_deletedItemsIndex];
}
break;
}
if (item.isValid()) {
return syncItem(item, withData, state);
} else {
kDebug() << "Fetched invalid item";
return 0;
}
}
SyncItem *AkonadiSyncSource::next(ItemSet set, bool withData)
{
SyncState state = SYNC_STATE_NONE;
Akonadi::Item item;
switch (set) {
case AllItems:
m_allItemsIndex++;
if (m_allItemsIndex < m_allItems.size()) {
kDebug() << "Fetch item" << m_allItemsIndex << "from 'all items' set";
item = m_allItems[m_allItemsIndex];
}
break;
case NewItems:
m_newItemsIndex++;
if (m_newItemsIndex < m_newItems.size()) {
kDebug() << "Fetch item" << m_newItemsIndex << "from 'new items' set";
state = SYNC_STATE_NEW;
item = m_newItems[m_newItemsIndex];
}
break;
case UpdatedItems:
m_updatedItemsIndex++;
if (m_updatedItemsIndex < m_updatedItems.size()) {
kDebug() << "Fetch item" << m_updatedItemsIndex << "from 'updated items' set";
state = SYNC_STATE_UPDATED;
item = m_updatedItems[m_updatedItemsIndex];
}
break;
case DeletedItems:
m_deletedItemsIndex++;
if (m_deletedItemsIndex < m_deletedItems.size()) {
kDebug() << "Fetch item" << m_deletedItemsIndex << "from 'deleted items' set";
state = SYNC_STATE_DELETED;
item = m_deletedItems[m_deletedItemsIndex];
}
break;
}
if (item.isValid()) {
return syncItem(item, withData, state);
} else {
kDebug() << "Fetched invalid item";
return 0;
}
}
SyncItem *AkonadiSyncSource::syncItem(const Item &item, bool withData, SyncState state) const
{
SyncItem *syncItem = new SyncItem();
kDebug() << "Return SyncItem for item" << item.id();
syncItem->setKey(QByteArray::number(item.id()));
syncItem->setModificationTime(m_lastSyncTime.toTime_t());
syncItem->setState(state);
if (withData) {
ItemFetchJob *fetchJob = new ItemFetchJob(item);
fetchJob->fetchScope().fetchFullPayload();
if (fetchJob->exec()) {
kDebug() << "Add payload data";
QByteArray data = fetchJob->items().first().payloadData().toBase64();
syncItem->setData(data, data.size());
syncItem->setDataEncoding(SyncItem::encodings::escaped);
syncItem->setDataType(getConfig().getType());
} else {
kDebug() << "Unable to add payload data";
}
}
//kDebug() << "Created SyncItem:" << syncItemToString(*syncItem);
return syncItem;
}
QString AkonadiSyncSource::syncItemToString(SyncItem& syncItem) const
{
QByteArray data((char *)syncItem.getData());
QString ret("Key: ");
ret += syncItem.getKey();
ret += " Mod.Time: ";
ret += QString::number(syncItem.getModificationTime());
ret += " Encoding: ";
ret += syncItem.getDataEncoding();
ret += " Size: ";
ret += QString::number(syncItem.getDataSize());
ret += " Type: ";
ret += syncItem.getDataType();
ret += " State: ";
ret += QString::number(syncItem.getState());
ret += " Data:\n";
ret += data;
return ret;
}
#include "moc_akonadisyncsource.cpp"
SE_END_CXX
#endif // ENABLE_AKONADI

View File

@ -20,136 +20,112 @@
#ifndef AKONADISYNCSOURCE_H
#define AKONADISYNCSOURCE_H
#include "settings.h"
#include "config.h"
#ifdef ENABLE_AKONADI
#include <Akonadi/Collection>
#include <Akonadi/Item>
#include <QDateTime>
#include <QObject>
#include <QtCore/QDateTime>
#include <funambol/common/spds/SyncManagerConfig.h>
#include <funambol/common/spds/SyncSource.h>
#include <syncevo/TrackingSyncSource.h>
SE_BEGIN_CXX
class TimeTrackingObserver;
/**
* Base config for all sync sources.
* General purpose Akonadi Sync Source. Choosing the type of data is
* done when instantiating it, using the Akonadi MIME subtypes.
* Payload is always using the native Akonadi format (no special "raw"
* and "engine" formats).
*
* Change tracking is done via the item uid/revision attributes.
*
*
* Databases (collections in Akonadi terminology) are selected via
* their int64 ID number.
*/
class AkonadiSyncSourceConfig : public QObject,
public SyncSourceConfig
class AkonadiSyncSource : public TrackingSyncSource
{
Q_OBJECT
public:
enum SyncMode {
Slow = 0,
TwoWay,
OneWayFromServer,
OneWayFromClient,
RefreshFromServer,
RefreshFromClient
};
AkonadiSyncSourceConfig(unsigned long lastSync = 0,
const char *uri = "default")
: SyncSourceConfig()
{
setURI(uri);
setLast(lastSync); // Set last sync time
setVersion(""); // Don't care for the SyncML version
setEncoding(SyncItem::encodings::escaped); // Means base64 in Funambol tongue
setSyncModes("slow,two-way,one-way-from-server,one-way-from-client,refresh-from-server,refresh-from-client");
//setSupportedTypes(""); // This can be set by derived sync sources
setEncryption("");
// Determine how to sync
switch(Settings::self()->syncMode()) {
case 0: setSync("slow");
kDebug() << "Use 'Slow' sync mode"; break;
case 1: setSync("two-way");
kDebug() << "Use 'TwoWay' sync mode"; break;
case 2: setSync("one-way-from-server");
kDebug() << "Use 'OneWayFromServer' sync mode"; break;
case 3: setSync("one-way-from-client");
kDebug() << "Use 'OneWayFromClient' sync mode"; break;
case 4: setSync("refresh-from-server");
kDebug() << "Use 'RefreshFromServer' sync mode"; break;
case 5: setSync("refresh-from-client");
kDebug() << "Use 'RefreshFromClient' sync mode"; break;
}
kDebug() << "Sync source config for" << getName() << "with URI" << getURI() << "set up";
}
};
/**
* Abstract base class for all sync sources.
*/
class AkonadiSyncSource : public QObject
, public SyncSource
{
Q_OBJECT
public:
/**
* @param submime the MIME type string used by Akonadi
* to identify contacts, tasks, events, etc.
* @param params the SyncEvolution source parameters
*/
AkonadiSyncSource(const char *submime,
const SyncSourceParams &params);
virtual ~AkonadiSyncSource();
// The following are Funambol API specific methods
int beginSync();
int endSync();
int addItem(SyncItem& syncItem);
int updateItem(SyncItem& syncItem);
int deleteItem(SyncItem& syncItem);
int removeAllItems();
SyncItem *getFirstItem() { return first(AllItems); }
SyncItem *getNextItem() { return next(AllItems); }
SyncItem *getFirstNewItem() { return first(NewItems); }
SyncItem *getNextNewItem() { return next(NewItems); }
SyncItem *getFirstUpdatedItem() { return first(UpdatedItems); }
SyncItem *getNextUpdatedItem() { return next(UpdatedItems); }
SyncItem *getFirstDeletedItem() { return first(DeletedItems, false); }
SyncItem *getNextDeletedItem() { return next(DeletedItems, false); }
SyncItem *getFirstItemKey() { return first(AllItems, false); }
SyncItem *getNextItemKey() { return next(AllItems, false); }
// End of Funambol API specific methods
QDateTime lastSyncTime() const { return m_lastSyncTime; }
protected:
AkonadiSyncSource(TimeTrackingObserver *observer,
AkonadiSyncSourceConfig *config,
SyncManagerConfig *managerConfig);
TimeTrackingObserver *m_observer;
Akonadi::Entity::Id m_collectionId;
QDateTime m_lastSyncTime;
QDateTime m_currentTime;
/* methods that have to be implemented to complete TrackingSyncSource */
virtual Databases getDatabases();
virtual void open();
virtual void listAllItems(SyncSourceRevisions::RevisionMap_t &revisions);
virtual InsertItemResult insertItem(const std::string &luid, const std::string &item, bool raw);
virtual void readItem(const std::string &luid, std::string &item, bool raw);
virtual void removeItem(const string &luid);
virtual void close();
virtual bool isEmpty();
private:
enum ItemSet {
AllItems = 0,
NewItems,
UpdatedItems,
DeletedItems
};
Akonadi::Collection m_collection;
const std::string m_subMime;
SyncItem *first(ItemSet set, bool withData = true);
SyncItem *next(ItemSet set, bool withData = true);
SyncItem *syncItem(const Akonadi::Item &item, bool withData = true, SyncState state = SYNC_STATE_NONE) const;
QString syncItemToString(SyncItem& syncItem) const;
int m_allItemsIndex;
int m_newItemsIndex;
int m_updatedItemsIndex;
int m_deletedItemsIndex;
Akonadi::Item::List m_allItems;
Akonadi::Item::List m_newItems;
Akonadi::Item::List m_updatedItems;
Akonadi::Item::List m_deletedItems;
void start();
};
#endif
class AkonadiContactSource : public AkonadiSyncSource
{
public:
AkonadiContactSource(const SyncSourceParams &params) :
AkonadiSyncSource("text/vcard", params)
{}
virtual const char *getMimeType() const { return "text/vcard"; }
virtual const char *getMimeVersion() const { return "3.0"; }
};
class AkonadiCalendarSource : public AkonadiSyncSource
{
public:
AkonadiCalendarSource(const SyncSourceParams &params) :
AkonadiSyncSource("application/x-vnd.akonadi.calendar.event", params)
{}
// TODO: the items are expected to be complete VCALENDAR with
// all necessary VTIMEZONEs and one VEVENT (here) resp. VTODO
// (AkonadiTodoSource). Not sure what we get from Akonadi.
virtual const char *getMimeType() const { return "text/calendar"; }
virtual const char *getMimeVersion() const { return "2.0"; }
};
class AkonadiTaskSource : public AkonadiSyncSource
{
public:
AkonadiTaskSource(const SyncSourceParams &params) :
AkonadiSyncSource("text/x-vnd.akonadi.calendar.todo", params)
{}
virtual const char *getMimeType() const { return "text/calendar"; }
virtual const char *getMimeVersion() const { return "2.0"; }
};
class AkonadiMemoSource : public AkonadiSyncSource
{
public:
AkonadiMemoSource(const SyncSourceParams &params) :
AkonadiSyncSource("text/x-vnd.akonadi.calendar.journal", params)
{}
// TODO: the AkonadiMemoSource is expected to import/export
// plain text with the summary in the first line; currently
// the AkonadiSyncSource will use VJOURNAL
virtual const char *getMimeType() const { return "text/plain"; }
virtual const char *getMimeVersion() const { return "1.0"; }
};
SE_END_CXX
#endif // ENABLE_AKONADI
#endif // AKONADISYNCSOURCE_H

View File

@ -0,0 +1,49 @@
dnl -*- mode: Autoconf; -*-
dnl Invoke autogen.sh to produce a configure script.
# Check for Akonadi. There is no .pc file for it,
# so fall back to normal header file and library checking.
# kdepimlibs5-dev >= 4.3 provides the necessary files.
AKONADIFOUND=yes
if ! test "$KDEPIM_CFLAGS"; then
KDEPIM_CFLAGS="-I/usr/include/KDE -I/usr/include/qt4"
fi
if ! test "$KDEPIM_LIBS"; then
KDEPIM_LIBS="-lakonadi-kde"
fi
AC_LANG_PUSH(C++)
old_CPPFLAGS="$CPPFLAGS"
CPPFLAGS="$CPPFLAGS $KDEPIM_CFLAGS"
AC_CHECK_HEADERS(Akonadi/Collection, [], [AKONADIFOUND=no])
CPPFLAGS="$old_CPPFLAGS"
AC_LANG_POP(C++)
# In contrast to the Evolution backend, the Akonadi backend is
# currently considered optional. "configure" will enable it if
# possible, but won't complain if the necessary development files
# are missing.
AC_ARG_ENABLE_BACKEND(akonadi, akonadi,
AS_HELP_STRING([--disable-akonadi], [disable access to Akonadi (default is to use it if akonadi.pc is found)]),
[enable_akonadi="$enableval"
test $AKONADIFOUND = "yes" || $enable_akonadi = "no" || AC_MSG_ERROR([akonadi.pc not found. Install it to compile with the Akonadi backend enabled.])],
[enable_akonadi=$AKONADIFOUND])
if test "$enable_akonadi" = "yes"; then
# conditional compilation in preprocessor
AC_DEFINE(ENABLE_AKONADI, 1, [Akonadi available])
else
# avoid unneeded dependencies on Akonadi
KDEPIM_CFLAGS=
KDEPIM_LIBS=
fi
AC_SUBST(KDEPIM_LIBS)
AC_SUBST(KDEPIM_CFLAGS)
# conditional compilation in make
AM_CONDITIONAL([ENABLE_AKONADI], [test "$enable_akonadi" = "yes"])
# let others include Akonadi backend's header file
# (not strictly necessary, could be avoided by not
# including Akonadi header files in public header file
# of source)
BACKEND_CPPFLAGS="$BACKEND_CPPFLAGS $KDEPIM_CFLAGS"