added a sync source which stores contacts in a SQLite database

git-svn-id: https://zeitsenke.de/svn/SyncEvolution/trunk@381 15ad00c4-1369-45f4-8270-35d70d36bdcd
This commit is contained in:
Patrick Ohly 2007-08-13 20:46:49 +00:00
parent ce163f1bb1
commit 03aaa74e45
13 changed files with 986 additions and 29 deletions

View file

@ -179,7 +179,7 @@ PKG_CHECK_MODULES(EPACKAGE, [$pkg_emodules_12], EDSFOUND=yes, [EDSFOUND=no])
if test "x${EDSFOUND}" = "xno"; then
PKG_CHECK_MODULES(EPACKAGE, [$pkg_emodules_11], EDSFOUND=yes, [EDSFOUND=no])
if test "x${EDSFOUND}" = "xno"; then
PKG_CHECK_MODULES(EPACKAGE, [$pkg_emodules_10],,AC_MSG_ERROR($evomissing))
PKG_CHECK_MODULES(EPACKAGE, [$pkg_emodules_10], EDSFOUND=yes, [EDSFOUND=no])
fi
fi
AC_SUBST(EPACKAGE_CFLAGS)
@ -207,21 +207,39 @@ fi
AC_SUBST(EBOOK_CFLAGS)
AC_SUBST(EBOOK_LIBS)
dnl check for sqlite
PKG_CHECK_MODULES(SQLITE, sqlite3, SQLITEFOUND=yes, [SQLITEFOUND=no])
AC_SUBST(SQLITE_CFLAGS)
AC_SUBST(SQLITE_LIBS)
dnl select backends
AC_ARG_ENABLE(ebook, AS_HELP_STRING([--disable-ebook], [disable access to addressbooks (default on if available)]),
[enable_ebook="$enableval"], [enable_ebook=$EBOOKFOUND])
AC_ARG_ENABLE(ecal, AS_HELP_STRING([--disable-ecal], [disable access to calendars and tasks (default on if available)]),
[enable_ecal="$enableval"], [enable_ecal=$ECALFOUND])
AC_ARG_ENABLE(sqlite, AS_HELP_STRING([--enable-sqlite], [enable access to PIM data stored in SQLite files (default off)]),
[enable_sqlite="$enableval"], [enable_sqlite="no"])
enable_evo="no"
enable_any="no"
if test "$enable_ebook" = "yes"; then
test "x${EBOOKFOUND}" == "xyes" || AC_MSG_ERROR(["--enable-ebook requires pkg-config information for libebook, which was not found"])
AC_DEFINE(ENABLE_EBOOK, 1, [libebook available])
enable_evo="yes"
enable_any="yes"
fi
if test "$enable_ecal" = "yes"; then
test "x${ECALFOUND}" == "xyes" || AC_MSG_ERROR(["--enable-ecal requires pkg-config information for libecal, which was not found"])
AC_DEFINE(ENABLE_ECAL, 1, [libecal available])
enable_evo="yes"
enable_any="yes"
fi
if test "$enable_sqlite" = "yes"; then
test "x${SQLITEFOUND}" == "xyes" || AC_MSG_ERROR(["--enable-sqlite requires pkg-config information for sqlite, which was not found"])
AC_DEFINE(ENABLE_SQLITE, 1, [sqlite available])
enable_any="yes"
fi
@ -229,6 +247,17 @@ if test "$enable_any" = "no"; then
AC_MSG_ERROR([no backend enabled - refusing to continue: $anymissing])
fi
if test "$enable_evo" = "yes"; then
if test "$EDSFOUND" = "yes"; then
AC_DEFINE(HAVE_EDS, 1, [evolution-dataserver available])
else
AC_MSG_ERROR($evomissing)
fi
else
EPACKAGE_CFLAGS=
EPACKAGE_LIBS=
fi
dnl check for glib - calling g_type_init() is expected on Maemo
PKG_CHECK_MODULES(GLIB, "glib-2.0", GLIBFOUND=yes, GLIBFOUND=no)
if test "x${GLIBFOUND}" = "xno"; then
@ -252,6 +281,9 @@ fi
if test "$enable_ecal" == "yes"; then
SYNCEVOLUTION_MODULES="$SYNCEVOLUTION_MODULES syncecal.la"
fi
if test "$enable_sqlite" == "yes"; then
SYNCEVOLUTION_MODULES="$SYNCEVOLUTION_MODULES syncsqlite.la"
fi
dnl figure out whether we link all code statically or as modules
if test "$enable_shared" == "yes"; then

View file

@ -138,6 +138,10 @@ class EvolutionCalendarSource : public EvolutionSyncSource
void getChanges();
};
#else
typedef int ECalSourceType;
#endif // ENABLE_ECAL
#endif // INCL_EVOLUTIONSYNCSOURCE

View file

@ -23,12 +23,14 @@
#include "EvolutionContactSource.h"
#include "EvolutionCalendarSource.h"
#include "EvolutionMemoSource.h"
#include "SQLiteContactSource.h"
#include <common/base/Log.h>
#include <list>
#include <dlfcn.h>
#ifdef HAVE_EDS
ESource *EvolutionSyncSource::findSource( ESourceList *list, const string &id )
{
for (GSList *g = e_source_list_peek_groups (list); g; g = g->next) {
@ -47,21 +49,28 @@ ESource *EvolutionSyncSource::findSource( ESourceList *list, const string &id )
}
return NULL;
}
#endif // HAVE_EDS
void EvolutionSyncSource::throwError( const string &action, GError *gerror )
void EvolutionSyncSource::throwError( const string &action
#ifdef HAVE_EDS
, GError *gerror
#endif
)
{
string gerrorstr;
#ifdef HAVE_EDS
if (gerror) {
gerrorstr += ": ";
gerrorstr += gerror->message;
g_clear_error(&gerror);
} else {
} else
#endif
gerrorstr = ": failed";
}
throw runtime_error(string(getName()) + ": " + action + gerrorstr);
}
void EvolutionSyncSource::resetItems()
{
m_allItems.clear();
@ -143,6 +152,7 @@ EvolutionSyncSource *EvolutionSyncSource::createSource(
const char *modules[] = {
"syncebook.so.0",
"syncecal.so.0",
"syncsqlite.so.0",
NULL
};
@ -246,6 +256,14 @@ EvolutionSyncSource *EvolutionSyncSource::createSource(
if (error) {
throw runtime_error(name + ": access to calendars not compiled into this binary, " + mimeType + " not supported");
}
#endif
} else if (mimeType == "sqlite") {
#ifdef ENABLE_SQLITE
return new SQLiteContactSource(name, sc, strippedChangeId, id);
#else
if (error) {
throw runtime_error(name + ": access to sqlite not compiled into this binary, " + mimeType + " not supported");
}
#endif
}
#endif // ENABLE_MODULES

View file

@ -26,16 +26,18 @@
#include <ostream>
using namespace std;
#ifdef HAVE_EDS
#include <libedataserver/e-source.h>
#include <libedataserver/e-source-list.h>
#endif
#include <spds/SyncSource.h>
#include <spdm/ManagementNode.h>
#include <base/Log.h>
/**
* This class implements the functionality shared by
* both EvolutionCalenderSource and EvolutionContactSource:
* SyncEvolution accesses all sources through this interface. This
* class also implements common functionality for all SyncSources:
* - handling of change IDs and URI
* - finding the calender/contact backend
* - default implementation of SyncSource interface
@ -249,6 +251,7 @@ class EvolutionSyncSource : public SyncSource
protected:
#ifdef HAVE_EDS
/**
* searches the list for a source with the given uri or name
*
@ -257,6 +260,7 @@ class EvolutionSyncSource : public SyncSource
* @return pointer to source or NULL if not found
*/
ESource *findSource( ESourceList *list, const string &id );
#endif
/**
* throw an exception after a Gnome action failed and
@ -266,7 +270,11 @@ class EvolutionSyncSource : public SyncSource
* @param gerror if not NULL: a more detailed description of the failure,
* will be freed
*/
void throwError( const string &action, GError *gerror );
void throwError( const string &action
#ifdef HAVE_EDS
, GError *gerror = NULL
#endif
);
/**
* source specific part of beginSync() - throws exceptions in case of error

View file

@ -1,12 +1,12 @@
INCLUDES = @EPACKAGE_CFLAGS@ @EBOOK_CFLAGS@ @ECAL_CFLAGS@ @GLIB_CFLAGS@ @SYNC4J_CFLAGS@
INCLUDES = @EPACKAGE_CFLAGS@ @EBOOK_CFLAGS@ @ECAL_CFLAGS@ @GLIB_CFLAGS@ @SYNC4J_CFLAGS@ @SQLITE_CFLAGS@
# Please remove '-Wno-return-type' when Evo fixes the warning in their headers.
AM_CFLAGS = -Wall -Werror -Wno-return-type
bin_PROGRAMS = syncevolution
bin_SCRIPTS = synccompare
EXTRA_LTLIBRARIES = syncecal.la syncebook.la
MOSTLYCLEANFILES = syncecal.la syncebook.la
EXTRA_LTLIBRARIES = syncecal.la syncebook.la syncsqlite.la
MOSTLYCLEANFILES = syncecal.la syncebook.la syncsqlite.la
pkglib_LTLIBRARIES = @SYNCEVOLUTION_LTLIBRARIES@
EXTRA_PROGRAMS = client-test
check_PROGRAMS = @CPPUNIT_TESTS@
@ -53,6 +53,13 @@ SYNCEBOOK_SOURCES = \
EvolutionContactSource.h \
EvolutionContactSource.cpp
SYNCSQLITE_SOURCES = \
SQLiteSyncSource.h \
SQLiteSyncSource.cpp \
SQLiteContactSource.h \
SQLiteContactSource.cpp
CORE_LDADD = @EPACKAGE_LIBS@ @GLIB_LIBS@ @SYNC4J_LIBS@ @LIBS@
# put link to static c++ library into current directory, needed if compiling with --enable-static-c++
@ -63,7 +70,7 @@ syncevolution_SOURCES = \
syncevolution.cpp \
$(CORE_SOURCES)
# SYNCEVOLUTION_LDADD@ will be replaced with libsyncebook.la/libsyncecal.la
# SYNCEVOLUTION_LDADD@ will be replaced with libsyncebook.la/libsyncecal.la/libsyncsqlite.la
# if linking statically against them, empty otherwise;
# either way this does not lead to a dependency on those libs - done explicitly
#
@ -80,6 +87,10 @@ syncebook_la_SOURCES = $(SYNCEBOOK_SOURCES)
syncebook_la_LIBADD = @EBOOK_LIBS@
syncebook_la_LDFLAGS = -module -rpath '$(pkglibdir)'
syncsqlite_la_SOURCES = $(SYNCSQLITE_SOURCES)
syncsqlite_la_LIBADD = @SQLITE_LIBS@
syncsqlite_la_LDFLAGS = -module -rpath '$(pkglibdir)'
# test suite - *not* declared as an obligatory check,
# because some of them are known to fail and thus
# prevent a successful "distcheck"

420
src/SQLiteContactSource.cpp Normal file
View file

@ -0,0 +1,420 @@
/*
* Copyright (C) 2007 Patrick Ohly
* Copyright (C) 2007 Funambol
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "config.h"
#ifdef ENABLE_SQLITE
#include "SQLiteContactSource.h"
#include <common/base/Log.h>
#include "vocl/VConverter.h"
#include <algorithm>
#include <cctype>
#include <sstream>
using namespace vocl;
const char *SQLiteContactSource::getDefaultSchema()
{
return
"BEGIN TRANSACTION;"
"CREATE TABLE ABMultiValue (UID INTEGER PRIMARY KEY, record_id INTEGER, property INTEGER, identifier INTEGER, label INTEGER, value TEXT);"
"CREATE TABLE ABMultiValueEntry (parent_id INTEGER, key INTEGER, value TEXT, UNIQUE(parent_id, key));"
"CREATE TABLE ABMultiValueEntryKey (value TEXT, UNIQUE(value));"
"CREATE TABLE ABMultiValueLabel (value TEXT, UNIQUE(value));"
"CREATE TABLE ABPerson (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, First TEXT, Last TEXT, Middle TEXT, FirstPhonetic TEXT, MiddlePhonetic TEXT, LastPhonetic TEXT, Organization TEXT, Department TEXT, Note TEXT, Kind INTEGER, Birthday TEXT, JobTitle TEXT, Nickname TEXT, Prefix TEXT, Suffix TEXT, FirstSort TEXT, LastSort TEXT, CreationDate INTEGER, ModificationDate INTEGER, CompositeNameFallback TEXT);"
"INSERT INTO ABMultiValueLabel VALUES('_$!<Mobile>!$_');"
"INSERT INTO ABMultiValueLabel VALUES('_$!<Home>!$_');"
"INSERT INTO ABMultiValueLabel VALUES('_$!<Work>!$_');"
"INSERT INTO ABMultiValueEntryKey VALUES('CountryCode');"
"INSERT INTO ABMultiValueEntryKey VALUES('City');"
"INSERT INTO ABMultiValueEntryKey VALUES('Street');"
"INSERT INTO ABMultiValueEntryKey VALUES('State');"
"INSERT INTO ABMultiValueEntryKey VALUES('ZIP');"
"INSERT INTO ABMultiValueEntryKey VALUES('Country');"
"COMMIT;"
;
}
enum {
PERSON_LAST,
PERSON_MIDDLE,
PERSON_FIRST,
PERSON_PREFIX,
PERSON_SUFFIX,
PERSON_LASTSORT,
PERSON_FIRSTSORT,
PERSON_ORGANIZATION,
PERSON_DEPARTMENT,
PERSON_NOTE,
PERSON_BIRTHDAY,
PERSON_JOBTITLE,
PERSON_NICKNAME,
LAST_COL
};
const SQLiteSyncSource::Mapping *SQLiteContactSource::getConstMapping()
{
static const Mapping mapping[LAST_COL] = {
{ "Last", "ABPerson" },
{ "Middle", "ABPerson" },
{ "First", "ABPerson" },
{ "Prefix", "ABPerson" },
{ "Suffix", "ABPerson" },
{ "FirstSort", "ABPerson" },
{ "LastSort", "ABPerson" },
{ "Organization", "ABPerson" },
{ "Department", "ABPerson" },
{ "Note", "ABPerson", "NOTE" },
{ "Birthday", "ABPerson", "BIRTHDAY" },
{ "JobTitle", "ABPerson", "ROLE" },
{ "Nickname", "ABPerson", "NICKNAME" }
};
return mapping;
}
void SQLiteContactSource::open()
{
SQLiteSyncSource::open();
// query database for certain constant indices
m_addrCountryCode = findKey("ABMultiValueEntryKey", "value", "CountryCode");
m_addrCity = findKey("ABMultiValueEntryKey", "value", "City");
m_addrStreet = findKey("ABMultiValueEntryKey", "value", "Street");
m_addrState = findKey("ABMultiValueEntryKey", "value", "State");
m_addrZIP = findKey("ABMultiValueEntryKey", "value", "ZIP");
m_typeMobile = findKey("ABMultiValueLabel", "value", "_$!<Mobile>!$_");
m_typeHome = findKey("ABMultiValueLabel", "value", "_$!<Home>!$_");
m_typeWork = findKey("ABMultiValueLabel", "value", "_$!<Work>!$_");
}
void SQLiteContactSource::beginSyncThrow(bool needAll,
bool needPartial,
bool deleteLocal)
{
syncml_time_t lastSyncTime = anchorToTimestamp(getLastAnchor());
eptr<sqlite3_stmt> all(prepareSQL("SELECT ROWID, CreationDate, ModificationDate FROM ABPerson;"));
while (checkSQL(sqlite3_step(all)) == SQLITE_ROW) {
string uid = toString(SQLITE3_COLUMN_KEY(all, 0));
m_allItems.addItem(uid);
// find new and updated items by comparing their creation resp. modification time stamp
// against the end of the last sync
if (needPartial) {
syncml_time_t creationTime = getTimeColumn(all, 1);
syncml_time_t modTime = getTimeColumn(all, 2);
if (creationTime > lastSyncTime) {
m_newItems.addItem(uid);
} else if (modTime > lastSyncTime) {
m_updatedItems.addItem(uid);
}
}
}
// TODO: find deleted items
// all modifications from now on will be rolled back on an error:
// if the server does the same, theoretically client and server
// could restart with a two-way sync
//
// TODO: currently syncevolution resets the last anchor in case of
// a failure and thus forces a slow sync - avoid that for SQLite
// database sources
eptr<sqlite3_stmt> start(prepareSQL("BEGIN TRANSACTION;"));
checkSQL(sqlite3_step(start));
if (deleteLocal) {
for (itemList::const_iterator it = m_allItems.begin();
it != m_allItems.end();
it++) {
deleteItemThrow(*it);
}
m_allItems.clear();
}
}
void SQLiteContactSource::endSyncThrow()
{
// complete the transaction started in beginSyncThrow()
eptr<sqlite3_stmt> end(prepareSQL("COMMIT;"));
checkSQL(sqlite3_step(end));
}
void SQLiteContactSource::exportData(ostream &out)
{
eptr<sqlite3_stmt> all(prepareSQL("SELECT ROWID FROM ABPerson;"));
while (checkSQL(sqlite3_step(all)) == SQLITE_ROW) {
string uid = toString(SQLITE3_COLUMN_KEY(all, 1));
auto_ptr<SyncItem> item(createItem(uid, SYNC_STATE_NONE));
out << item->getData();
out << "\n";
}
}
SyncItem *SQLiteContactSource::createItem( const string &uid, SyncState state )
{
logItem(uid, "extracting from database");
eptr<sqlite3_stmt> contact(prepareSQL("SELECT * FROM ABPerson WHERE ROWID = '%s';", uid.c_str()));
if (checkSQL(sqlite3_step(contact)) != SQLITE_ROW) {
throw runtime_error(string(getName()) + ": contact not found: " + uid);
}
VObject vobj;
string tmp;
const unsigned char *text;
vobj.addProperty("BEGIN", "VCARD");
vobj.addProperty("VERSION", "2.1");
vobj.setVersion("2.1");
tmp = getTextColumn(contact, m_mapping[PERSON_LAST].colindex);
tmp += VObject::SEMICOLON_REPLACEMENT;
tmp += getTextColumn(contact, m_mapping[PERSON_MIDDLE].colindex);
tmp += VObject::SEMICOLON_REPLACEMENT;
tmp += getTextColumn(contact, m_mapping[PERSON_FIRST].colindex);
if (tmp.size() > 2) {
vobj.addProperty("N", tmp.c_str());
}
tmp = getTextColumn(contact, m_mapping[PERSON_ORGANIZATION].colindex);
tmp += VObject::SEMICOLON_REPLACEMENT;
tmp += getTextColumn(contact, m_mapping[PERSON_DEPARTMENT].colindex);
if (tmp.size() > 1) {
vobj.addProperty("ORG", tmp.c_str());
}
rowToVObject(contact, vobj);
vobj.addProperty("END", "VCARD");
vobj.fromNativeEncoding();
arrayptr<char> finalstr(vobj.toString(), "VOCL string");
LOG.debug("%s", (char *)finalstr);
auto_ptr<SyncItem> item( new SyncItem( uid.c_str() ) );
item->setData( (char *)finalstr, strlen(finalstr) );
item->setDataType( getMimeType() );
item->setModificationTime( 0 );
item->setState( state );
return item.release();
}
int SQLiteContactSource::addItemThrow(SyncItem& item)
{
return insertItemThrow(item, NULL, "");
}
int SQLiteContactSource::updateItemThrow(SyncItem& item)
{
// Make sure that there is no contact with this uid,
// then insert the new data. If there was no such uid,
// then this behaves like an add.
string creationTime = findColumn("ABPerson", "ROWID", item.getKey(), "CreationDate", "");
deleteItemThrow(item.getKey());
return insertItemThrow(item, item.getKey(), creationTime);
}
int SQLiteContactSource::insertItemThrow(SyncItem &item, const char *uid, const string &creationTime)
{
string data(getData(item));
std::auto_ptr<VObject> vobj(VConverter::parse((char *)data.c_str()));
if (vobj.get() == 0) {
throwError(string("parsing contact ") + item.getKey());
}
vobj->toNativeEncoding();
stringstream cols;
stringstream values;
VProperty *prop;
// always set the name, even if not in the vcard
prop = vobj->getProperty("N");
string first, middle, last, prefix, suffix, firstsort, lastsort;
if (prop && prop->getValue()) {
string fn = prop->getValue();
size_t sep1 = fn.find(VObject::SEMICOLON_REPLACEMENT);
size_t sep2 = sep1 == fn.npos ? fn.npos : fn.find(VObject::SEMICOLON_REPLACEMENT, sep1 + 1);
size_t sep3 = sep2 == fn.npos ? fn.npos : fn.find(VObject::SEMICOLON_REPLACEMENT, sep2 + 1);
size_t sep4 = sep3 == fn.npos ? fn.npos : fn.find(VObject::SEMICOLON_REPLACEMENT, sep3 + 1);
last = fn.substr(0, sep1);
if (sep1 != fn.npos) {
first = fn.substr(sep1 + 1, (sep2 == fn.npos) ? fn.npos : sep2 - sep1 - 1);
}
if (sep2 != fn.npos) {
middle = fn.substr(sep2 + 1, (sep3 == fn.npos) ? fn.npos : sep3 - sep2 - 1);
}
if (sep3 != fn.npos) {
prefix = fn.substr(sep3 + 1, (sep4 == fn.npos) ? fn.npos : sep4 - sep3 - 1);
}
if (sep4 != fn.npos) {
suffix = fn.substr(sep4 + 1);
}
}
cols << m_mapping[PERSON_FIRST].colname << ", " <<
m_mapping[PERSON_MIDDLE].colname << ", " <<
m_mapping[PERSON_LAST].colname << ", " <<
m_mapping[PERSON_LASTSORT].colname << ", " <<
m_mapping[PERSON_FIRSTSORT].colname;
values << "?, ?, ?, ?, ?";
// synthesize sort keys: upper case with specific order of first/last name
firstsort = first + " " + last;
transform(firstsort.begin(), firstsort.end(), firstsort.begin(), ::toupper);
lastsort = last + " " + first;
transform(lastsort.begin(), lastsort.end(), lastsort.begin(), ::toupper);
// optional fixed UID, potentially fixed creation time
if (uid) {
cols << ", ROWID";
values << ", ?";
}
cols << ", CreationDate, ModificationDate";
values << ", ?, ?";
eptr<sqlite3_stmt> insert(prepareSQL("INSERT INTO ABPerson( %s ) "
"VALUES( %s );",
cols.str().c_str(),
values.str().c_str()));
// now bind parameter values in the same order as the columns specification above
int param = 1;
checkSQL(sqlite3_bind_text(insert, param++, first.c_str(), -1, SQLITE_TRANSIENT));
checkSQL(sqlite3_bind_text(insert, param++, middle.c_str(), -1, SQLITE_TRANSIENT));
checkSQL(sqlite3_bind_text(insert, param++, last.c_str(), -1, SQLITE_TRANSIENT));
checkSQL(sqlite3_bind_text(insert, param++, lastsort.c_str(), -1, SQLITE_TRANSIENT));
checkSQL(sqlite3_bind_text(insert, param++, firstsort.c_str(), -1, SQLITE_TRANSIENT));
if (uid) {
checkSQL(sqlite3_bind_text(insert, param++, uid, -1, SQLITE_TRANSIENT));
checkSQL(sqlite3_bind_text(insert, param++, creationTime.c_str(), -1, SQLITE_TRANSIENT));
} else {
checkSQL(sqlite3_bind_int64(insert, param++, (long long)time(NULL)));
}
checkSQL(sqlite3_bind_int64(insert, param++, (long long)time(NULL)));
checkSQL(sqlite3_step(insert));
if (!uid) {
// figure out which UID was assigned to the new contact
string newuid = findColumn("SQLITE_SEQUENCE", "NAME", "ABPerson", "SEQ", "");
item.setKey(newuid.c_str());
}
return STC_OK;
}
int SQLiteContactSource::deleteItemThrow(const string &uid)
{
int status = STC_OK;
// delete address field members of contact
eptr<sqlite3_stmt> del(prepareSQL("DELETE FROM ABMultiValueEntry "
"WHERE ABMultiValueEntry.parent_id IN "
"(SELECT ABMultiValue.uid FROM ABMultiValue WHERE "
" ABMultiValue.record_id = ?);"));
checkSQL(sqlite3_bind_text(del, 1, uid.c_str(), -1, SQLITE_TRANSIENT));
checkSQL(sqlite3_step(del));
// delete addresses and emails of contact
del.set(prepareSQL("DELETE FROM ABMultiValue WHERE "
"ABMultiValue.record_id = ?;"));
checkSQL(sqlite3_bind_text(del, 1, uid.c_str(), -1, SQLITE_TRANSIENT));
checkSQL(sqlite3_step(del));
// now delete the contact itself
del.set(prepareSQL("DELETE FROM ABPerson WHERE "
"ABPerson.ROWID = ?;"));
checkSQL(sqlite3_bind_text(del, 1, uid.c_str(), -1, SQLITE_TRANSIENT));
checkSQL(sqlite3_step(del));
return status;
}
int SQLiteContactSource::deleteItemThrow(SyncItem& item)
{
return deleteItemThrow(item.getKey());
}
void SQLiteContactSource::logItem(const string &uid, const string &info, bool debug)
{
if (LOG.getLevel() >= (debug ? LOG_LEVEL_DEBUG : LOG_LEVEL_INFO)) {
(LOG.*(debug ? &Log::debug : &Log::info))("%s: %s %s",
getName(),
findColumn("ABPerson",
"ROWID",
uid.c_str(),
"FirstSort",
uid.c_str()).c_str(),
info.c_str());
}
}
void SQLiteContactSource::logItem(SyncItem &item, const string &info, bool debug)
{
if (LOG.getLevel() >= (debug ? LOG_LEVEL_DEBUG : LOG_LEVEL_INFO)) {
string data(getData(item));
// avoid pulling in a full vcard parser by just searching for a specific property,
// FN in this case
string name = "???";
string prop = "\nFN:";
size_t start = data.find(prop);
if (start != data.npos) {
start += prop.size();
size_t end = data.find("\n", start);
if (end != data.npos) {
name = data.substr(start, end - start);
}
}
(LOG.*(debug ? &Log::debug : &Log::info))("%s: %s %s",
getName(),
name.c_str(),
info.c_str());
}
}
#ifdef ENABLE_MODULES
extern "C" EvolutionSyncSource *SyncEvolutionCreateSource(const string &name,
SyncSourceConfig *sc,
const string &changeId,
const string &id,
const string &mimeType)
{
return new SQLiteContactSource(name, sc, changeId, id);
}
#endif /* ENABLE_MODULES */
#endif /* ENABLE_SQLITE */

89
src/SQLiteContactSource.h Normal file
View file

@ -0,0 +1,89 @@
/*
* Copyright (C) 2007 Patrick Ohly
* Copyright (C) 2007 Funambol
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef INCL_SQLITECONTACTSOURCE
#define INCL_SQLITECONTACTSOURCE
#include "SQLiteSyncSource.h"
#ifdef ENABLE_SQLITE
/**
* Specialization of SQLiteSyncSource for contacts
* with a schema as used by Mac OS X.
*/
class SQLiteContactSource : public SQLiteSyncSource
{
public:
SQLiteContactSource( const string name, SyncSourceConfig *sc, const string &changeId, const string &id ) :
SQLiteSyncSource( name, sc, changeId, id)
{}
virtual ~SQLiteContactSource() {}
protected:
/* implementation of EvolutionSyncSource interface */
virtual void open();
virtual SyncItem *createItem( const string &uid, SyncState state );
virtual void exportData(ostream &out);
virtual string fileSuffix() { return "vcf"; }
virtual const char *getMimeType() { return "text/x-vcard:2.1"; }
virtual const char *getMimeVersion() { return "2.1"; }
virtual const char *getSupportedTypes() { return "text/vcard:3.0,text/x-vcard:2.1"; }
virtual ArrayElement* clone() { new SQLiteContactSource(getName(), &getConfig(), m_changeId, m_id); }
virtual void beginSyncThrow(bool needAll,
bool needPartial,
bool deleteLocal);
virtual void endSyncThrow();
virtual int addItemThrow(SyncItem& item);
virtual int updateItemThrow(SyncItem& item);
virtual int deleteItemThrow(SyncItem& item);
virtual void logItem(const string &uid, const string &info, bool debug = false);
virtual void logItem(SyncItem &item, const string &info, bool debug = false);
/* implementation of SQLiteSyncSource interface */
virtual const char *getDefaultSchema();
virtual const Mapping *getConstMapping();
private:
/** constant key values defined by tables in the database, queried during open() */
key_t m_addrCountryCode,
m_addrCity,
m_addrStreet,
m_addrState,
m_addrZIP,
m_addrCountry,
m_typeMobile,
m_typeHome,
m_typeWork;
/** same as deleteItemThrow() but works with the uid directly */
virtual int deleteItemThrow(const string &uid);
/**
* inserts the contact under a specific UID (if given) or
* adds under a new UID
*/
virtual int insertItemThrow(SyncItem &item, const char *uid, const string &creationTime);
};
#endif // ENABLE_SQLITE
#endif // INCL_SQLITECONTACTSOURCE

203
src/SQLiteSyncSource.cpp Normal file
View file

@ -0,0 +1,203 @@
/*
* Copyright (C) 2007 Patrick Ohly
* Copyright (C) 2007 Funambol
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "config.h"
#ifdef ENABLE_SQLITE
#include "SQLiteSyncSource.h"
#include "base/util/StringBuffer.h"
#include "vocl/VConverter.h"
#include <stdarg.h>
void SQLiteSyncSource::throwError(const string &operation)
{
string descr = string(getName()) + ": '" + m_id + "': " + operation + " failed";
if (m_db) {
const char *error = sqlite3_errmsg(m_db);
descr += ": ";
descr += error ? error : "unspecified error";
}
throw runtime_error(descr);
}
sqlite3_stmt *SQLiteSyncSource::prepareSQLWrapper(const char *sql, const char **nextsql)
{
sqlite3_stmt *stmt = NULL;
checkSQL(sqlite3_prepare(m_db, sql, -1, &stmt, nextsql), sql);
return stmt;
}
sqlite3_stmt *SQLiteSyncSource::prepareSQL(const char *sqlfmt, ...)
{
StringBuffer sql;
va_list ap;
va_start(ap, sqlfmt);
sql.vsprintf(sqlfmt, ap);
va_end(ap);
return prepareSQLWrapper(sql.c_str());
}
SQLiteSyncSource::key_t SQLiteSyncSource::findKey(const char *database, const char *keyname, const char *key)
{
eptr<sqlite3_stmt> query(prepareSQL("SELECT ROWID FROM %s WHERE %s = '%s';", database, keyname, key));
int res = checkSQL(sqlite3_step(query), "getting key");
if (res == SQLITE_ROW) {
return sqlite3_column_int64(query, 0);
} else {
return -1;
}
}
string SQLiteSyncSource::findColumn(const char *database, const char *keyname, const char *key, const char *column, const char *def)
{
eptr<sqlite3_stmt> query(prepareSQL("SELECT %s FROM %s WHERE %s = '%s';", column, database, keyname, key));
int res = checkSQL(sqlite3_step(query), "getting key");
if (res == SQLITE_ROW) {
const unsigned char *text = sqlite3_column_text(query, 0);
return text ? (const char *)text : def;
} else {
return def;
}
}
string SQLiteSyncSource::getTextColumn(sqlite3_stmt *stmt, int col, const char *def)
{
const unsigned char *text = sqlite3_column_text(stmt, 0);
return text ? (const char *)text : def;
}
SQLiteSyncSource::syncml_time_t SQLiteSyncSource::getTimeColumn(sqlite3_stmt *stmt, int col)
{
// assumes that the database stores the result of time() directly
return sqlite3_column_int64(stmt, col);
}
void SQLiteSyncSource::rowToVObject(sqlite3_stmt *stmt, vocl::VObject &vobj)
{
const unsigned char *text;
const char *tablename;
int i;
for (i = 0; m_mapping[i].colname; i++) {
if (m_mapping[i].colindex < 0 ||
!m_mapping[i].propname) {
continue;
}
tablename = sqlite3_column_table_name(stmt, m_mapping[i].colindex);
if (!tablename || strcasecmp(tablename, m_mapping[i].tablename)) {
continue;
}
text = sqlite3_column_text(stmt, m_mapping[i].colindex);
if (text) {
vobj.addProperty(m_mapping[i].propname, (const char *)text);
}
}
}
void SQLiteSyncSource::open()
{
const string prefix("file://");
bool create = m_id.substr(0, prefix.size()) == prefix;
string filename = create ? m_id.substr(prefix.size()) : m_id;
if (!create && access(filename.c_str(), F_OK)) {
throw runtime_error(string(getName()) + ": no such database: '" + filename + "'");
}
sqlite3 *db;
int res = sqlite3_open(filename.c_str(), &db);
m_db = db;
checkSQL(res, "opening");
// check whether file is empty = newly created, define schema if that's the case
eptr<sqlite3_stmt> check(prepareSQLWrapper("SELECT * FROM sqlite_master;"));
switch (sqlite3_step(check)) {
case SQLITE_ROW:
// okay
break;
case SQLITE_DONE: {
// empty
const char *schema = getDefaultSchema();
const char *nextsql = schema;
while (nextsql && *nextsql) {
const char *sql = nextsql;
eptr<sqlite3_stmt> create(prepareSQLWrapper(sql, &nextsql));
while (TRUE) {
int res = sqlite3_step(create);
if (res == SQLITE_DONE) {
break;
} else if (res == SQLITE_ROW) {
// continue
} else {
throwError("creating database");\
}
}
}
break;
}
default:
throwError("checking content");
break;
}
// query database schema to find columns we need
const Mapping *mapping = getConstMapping();
int i;
for (i = 0; mapping[i].colname; i++) ;
m_mapping.set(new Mapping[i + 1]);
eptr<sqlite3_stmt> query;
string tablename;
for (i = 0; mapping[i].colname; i++) {
m_mapping[i] = mapping[i];
// switching to a different table?
if (tablename != m_mapping[i].tablename) {
tablename = m_mapping[i].tablename;
query.set(prepareSQL("SELECT * FROM %s;", tablename.c_str()));
}
// search for this column name
for (m_mapping[i].colindex = sqlite3_column_count(query) - 1;
m_mapping[i].colindex >= 0;
m_mapping[i].colindex--) {
const char *name = sqlite3_column_name(query, m_mapping[i].colindex);
if (name && !strcasecmp(m_mapping[i].colname, name)) {
break;
}
}
}
memset(&m_mapping[i], 0, sizeof(m_mapping[i]));
}
void SQLiteSyncSource::close()
{
}
#endif /* ENABLE_SQLITE */

142
src/SQLiteSyncSource.h Normal file
View file

@ -0,0 +1,142 @@
/*
* Copyright (C) 2007 Patrick Ohly
* Copyright (C) 2007 Funambol
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef INCL_SQLITESYNCSOURCE
#define INCL_SQLITESYNCSOURCE
#include "EvolutionSyncSource.h"
#ifdef ENABLE_SQLITE
#include <sqlite3.h>
#include "EvolutionSmartPtr.h"
namespace vocl {
class VObject;
}
void inline unref(sqlite3 *db) { sqlite3_close(db); }
void inline unref(sqlite3_stmt *stmt) { sqlite3_finalize(stmt); }
/**
* This class implements access to SQLite database files:
* - opening the database file
* - error reporting
* - creating a database file in debugging mode
*/
class SQLiteSyncSource : public EvolutionSyncSource
{
public:
/**
* Creates a new Evolution sync source.
*
* @param name the named needed by SyncSource
* @param sc obligatory config for this source, must remain valid throughout the lifetime of the source
* @param changeId is used to track changes in the Evolution backend
* @param id identifies the backend; not specifying it makes this instance
* unusable for anything but listing backend databases
*/
SQLiteSyncSource( const string name, SyncSourceConfig *sc, const string &changeId, const string &id ) :
EvolutionSyncSource( name, sc, changeId, id),
m_db(NULL)
{}
virtual ~SQLiteSyncSource() {}
/* implementation of EvolutionSyncSource interface */
virtual sources getSyncBackends() { return sources(); /* we cannot list available databases */ }
virtual void open();
virtual void close();
protected:
/** throw error for a specific sqlite3 operation on m_db */
void throwError(const string &operation);
/**
* wrapper around sqlite3_prepare() which operates on the current
* database and throws an error if the call fails
*
* @param sqlfmt printf-style format string for query, followed by parameters for sprintf
*/
sqlite3_stmt *prepareSQL(const char *sqlfmt, ...);
/**
* wrapper around sqlite3_prepare() which operates on the current
* database and throws an error if the call fails
*
* @param sql preformatted SQL statement(s)
* @param nextsql pointer to next statement in sql
*/
sqlite3_stmt *prepareSQLWrapper(const char *sql, const char **nextsql = NULL);
/** checks the result of an sqlite3 call, throws an error if faulty, otherwise returns the result */
int checkSQL(int res, const char *operation = "SQLite call") {
if (res != SQLITE_OK && res != SQLITE_ROW && res != SQLITE_DONE) {
throwError(operation);
}
return res;
}
/** type used for row keys */
typedef long long key_t;
string toString(key_t key) { char buffer[32]; sprintf(buffer, "%lld", key); return buffer; }
#define SQLITE3_COLUMN_KEY sqlite3_column_int64
/** return row ID for a certain row */
key_t findKey(const char *database, const char *keyname, const char *key);
/** return a specific column for a row identified by a certain key column as text, returns default text if not found */
string findColumn(const char *database, const char *keyname, const char *key, const char *column, const char *def);
/** a wrapper for sqlite3_column_test() which will check for NULL and returns default text instead */
string getTextColumn(sqlite3_stmt *stmt, int col, const char *def = "");
typedef unsigned long syncml_time_t;
/** transform column to same time base as used by SyncML libary (typically time()) */
syncml_time_t getTimeColumn(sqlite3_stmt *stmt, int col);
/** copies all columns which directly map to a property into the vobj */
void rowToVObject(sqlite3_stmt *stmt, vocl::VObject &vobj);
/** database schema to use when creating new databases, may be NULL */
virtual const char *getDefaultSchema() = 0;
/** information about the database mapping */
struct Mapping {
const char *colname; /**< column name in SQL table */
const char *tablename; /**< name of the SQL table which has this column */
const char *propname; /**< optional: vcard/vcalendar property which corresponds to this */
int colindex; /**< determined dynamically in open(): index of the column, -1 if not present */
};
/**
* array with database mapping, terminated by NULL colname:
* variable fields are stored in a copy maintained by the SQLiteSyncSource class
*/
virtual const Mapping *getConstMapping() = 0;
/** filled in by SQLiteSyncSource::open() */
arrayptr<Mapping> m_mapping;
/** after opening: current databse */
eptr<sqlite3> m_db;
};
#endif // ENABLE_SQLITE
#endif // INCL_SQLITESYNCSOURCE

View file

@ -31,6 +31,7 @@
#include "EvolutionCalendarSource.h"
#include "EvolutionMemoSource.h"
#include "EvolutionContactSource.h"
#include "SQLiteContactSource.h"
/** a wrapper class which automatically does an open() in the constructor and a close() in the destructor */
template<class T> class TestEvolutionSyncSource : public T {
@ -132,7 +133,7 @@ public:
/* check sources */
const char *sourcelist = getenv("CLIENT_TEST_SOURCES");
if (!sourcelist) {
sourcelist = "vcard21,vcard30,ical20,text,itodo20";
sourcelist = "vcard21,vcard30,ical20,text,itodo20,sqlite";
}
numSources = 0;
for (SourceType sourceType = (SourceType)0; sourceType < TEST_MAX_SOURCE; sourceType = (SourceType)((int)sourceType + 1) ) {
@ -149,6 +150,11 @@ public:
sourceType == TEST_MEMO_SOURCE) {
continue;
}
#endif
#ifndef ENABLE_SQLITE
if (sourceType == TEST_SQLITE_CONTACT_SOURCE) {
continue;
}
#endif
if (strstr(sourcelist, name.c_str())) {
enabledSources[numSources++] = sourceType;
@ -212,6 +218,7 @@ public:
TEST_CALENDAR_SOURCE,
TEST_TASK_SOURCE,
TEST_MEMO_SOURCE,
TEST_SQLITE_CONTACT_SOURCE,
TEST_MAX_SOURCE
};
@ -286,6 +293,11 @@ public:
config.dump = dumpMemoSource;
config.testcases = "testcases/imemo20.ics";
break;
case TEST_SQLITE_CONTACT_SOURCE:
getTestData("vcard21", config);
config.sourceName = "sqlite";
config.type = "sqlite";
break;
default:
CPPUNIT_ASSERT(sourceType < TEST_MAX_SOURCE);
break;
@ -407,6 +419,9 @@ private:
case TEST_MEMO_SOURCE:
return "text";
break;
case TEST_SQLITE_CONTACT_SOURCE:
return "sqlite";
break;
default:
CPPUNIT_ASSERT(type >= 0 && type < TEST_MAX_SOURCE);
break;
@ -423,41 +438,54 @@ private:
SourceType type = ((TestEvolution &)client).enabledSources[source];
changeID += isSourceA ? "1" : "2";
string database = ((TestEvolution &)client).getDatabaseName(type);
SyncSource *ss = NULL;
switch (type) {
case TEST_CONTACT21_SOURCE:
case TEST_CONTACT30_SOURCE:
#ifdef ENABLE_EBOOK
return new TestEvolutionSyncSource<EvolutionContactSource>(changeID, database);
#else
return NULL;
ss = new TestEvolutionSyncSource<EvolutionContactSource>(changeID, database);
#endif
break;
case TEST_CALENDAR_SOURCE:
#ifdef ENABLE_ECAL
return new TestEvolutionSyncSource<EvolutionCalendarSource>(E_CAL_SOURCE_TYPE_EVENT, changeID, database);
#else
return NULL;
ss = new TestEvolutionSyncSource<EvolutionCalendarSource>(E_CAL_SOURCE_TYPE_EVENT, changeID, database);
#endif
break;
case TEST_TASK_SOURCE:
#ifdef ENABLE_ECAL
return new TestEvolutionSyncSource<EvolutionCalendarSource>(E_CAL_SOURCE_TYPE_TODO, changeID, database);
#else
return NULL;
ss = new TestEvolutionSyncSource<EvolutionCalendarSource>(E_CAL_SOURCE_TYPE_TODO, changeID, database);
#endif
break;
case TEST_MEMO_SOURCE:
#ifdef ENABLE_ECAL
return new TestEvolutionSyncSource<EvolutionMemoSource>(E_CAL_SOURCE_TYPE_JOURNAL, changeID, database);
#else
return NULL;
ss = new TestEvolutionSyncSource<EvolutionMemoSource>(E_CAL_SOURCE_TYPE_JOURNAL, changeID, database);
#endif
break;
case TEST_SQLITE_CONTACT_SOURCE:
#ifdef ENABLE_SQLITE
ss = new TestEvolutionSyncSource<SQLiteContactSource>(changeID, database);
// this is a hack: it guesses the last sync time stamp by remembering
// the last time the sync source was created
static time_t lastts[TEST_MAX_SOURCE];
char anchor[DIM_ANCHOR];
time_t nextts;
timestampToAnchor(lastts[type], anchor);
ss->setLastAnchor(anchor);
nextts = time(NULL);
while (lastts[type] == nextts) {
sleep(1);
nextts = time(NULL);
}
lastts[type] = nextts;
#endif
default:
CPPUNIT_ASSERT(type >= 0 && type < TEST_MAX_SOURCE);
return NULL;
}
return ss;
}
/**
@ -480,7 +508,7 @@ static class RegisterTestEvolution {
public:
RegisterTestEvolution() :
testClient("1") {
#ifdef HAVE_GLIB
#if defined(HAVE_GLIB) && defined(HAVE_EDS)
// this is required on Maemo and does not harm either on a normal
// desktop system with Evolution
g_type_init();

View file

@ -87,7 +87,7 @@ int main( int argc, char **argv )
setenv("DBUS_DEFAULT_TIMEOUT", "600000", 0);
#endif
#ifdef HAVE_GLIB
#if defined(HAVE_GLIB) && defined(HAVE_EDS)
// this is required on Maemo and does not harm either on a normal
// desktop system with Evolution
g_type_init();

View file

@ -227,8 +227,6 @@ static int hex2int( wchar_t x )
0;
}
#define SEMICOLON_REPLACEMENT '\a'
void VObject::toNativeEncoding()
{
bool is_30 = !wcscmp(getVersion(), TEXT("3.0"));

View file

@ -23,6 +23,7 @@ public:
char* getVersion();
char* getProdID();
void addProperty(VProperty* property);
void addProperty(const char *name, const char *value) { VProperty vprop((char *)name, (char *)value); addProperty(&vprop); }
void addFirstProperty(VProperty* property);
void insertProperty(VProperty* property);
bool removeProperty(int index);
@ -77,6 +78,9 @@ public:
// object.
//
void fromNativeEncoding();
/** in native property values this special character separates sub-values in multi-value properties */
static const char SEMICOLON_REPLACEMENT = '\a';
};
};