summaryrefslogtreecommitdiff
path: root/src/addressbook/libedata-book
diff options
context:
space:
mode:
authorMilan Crha <mcrha@redhat.com>2016-10-11 11:47:14 +0200
committerMilan Crha <mcrha@redhat.com>2016-10-11 11:47:14 +0200
commitd7931c6dd9db1e090f4bb466983c3dced19e2201 (patch)
tree31e31eef195355e800c63be6b4dcfefe6e37bb84 /src/addressbook/libedata-book
parent4febe3ae82e850ca9f17229dd2dbd9cdd8708a8f (diff)
downloadevolution-data-server-d7931c6dd9db1e090f4bb466983c3dced19e2201.tar.gz
Reorganize directory structure
Let's have it as it's common to be, which means top level src/ for sources, single data/ for data, and so on.
Diffstat (limited to 'src/addressbook/libedata-book')
-rw-r--r--src/addressbook/libedata-book/CMakeLists.txt199
-rw-r--r--src/addressbook/libedata-book/TODO2
-rw-r--r--src/addressbook/libedata-book/dbus.dtd57
-rw-r--r--src/addressbook/libedata-book/e-book-backend-cache.c332
-rw-r--r--src/addressbook/libedata-book/e-book-backend-cache.h115
-rw-r--r--src/addressbook/libedata-book/e-book-backend-db-cache.c512
-rw-r--r--src/addressbook/libedata-book/e-book-backend-db-cache.h70
-rw-r--r--src/addressbook/libedata-book/e-book-backend-factory.c113
-rw-r--r--src/addressbook/libedata-book/e-book-backend-factory.h93
-rw-r--r--src/addressbook/libedata-book/e-book-backend-sexp.c1261
-rw-r--r--src/addressbook/libedata-book/e-book-backend-sexp.h90
-rw-r--r--src/addressbook/libedata-book/e-book-backend-sqlitedb-test.c211
-rw-r--r--src/addressbook/libedata-book/e-book-backend-sqlitedb.c6632
-rw-r--r--src/addressbook/libedata-book/e-book-backend-sqlitedb.h435
-rw-r--r--src/addressbook/libedata-book/e-book-backend-summary.c1347
-rw-r--r--src/addressbook/libedata-book/e-book-backend-summary.h123
-rw-r--r--src/addressbook/libedata-book/e-book-backend.c3642
-rw-r--r--src/addressbook/libedata-book/e-book-backend.h481
-rw-r--r--src/addressbook/libedata-book/e-book-sqlite.c8543
-rw-r--r--src/addressbook/libedata-book/e-book-sqlite.h481
-rw-r--r--src/addressbook/libedata-book/e-data-book-cursor-sqlite.c568
-rw-r--r--src/addressbook/libedata-book/e-data-book-cursor-sqlite.h78
-rw-r--r--src/addressbook/libedata-book/e-data-book-cursor.c1216
-rw-r--r--src/addressbook/libedata-book/e-data-book-cursor.h318
-rw-r--r--src/addressbook/libedata-book/e-data-book-direct.c144
-rw-r--r--src/addressbook/libedata-book/e-data-book-direct.h63
-rw-r--r--src/addressbook/libedata-book/e-data-book-factory.c200
-rw-r--r--src/addressbook/libedata-book/e-data-book-factory.h85
-rw-r--r--src/addressbook/libedata-book/e-data-book-view.c1148
-rw-r--r--src/addressbook/libedata-book/e-data-book-view.h113
-rw-r--r--src/addressbook/libedata-book/e-data-book-view.xml44
-rw-r--r--src/addressbook/libedata-book/e-data-book.c2375
-rw-r--r--src/addressbook/libedata-book/e-data-book.h146
-rw-r--r--src/addressbook/libedata-book/e-subprocess-book-factory.c418
-rw-r--r--src/addressbook/libedata-book/e-subprocess-book-factory.h68
-rw-r--r--src/addressbook/libedata-book/evolution-addressbook-factory-subprocess.c227
-rw-r--r--src/addressbook/libedata-book/evolutionperson.schema212
-rw-r--r--src/addressbook/libedata-book/libedata-book.h44
-rw-r--r--src/addressbook/libedata-book/libedata-book.pc.in18
-rw-r--r--src/addressbook/libedata-book/ximian-vcard.h80
40 files changed, 32304 insertions, 0 deletions
diff --git a/src/addressbook/libedata-book/CMakeLists.txt b/src/addressbook/libedata-book/CMakeLists.txt
new file mode 100644
index 000000000..a247c7b11
--- /dev/null
+++ b/src/addressbook/libedata-book/CMakeLists.txt
@@ -0,0 +1,199 @@
+add_pkgconfig_file(libedata-book.pc.in libedata-book-${API_VERSION}.pc)
+
+set(DEPENDENCIES
+ camel
+ ebackend
+ ebook-contacts
+ edbus-private
+ edataserver
+ egdbus-book
+)
+
+set(SOURCES
+ e-book-backend-factory.c
+ e-book-backend-sexp.c
+ e-book-backend-summary.c
+ e-book-backend-cache.c
+ e-book-backend-sqlitedb.c
+ e-book-backend.c
+ e-book-sqlite.c
+ e-data-book.c
+ e-data-book-cursor.c
+ e-data-book-cursor-sqlite.c
+ e-data-book-direct.c
+ e-data-book-factory.c
+ e-data-book-view.c
+ e-subprocess-book-factory.c
+ ximian-vcard.h
+)
+
+set(HEADERS
+ libedata-book.h
+ e-book-backend-factory.h
+ e-book-backend-sexp.h
+ e-book-backend-summary.h
+ e-book-backend.h
+ e-data-book-factory.h
+ e-data-book-view.h
+ e-data-book.h
+ e-data-book-cursor.h
+ e-data-book-cursor-sqlite.h
+ e-data-book-direct.h
+ e-book-backend-cache.h
+ e-book-backend-sqlitedb.h
+ e-book-sqlite.h
+ e-subprocess-book-factory.h
+)
+
+if(WITH_LIBDB)
+ list(APPEND SOURCES
+ e-book-backend-db-cache.c
+ )
+
+ list(APPEND HEADERS
+ e-book-backend-db-cache.h
+ )
+endif(WITH_LIBDB)
+
+add_library(edata-book SHARED
+ ${SOURCES}
+ ${HEADERS}
+)
+
+add_dependencies(edata-book
+ ${DEPENDENCIES}
+)
+
+set_target_properties(edata-book PROPERTIES
+ VERSION "${LIBEDATABOOK_CURRENT}.${LIBEDATABOOK_REVISION}.${LIBEDATABOOK_AGE}"
+ SOVERSION ${LIBEDATABOOK_CURRENT}
+ OUTPUT_NAME edata-book-${API_VERSION}
+)
+
+target_compile_definitions(edata-book PRIVATE
+ -DG_LOG_DOMAIN=\"libedata-book\"
+ -DBACKENDDIR=\"${ebook_backenddir}\"
+ -DSUBPROCESS_BOOK_BACKEND_PATH=\"${LIBEXEC_INSTALL_DIR}/evolution-addressbook-factory-subprocess\"
+ -DLIBEDATA_BOOK_COMPILATION
+)
+
+target_compile_options(edata-book PUBLIC
+ ${ADDRESSBOOK_CFLAGS}
+ ${LIBDB_CFLAGS}
+ ${SQLITE3_CFLAGS}
+)
+
+target_include_directories(edata-book PUBLIC
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_BINARY_DIR}/src
+ ${CMAKE_BINARY_DIR}/src/addressbook
+ ${CMAKE_BINARY_DIR}/src/addressbook/libegdbus
+ ${CMAKE_BINARY_DIR}/src/private
+ ${CMAKE_SOURCE_DIR}/src
+ ${CMAKE_SOURCE_DIR}/src/private
+ ${CMAKE_SOURCE_DIR}/src/addressbook
+ ${CMAKE_SOURCE_DIR}/src/addressbook/libegdbus
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${ADDRESSBOOK_INCLUDE_DIRS}
+ ${LIBDB_INCLUDE_DIRS}
+ ${SQLITE3_INCLUDE_DIRS}
+)
+
+target_link_libraries(edata-book
+ ${DEPENDENCIES}
+ ${ADDRESSBOOK_LDFLAGS}
+ ${LIBDB_LIBS}
+ ${SQLITE3_LDFLAGS}
+)
+
+install(TARGETS edata-book
+ DESTINATION ${LIB_INSTALL_DIR}
+)
+
+install(FILES ${HEADERS}
+ DESTINATION ${privincludedir}/libedata-book
+)
+
+add_executable(e-book-backend-sqlitedb-test EXCLUDE_FROM_ALL e-book-backend-sqlitedb-test.c)
+
+target_compile_definitions(e-book-backend-sqlitedb-test PRIVATE
+ -DG_LOG_DOMAIN=\"libedata-book\"
+ -DBACKENDDIR=\"${ebook_backenddir}\"
+ -DSUBPROCESS_BOOK_BACKEND_PATH=\"${LIBEXEC_INSTALL_DIR}/evolution-addressbook-factory-subprocess\"
+ -DLIBEDATA_BOOK_COMPILATION
+)
+
+target_compile_options(e-book-backend-sqlitedb-test PUBLIC
+ ${ADDRESSBOOK_CFLAGS}
+ ${LIBDB_CFLAGS}
+ ${SQLITE3_CFLAGS}
+)
+
+target_include_directories(e-book-backend-sqlitedb-test PUBLIC
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_BINARY_DIR}/src
+ ${CMAKE_BINARY_DIR}/src/addressbook
+ ${CMAKE_BINARY_DIR}/src/addressbook/libegdbus
+ ${CMAKE_BINARY_DIR}/src/private
+ ${CMAKE_SOURCE_DIR}/src
+ ${CMAKE_SOURCE_DIR}/src/private
+ ${CMAKE_SOURCE_DIR}/src/addressbook
+ ${CMAKE_SOURCE_DIR}/src/addressbook/libegdbus
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${ADDRESSBOOK_INCLUDE_DIRS}
+ ${LIBDB_INCLUDE_DIRS}
+ ${SQLITE3_INCLUDE_DIRS}
+)
+
+target_link_libraries(e-book-backend-sqlitedb-test
+ edata-book
+ ${DEPENDENCIES}
+ ${ADDRESSBOOK_LDFLAGS}
+ ${LIBDB_LIBS}
+ ${SQLITE3_LDFLAGS}
+)
+
+set(DEPENDENCIES
+ ebackend
+ edataserver
+ edata-book
+ edbus-private
+)
+
+add_executable(evolution-addressbook-factory-subprocess
+ evolution-addressbook-factory-subprocess.c)
+
+target_compile_definitions(evolution-addressbook-factory-subprocess PRIVATE
+ -DG_LOG_DOMAIN=\"evolution-addressbook-factory-subprocess\"
+ -DLOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
+)
+
+target_compile_options(evolution-addressbook-factory-subprocess PUBLIC
+ ${ADDRESSBOOK_CFLAGS}
+ ${GTK_CFLAGS}
+)
+
+target_include_directories(evolution-addressbook-factory-subprocess PUBLIC
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_BINARY_DIR}/src
+ ${CMAKE_BINARY_DIR}/src/addressbook
+ ${CMAKE_BINARY_DIR}/src/addressbook/libegdbus
+ ${CMAKE_BINARY_DIR}/src/private
+ ${CMAKE_SOURCE_DIR}/src
+ ${CMAKE_SOURCE_DIR}/src/private
+ ${CMAKE_SOURCE_DIR}/src/addressbook
+ ${CMAKE_SOURCE_DIR}/src/addressbook/libegdbus
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${ADDRESSBOOK_INCLUDE_DIRS}
+ ${GTK_INCLUDE_DIRS}
+)
+
+target_link_libraries(evolution-addressbook-factory-subprocess
+ ${DEPENDENCIES}
+ ${ADDRESSBOOK_LDFLAGS}
+ ${GTK_LDFLAGS}
+)
+
+install(TARGETS evolution-addressbook-factory-subprocess
+ DESTINATION ${LIBEXEC_INSTALL_DIR}
+)
diff --git a/src/addressbook/libedata-book/TODO b/src/addressbook/libedata-book/TODO
new file mode 100644
index 000000000..0c77c1b20
--- /dev/null
+++ b/src/addressbook/libedata-book/TODO
@@ -0,0 +1,2 @@
+* Implement pas_book_factory_activate
+* Authentication \ No newline at end of file
diff --git a/src/addressbook/libedata-book/dbus.dtd b/src/addressbook/libedata-book/dbus.dtd
new file mode 100644
index 000000000..6ffdb22fe
--- /dev/null
+++ b/src/addressbook/libedata-book/dbus.dtd
@@ -0,0 +1,57 @@
+<?xml version ="1.0" ?>
+
+<!ENTITY % name.attr
+ "name CDATA #REQUIRED">
+
+<!ELEMENT node (interface+)>
+<!ATTLIST node
+ %name.attr;
+>
+
+<!ELEMENT interface (annotation?, (method|signal|property)*)>
+<!ATTLIST interface
+ %name.attr;
+>
+
+<!ELEMENT annotation EMPTY>
+<!ATTLIST annotation
+ name (org.freedesktop.DBus.GLib.CSymbol|org.freedesktop.DBus.Deprecated) #REQUIRED
+ value CDATA #REQUIRED
+>
+
+<!ELEMENT method (annotation?, arg*)>
+<!ATTLIST method
+ %name.attr;
+>
+
+<!ELEMENT signal (arg*)>
+<!ATTLIST signal
+ %name.attr;
+>
+
+<!--
+The types:
+byte: y
+boolean: b
+int16: n
+uint16: q
+int32: i
+unit32: u
+int64: x
+uint64: t
+double: d
+string: s
+object path: o
+signature: g
+array: a
+variant: v
+struct: r
+dict entry: e
+-->
+
+<!ELEMENT arg EMPTY>
+<!ATTLIST arg
+ %name.attr;
+ type (y|b|n|q|i|u|x|t|d|s|o|g|a|v|r|e) #REQUIRED
+ direction (in|out) #IMPLIED
+>
diff --git a/src/addressbook/libedata-book/e-book-backend-cache.c b/src/addressbook/libedata-book/e-book-backend-cache.c
new file mode 100644
index 000000000..08cd62313
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-cache.c
@@ -0,0 +1,332 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* A class to cache address book conents on local file system
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Sivaiah Nallagatla <snallagatla@novell.com>
+ */
+
+/**
+ * SECTION: e-book-backend-cache
+ * @include: libedata-book/libedata-book.h
+ * @short_description: A utility for storing contact data and searching for contacts
+ *
+ * The #EBookBackendCache is deprecated, use #EBookSqlite instead.
+ */
+#include "evolution-data-server-config.h"
+
+#include <string.h>
+
+#include "e-book-backend-cache.h"
+#include "e-book-backend-sexp.h"
+
+#define E_BOOK_BACKEND_CACHE_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_BOOK_BACKEND_CACHE, EBookBackendCachePrivate))
+
+struct _EBookBackendCachePrivate {
+ gint placeholder;
+};
+
+G_DEFINE_TYPE (EBookBackendCache, e_book_backend_cache, E_TYPE_FILE_CACHE)
+
+static void
+e_book_backend_cache_class_init (EBookBackendCacheClass *class)
+{
+ g_type_class_add_private (class, sizeof (EBookBackendCachePrivate));
+}
+
+static void
+e_book_backend_cache_init (EBookBackendCache *cache)
+{
+ cache->priv = E_BOOK_BACKEND_CACHE_GET_PRIVATE (cache);
+}
+
+/**
+ * e_book_backend_cache_new
+ * @filename: file to write cached data
+ *
+ * Creates a new #EBookBackendCache, which implements a local cache of
+ * #EContact objects, useful for remote backends.
+ *
+ * Returns: a new #EBookBackendCache
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+EBookBackendCache *
+e_book_backend_cache_new (const gchar *filename)
+{
+ g_return_val_if_fail (filename != NULL, NULL);
+
+ return g_object_new (
+ E_TYPE_BOOK_BACKEND_CACHE,
+ "filename", filename, NULL);
+}
+
+/**
+ * e_book_backend_cache_get_contact:
+ * @cache: an #EBookBackendCache
+ * @uid: a unique contact ID
+ *
+ * Get a cached contact. Note that the returned #EContact will be
+ * newly created, and must be unreffed by the caller when no longer
+ * needed.
+ *
+ * Returns: A cached #EContact, or %NULL if @uid is not cached.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+EContact *
+e_book_backend_cache_get_contact (EBookBackendCache *cache,
+ const gchar *uid)
+{
+ const gchar *vcard_str;
+ EContact *contact = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_CACHE (cache), NULL);
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ vcard_str = e_file_cache_get_object (E_FILE_CACHE (cache), uid);
+ if (vcard_str) {
+ contact = e_contact_new_from_vcard_with_uid (vcard_str, uid);
+
+ }
+
+ return contact;
+}
+
+/**
+ * e_book_backend_cache_add_contact:
+ * @cache: an #EBookBackendCache
+ * @contact: an #EContact
+ *
+ * Adds @contact to @cache.
+ *
+ * Returns: %TRUE if the contact was cached successfully, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_cache_add_contact (EBookBackendCache *cache,
+ EContact *contact)
+{
+ gchar *vcard_str;
+ const gchar *uid;
+ gboolean retval;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_CACHE (cache), FALSE);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ vcard_str = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+ if (e_file_cache_get_object (E_FILE_CACHE (cache), uid))
+ retval = e_file_cache_replace_object (E_FILE_CACHE (cache), uid, vcard_str);
+ else
+ retval = e_file_cache_add_object (E_FILE_CACHE (cache), uid, vcard_str);
+
+ g_free (vcard_str);
+
+ return retval;
+}
+
+/**
+ * e_book_backend_cache_remove_contact:
+ * @cache: an #EBookBackendCache
+ * @uid: a unique contact ID
+ *
+ * Removes the contact identified by @uid from @cache.
+ *
+ * Returns: %TRUE if the contact was found and removed, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_cache_remove_contact (EBookBackendCache *cache,
+ const gchar *uid)
+
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (!e_file_cache_get_object (E_FILE_CACHE (cache), uid))
+ return FALSE;
+
+ return e_file_cache_remove_object (E_FILE_CACHE (cache), uid);
+}
+
+/**
+ * e_book_backend_cache_check_contact:
+ * @cache: an #EBookBackendCache
+ * @uid: a unique contact ID
+ *
+ * Checks if the contact identified by @uid exists in @cache.
+ *
+ * Returns: %TRUE if the cache contains the contact, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_cache_check_contact (EBookBackendCache *cache,
+ const gchar *uid)
+{
+
+ gboolean retval;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ retval = FALSE;
+ if (e_file_cache_get_object (E_FILE_CACHE (cache), uid))
+ retval = TRUE;
+ return retval;
+}
+
+/**
+ * e_book_backend_cache_get_contacts:
+ * @cache: an #EBookBackendCache
+ * @query: an s-expression
+ *
+ * Returns a list of #EContact elements from @cache matching @query.
+ * When done with the list, the caller must unref the contacts and
+ * free the list.
+ *
+ * Returns: A #GList of pointers to #EContact.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+GList *
+e_book_backend_cache_get_contacts (EBookBackendCache *cache,
+ const gchar *query)
+{
+ gchar *vcard_str;
+ GSList *l, *lcache;
+ GList *list = NULL;
+ EContact *contact;
+ EBookBackendSExp *sexp = NULL;
+ const gchar *uid;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_CACHE (cache), NULL);
+ if (query) {
+ sexp = e_book_backend_sexp_new (query);
+ if (!sexp)
+ return NULL;
+ }
+
+ lcache = l = e_file_cache_get_objects (E_FILE_CACHE (cache));
+
+ for (; l != NULL; l = g_slist_next (l)) {
+ vcard_str = l->data;
+ if (vcard_str && !strncmp (vcard_str, "BEGIN:VCARD", 11)) {
+ contact = e_contact_new_from_vcard (vcard_str);
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ if (uid && *uid && (!query || e_book_backend_sexp_match_contact (sexp, contact)))
+ list = g_list_prepend (list, contact);
+ else
+ g_object_unref (contact);
+ }
+
+ }
+ if (lcache)
+ g_slist_free (lcache);
+ if (sexp)
+ g_object_unref (sexp);
+
+ return g_list_reverse (list);
+}
+
+/**
+ * e_book_backend_cache_search:
+ * @cache: an #EBookBackendCache
+ * @query: an s-expression
+ *
+ * Returns an array of pointers to unique contact ID strings for contacts
+ * in @cache matching @query. When done with the array, the caller must
+ * free the ID strings and the array.
+ *
+ * Returns: A #GPtrArray of pointers to contact ID strings.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+GPtrArray *
+e_book_backend_cache_search (EBookBackendCache *cache,
+ const gchar *query)
+{
+ GList *matching_contacts, *temp;
+ GPtrArray *ptr_array;
+
+ matching_contacts = e_book_backend_cache_get_contacts (cache, query);
+ ptr_array = g_ptr_array_new ();
+
+ temp = matching_contacts;
+ for (; matching_contacts != NULL; matching_contacts = g_list_next (matching_contacts)) {
+ g_ptr_array_add (ptr_array, e_contact_get (matching_contacts->data, E_CONTACT_UID));
+ g_object_unref (matching_contacts->data);
+ }
+ g_list_free (temp);
+
+ return ptr_array;
+}
+
+/**
+ * e_book_backend_cache_set_populated:
+ * @cache: an #EBookBackendCache
+ *
+ * Flags @cache as being populated - that is, it is up-to-date on the
+ * contents of the book it's caching.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+void
+e_book_backend_cache_set_populated (EBookBackendCache *cache)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND_CACHE (cache));
+ e_file_cache_add_object (E_FILE_CACHE (cache), "populated", "TRUE");
+}
+
+/**
+ * e_book_backend_cache_is_populated:
+ * @cache: an #EBookBackendCache
+ *
+ * Checks if @cache is populated.
+ *
+ * Returns: %TRUE if @cache is populated, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_cache_is_populated (EBookBackendCache *cache)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_CACHE (cache), FALSE);
+ if (e_file_cache_get_object (E_FILE_CACHE (cache), "populated"))
+ return TRUE;
+ return FALSE;
+}
+
+void
+e_book_backend_cache_set_time (EBookBackendCache *cache,
+ const gchar *t)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND_CACHE (cache));
+ if (!e_file_cache_add_object (E_FILE_CACHE (cache), "last_update_time", t))
+ e_file_cache_replace_object (E_FILE_CACHE (cache), "last_update_time", t);
+}
+
+gchar *
+e_book_backend_cache_get_time (EBookBackendCache *cache)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_CACHE (cache), NULL);
+ return g_strdup (e_file_cache_get_object (E_FILE_CACHE (cache), "last_update_time"));
+}
+
diff --git a/src/addressbook/libedata-book/e-book-backend-cache.h b/src/addressbook/libedata-book/e-book-backend-cache.h
new file mode 100644
index 000000000..0db46df49
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-cache.h
@@ -0,0 +1,115 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * A class to cache address book conents on local file system
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Sivaiah Nallagatla <snallagatla@ximian.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_BACKEND_CACHE_H
+#define E_BOOK_BACKEND_CACHE_H
+
+#ifndef EDS_DISABLE_DEPRECATED
+
+#include <libebook-contacts/libebook-contacts.h>
+#include <libebackend/libebackend.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_BACKEND_CACHE \
+ (e_book_backend_cache_get_type ())
+#define E_BOOK_BACKEND_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_BACKEND_CACHE, EBookBackendCache))
+#define E_BOOK_BACKEND_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_BACKEND_CACHE, EBookBackendCacheClass))
+#define E_IS_BOOK_BACKEND_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_BACKEND_CACHE))
+#define E_IS_BOOK_BACKEND_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_BACKEND_CACHE))
+#define E_BOOK_BACKEND_CACHE_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_BACKEND_CACHE, EBookBackendCacheClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookBackendCache EBookBackendCache;
+typedef struct _EBookBackendCacheClass EBookBackendCacheClass;
+typedef struct _EBookBackendCachePrivate EBookBackendCachePrivate;
+
+/**
+ * EBookBackendCache:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+struct _EBookBackendCache {
+ /*< private >*/
+ EFileCache parent;
+ EBookBackendCachePrivate *priv;
+};
+
+/**
+ * EBookBackendCacheClass:
+ *
+ * Class structure for the #EBookBackendCache class.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+struct _EBookBackendCacheClass {
+ /*< private >*/
+ EFileCacheClass parent_class;
+};
+
+GType e_book_backend_cache_get_type (void);
+EBookBackendCache *
+ e_book_backend_cache_new (const gchar *filename);
+EContact * e_book_backend_cache_get_contact (EBookBackendCache *cache,
+ const gchar *uid);
+gboolean e_book_backend_cache_add_contact (EBookBackendCache *cache,
+ EContact *contact);
+gboolean e_book_backend_cache_remove_contact
+ (EBookBackendCache *cache,
+ const gchar *uid);
+gboolean e_book_backend_cache_check_contact
+ (EBookBackendCache *cache,
+ const gchar *uid);
+GList * e_book_backend_cache_get_contacts
+ (EBookBackendCache *cache,
+ const gchar *query);
+void e_book_backend_cache_set_populated
+ (EBookBackendCache *cache);
+gboolean e_book_backend_cache_is_populated
+ (EBookBackendCache *cache);
+void e_book_backend_cache_set_time (EBookBackendCache *cache,
+ const gchar *t);
+gchar * e_book_backend_cache_get_time (EBookBackendCache *cache);
+GPtrArray * e_book_backend_cache_search (EBookBackendCache *cache,
+ const gchar *query);
+
+G_END_DECLS
+
+#endif /* EDS_DISABLE_DEPRECATED */
+
+#endif /* E_BOOK_BACKEND_CACHE_H */
diff --git a/src/addressbook/libedata-book/e-book-backend-db-cache.c b/src/addressbook/libedata-book/e-book-backend-db-cache.c
new file mode 100644
index 000000000..943d9bc45
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-db-cache.c
@@ -0,0 +1,512 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* A class to cache address book conents on local file system
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Devashish Sharma <sdevashish@novell.com>
+ */
+
+/**
+ * SECTION: e-book-backend-db-cache
+ * @include: libedata-book/libedata-book.h
+ * @short_description: A Berkeley DB cache facility for addressbooks
+ *
+ * This API is deprecated, use #EBookSqlite instead.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <db.h>
+
+#include "e-book-backend-db-cache.h"
+#include "e-book-backend.h"
+#include "e-book-backend-sexp.h"
+
+static void
+string_to_dbt (const gchar *str,
+ DBT *dbt)
+{
+ memset (dbt, 0, sizeof (DBT));
+ dbt->data = (gpointer) str;
+ dbt->size = strlen (str) + 1;
+ dbt->flags = DB_DBT_USERMEM;
+}
+
+static gchar *
+get_filename_from_uri (const gchar *uri)
+{
+ const gchar *user_cache_dir;
+ gchar *mangled_uri, *filename;
+
+ user_cache_dir = e_get_user_cache_dir ();
+
+ /* Mangle the URI to not contain invalid characters. */
+ mangled_uri = g_strdelimit (g_strdup (uri), ":/", '_');
+
+ filename = g_build_filename (
+ user_cache_dir, "addressbook",
+ mangled_uri, "cache.db", NULL);
+
+ g_free (mangled_uri);
+
+ return filename;
+}
+
+/**
+ * e_book_backend_db_cache_set_filename:
+ * @db: DB Handle
+ * @filename: filename to be set
+ *
+ * Set the filename for db cacahe file.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+
+void
+e_book_backend_db_cache_set_filename (DB *db,
+ const gchar *filename)
+{
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+
+ string_to_dbt ("filename", &uid_dbt);
+ string_to_dbt (filename, &vcard_dbt);
+
+ db_error = db->put (db, NULL, &uid_dbt, &vcard_dbt, 0);
+ if (db_error != 0) {
+ g_warning ("db->put failed with %d", db_error);
+ }
+
+}
+
+/**
+ * e_book_backend_db_cache_get_filename:
+ * @db: DB Handle
+ *
+ * Get the filename for db cache file.
+ *
+ * Returns: The filename for db cache file. Free with g_free()
+ * when done with it.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+
+gchar *
+e_book_backend_db_cache_get_filename (DB *db)
+{
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+ gchar *filename;
+
+ string_to_dbt ("filename", &uid_dbt);
+ memset (&vcard_dbt, 0 , sizeof (vcard_dbt));
+ vcard_dbt.flags = DB_DBT_MALLOC;
+
+ db_error = db->get (db, NULL, &uid_dbt, &vcard_dbt, 0);
+ if (db_error != 0) {
+ g_warning ("db-<get failed with %d", db_error);
+ return NULL;
+ }
+ else {
+ filename = g_strdup (vcard_dbt.data);
+ g_free (vcard_dbt.data);
+ return filename;
+ }
+}
+
+/**
+ * e_book_backend_db_cache_get_contact:
+ * @db: DB Handle
+ * @uid: a unique contact ID
+ *
+ * Get a cached contact. Note that the returned #EContact will be
+ * newly created, and must be unreffed by the caller when no longer
+ * needed.
+ *
+ * Returns: A cached #EContact, or %NULL if @uid is not cached.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+EContact *
+e_book_backend_db_cache_get_contact (DB *db,
+ const gchar *uid)
+{
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+ EContact *contact = NULL;
+
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ string_to_dbt (uid, &uid_dbt);
+ memset (&vcard_dbt, 0 , sizeof (vcard_dbt));
+ vcard_dbt.flags = DB_DBT_MALLOC;
+
+ db_error = db->get (db, NULL, &uid_dbt, &vcard_dbt,0);
+ if (db_error != 0) {
+ g_warning ("db->get failed with %d", db_error);
+ return NULL;
+ }
+
+ contact = e_contact_new_from_vcard_with_uid ((const gchar *) vcard_dbt.data, uid);
+ g_free (vcard_dbt.data);
+ return contact;
+}
+
+/**
+ * e_book_backend_db_cache_add_contact:
+ * @db: DB Handle
+ * @contact: an #EContact
+ *
+ * Adds @contact to @cache.
+ *
+ * Returns: %TRUE if the contact was cached successfully, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_db_cache_add_contact (DB *db,
+ EContact *contact)
+{
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+ gchar *vcard_str;
+ const gchar *uid;
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ if (!uid) {
+ printf ("no uid\n");
+ printf (
+ "name:%s, email:%s\n",
+ (gchar *) e_contact_get (contact, E_CONTACT_GIVEN_NAME),
+ (gchar *) e_contact_get (contact, E_CONTACT_EMAIL_1));
+ return FALSE;
+ }
+ string_to_dbt (uid, &uid_dbt);
+
+ vcard_str = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+ string_to_dbt (vcard_str, &vcard_dbt);
+
+ /* db_error = db->del (db, NULL, &uid_dbt, 0); */
+ db_error = db->put (db, NULL, &uid_dbt, &vcard_dbt, 0);
+
+ g_free (vcard_str);
+
+ if (db_error != 0) {
+ g_warning ("db->put failed with %d", db_error);
+ return FALSE;
+ }
+ else
+ return TRUE;
+}
+
+/**
+ * e_book_backend_db_cache_remove_contact:
+ * @db: DB Handle
+ * @uid: a unique contact ID
+ *
+ * Removes the contact identified by @uid from @cache.
+ *
+ * Returns: %TRUE if the contact was found and removed, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_db_cache_remove_contact (DB *db,
+ const gchar *uid)
+
+{
+ DBT uid_dbt;
+ gint db_error;
+
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ string_to_dbt (uid, &uid_dbt);
+ db_error = db->del (db, NULL, &uid_dbt, 0);
+
+ if (db_error != 0) {
+ g_warning ("db->del failed with %d", db_error);
+ return FALSE;
+ }
+ else
+ return TRUE;
+
+}
+
+/**
+ * e_book_backend_db_cache_check_contact:
+ * @db: DB Handle
+ * @uid: a unique contact ID
+ *
+ * Checks if the contact identified by @uid exists in @cache.
+ *
+ * Returns: %TRUE if the cache contains the contact, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_db_cache_check_contact (DB *db,
+ const gchar *uid)
+{
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ string_to_dbt (uid, &uid_dbt);
+ memset (&vcard_dbt, 0 , sizeof (vcard_dbt));
+ vcard_dbt.flags = DB_DBT_MALLOC;
+
+ db_error = db->get (db, NULL, &uid_dbt, &vcard_dbt,0);
+ if (db_error != 0)
+ return FALSE;
+ else {
+ free (vcard_dbt.data);
+ return TRUE;
+ }
+}
+
+/**
+ * e_book_backend_db_cache_get_contacts:
+ * @db: DB Handle
+ * @query: an s-expression
+ *
+ * Returns a list of #EContact elements from @cache matching @query.
+ * When done with the list, the caller must unref the contacts and
+ * free the list.
+ *
+ * Returns: A #GList of pointers to #EContact.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+GList *
+e_book_backend_db_cache_get_contacts (DB *db,
+ const gchar *query)
+{
+ DBC *dbc;
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+ GList *list = NULL;
+ EBookBackendSExp *sexp = NULL;
+ EContact *contact;
+
+ if (query) {
+ sexp = e_book_backend_sexp_new (query);
+ if (!sexp)
+ return NULL;
+ }
+
+ db_error = db->cursor (db, NULL, &dbc, 0);
+ if (db_error != 0) {
+ g_warning ("db->cursor failed with %d", db_error);
+ if (sexp)
+ g_object_unref (sexp);
+ return NULL;
+ }
+
+ memset (&vcard_dbt, 0 , sizeof (vcard_dbt));
+ memset (&uid_dbt, 0, sizeof (uid_dbt));
+ db_error = dbc->c_get (dbc, &uid_dbt, &vcard_dbt, DB_FIRST);
+
+ while (db_error == 0) {
+ if (vcard_dbt.data && !strncmp (vcard_dbt.data, "BEGIN:VCARD", 11)) {
+ contact = e_contact_new_from_vcard (vcard_dbt.data);
+
+ if (!sexp || e_book_backend_sexp_match_contact (sexp, contact))
+ list = g_list_prepend (list, contact);
+ else
+ g_object_unref (contact);
+ }
+ db_error = dbc->c_get (dbc, &uid_dbt, &vcard_dbt, DB_NEXT);
+ }
+
+ db_error = dbc->c_close (dbc);
+ if (db_error != 0)
+ g_warning ("db->c_close failed with %d", db_error);
+
+ if (sexp)
+ g_object_unref (sexp);
+
+ return g_list_reverse (list);
+}
+
+/**
+ * e_book_backend_db_cache_search:
+ * @db: DB handle
+ * @query: an s-expression
+ *
+ * Returns an array of pointers to unique contact ID strings for contacts
+ * in @cache matching @query. When done with the array, the caller must
+ * free the ID strings and the array.
+ *
+ * Returns: A #GPtrArray of pointers to contact ID strings.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+GPtrArray *
+e_book_backend_db_cache_search (DB *db,
+ const gchar *query)
+{
+ GList *matching_contacts, *temp;
+ GPtrArray *ptr_array;
+
+ matching_contacts = e_book_backend_db_cache_get_contacts (db, query);
+ ptr_array = g_ptr_array_new ();
+
+ temp = matching_contacts;
+ for (; matching_contacts != NULL; matching_contacts = g_list_next (matching_contacts)) {
+ g_ptr_array_add (ptr_array, e_contact_get (matching_contacts->data, E_CONTACT_UID));
+ g_object_unref (matching_contacts->data);
+ }
+ g_list_free (temp);
+
+ return ptr_array;
+}
+
+/**
+ * e_book_backend_db_cache_exists:
+ * @uri: URI for the cache
+ *
+ * Checks if an #EBookBackendCache exists at @uri.
+ *
+ * Returns: %TRUE if cache exists, %FALSE if not.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_db_cache_exists (const gchar *uri)
+{
+ gchar *file_name;
+ gboolean exists = FALSE;
+ file_name = get_filename_from_uri (uri);
+
+ if (file_name && g_file_test (file_name, G_FILE_TEST_EXISTS))
+ exists = TRUE;
+
+ g_free (file_name);
+
+ return exists;
+}
+
+/**
+ * e_book_backend_db_cache_set_populated:
+ * @db: DB handle
+ *
+ * Flags @cache as being populated - that is, it is up-to-date on the
+ * contents of the book it's caching.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+void
+e_book_backend_db_cache_set_populated (DB *db)
+{
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+
+ string_to_dbt ("populated", &uid_dbt);
+ string_to_dbt ("TRUE", &vcard_dbt);
+ db_error = db->put (db, NULL, &uid_dbt, &vcard_dbt, 0);
+ if (db_error != 0) {
+ g_warning ("db->put failed with %d", db_error);
+ }
+
+}
+
+/**
+ * e_book_backend_db_cache_is_populated:
+ * @db: DB Handle
+ *
+ * Checks if @cache is populated.
+ *
+ * Returns: %TRUE if @cache is populated, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_db_cache_is_populated (DB *db)
+{
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+
+ string_to_dbt ("populated", &uid_dbt);
+ memset (&vcard_dbt, 0, sizeof (vcard_dbt));
+ vcard_dbt.flags = DB_DBT_MALLOC;
+
+ db_error = db->get (db, NULL, &uid_dbt, &vcard_dbt, 0);
+ if (db_error != 0) {
+ return FALSE;
+ }
+ else {
+ free (vcard_dbt.data);
+ return TRUE;
+ }
+}
+
+/**
+ * e_book_backend_db_cache_set_time:
+ * @db: A Berkeley DB handle
+ * @t: The time in string format
+ *
+ * Since: 2.26
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+void
+e_book_backend_db_cache_set_time (DB *db,
+ const gchar *t)
+{
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+
+ string_to_dbt ("last_update_time", &uid_dbt);
+ string_to_dbt (t, &vcard_dbt);
+
+ db_error = db->put (db, NULL, &uid_dbt, &vcard_dbt, 0);
+ if (db_error != 0) {
+ g_warning ("db->put failed with %d", db_error);
+ }
+}
+
+/**
+ * e_book_backend_db_cache_get_time:
+ * @db: A Berkeley DB handle
+ *
+ * Since: 2.26
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gchar *
+e_book_backend_db_cache_get_time (DB *db)
+{
+ DBT uid_dbt, vcard_dbt;
+ gint db_error;
+ gchar *t = NULL;
+
+ string_to_dbt ("last_update_time", &uid_dbt);
+ memset (&vcard_dbt, 0, sizeof (vcard_dbt));
+ vcard_dbt.flags = DB_DBT_MALLOC;
+
+ db_error = db->get (db, NULL, &uid_dbt, &vcard_dbt, 0);
+ if (db_error != 0) {
+ g_warning ("db->get failed with %d", db_error);
+ } else {
+ t = g_strdup (vcard_dbt.data);
+ free (vcard_dbt.data);
+ }
+
+ return t;
+}
diff --git a/src/addressbook/libedata-book/e-book-backend-db-cache.h b/src/addressbook/libedata-book/e-book-backend-db-cache.h
new file mode 100644
index 000000000..d3a03c70c
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-db-cache.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Devashish Sharma <sdevashish@novell.com>
+ */
+
+#ifndef E_BOOK_BACKEND_DB_CACHE_H
+#define E_BOOK_BACKEND_DB_CACHE_H
+
+#ifndef EDS_DISABLE_DEPRECATED
+
+#include <libebook-contacts/libebook-contacts.h>
+
+G_BEGIN_DECLS
+
+/* Avoid including <db.h> in a public header file. */
+struct __db;
+
+EContact * e_book_backend_db_cache_get_contact
+ (struct __db *db,
+ const gchar *uid);
+gchar * e_book_backend_db_cache_get_filename
+ (struct __db *db);
+void e_book_backend_db_cache_set_filename
+ (struct __db *db,
+ const gchar *filename);
+gboolean e_book_backend_db_cache_add_contact
+ (struct __db *db,
+ EContact *contact);
+gboolean e_book_backend_db_cache_remove_contact
+ (struct __db *db,
+ const gchar *uid);
+gboolean e_book_backend_db_cache_check_contact
+ (struct __db *db,
+ const gchar *uid);
+GList * e_book_backend_db_cache_get_contacts
+ (struct __db *db,
+ const gchar *query);
+gboolean e_book_backend_db_cache_exists (const gchar *uri);
+void e_book_backend_db_cache_set_populated
+ (struct __db *db);
+gboolean e_book_backend_db_cache_is_populated
+ (struct __db *db);
+GPtrArray * e_book_backend_db_cache_search (struct __db *db,
+ const gchar *query);
+void e_book_backend_db_cache_set_time
+ (struct __db *db,
+ const gchar *t);
+gchar * e_book_backend_db_cache_get_time
+ (struct __db *db);
+
+G_END_DECLS
+
+#endif /* EDS_DISABLE_DEPRECATED */
+
+#endif /* E_BOOK_BACKEND_DB_CACHE_H */
+
diff --git a/src/addressbook/libedata-book/e-book-backend-factory.c b/src/addressbook/libedata-book/e-book-backend-factory.c
new file mode 100644
index 000000000..27f984323
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-factory.c
@@ -0,0 +1,113 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Chris Toshok (toshok@ximian.com)
+ */
+
+/**
+ * SECTION: e-book-backend-factory
+ * @include: libedata-book/libedata-book.h
+ * @short_description: The factory for creating new addressbooks
+ *
+ * This class handles creation of new addressbooks of various
+ * backend types.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <string.h>
+
+#include "e-book-backend.h"
+#include "e-book-backend-factory.h"
+#include "e-data-book-factory.h"
+
+G_DEFINE_ABSTRACT_TYPE (
+ EBookBackendFactory,
+ e_book_backend_factory,
+ E_TYPE_BACKEND_FACTORY)
+
+static EDataBookFactory *
+book_backend_factory_get_data_factory (EBackendFactory *factory)
+{
+ EExtensible *extensible;
+
+ extensible = e_extension_get_extensible (E_EXTENSION (factory));
+
+ return E_DATA_BOOK_FACTORY (extensible);
+}
+
+static const gchar *
+book_backend_factory_get_hash_key (EBackendFactory *factory)
+{
+ EBookBackendFactoryClass *class;
+ const gchar *component_name;
+ gchar *hash_key;
+ gsize length;
+
+ class = E_BOOK_BACKEND_FACTORY_GET_CLASS (factory);
+ g_return_val_if_fail (class->factory_name != NULL, NULL);
+
+ component_name = E_SOURCE_EXTENSION_ADDRESS_BOOK;
+
+ /* Hash Key: FACTORY_NAME ':' COMPONENT_NAME */
+ length = strlen (class->factory_name) + strlen (component_name) + 2;
+ hash_key = g_alloca (length);
+ g_snprintf (
+ hash_key, length, "%s:%s",
+ class->factory_name, component_name);
+
+ return g_intern_string (hash_key);
+}
+
+static EBackend *
+book_backend_factory_new_backend (EBackendFactory *factory,
+ ESource *source)
+{
+ EBookBackendFactoryClass *class;
+ EDataBookFactory *data_factory;
+ ESourceRegistry *registry;
+
+ class = E_BOOK_BACKEND_FACTORY_GET_CLASS (factory);
+ g_return_val_if_fail (g_type_is_a (
+ class->backend_type, E_TYPE_BOOK_BACKEND), NULL);
+
+ data_factory = book_backend_factory_get_data_factory (factory);
+ registry = e_data_factory_get_registry (E_DATA_FACTORY (data_factory));
+
+ return g_object_new (
+ class->backend_type,
+ "registry", registry,
+ "source", source, NULL);
+}
+
+static void
+e_book_backend_factory_class_init (EBookBackendFactoryClass *class)
+{
+ EExtensionClass *extension_class;
+ EBackendFactoryClass *factory_class;
+
+ extension_class = E_EXTENSION_CLASS (class);
+ extension_class->extensible_type = E_TYPE_DATA_BOOK_FACTORY;
+
+ factory_class = E_BACKEND_FACTORY_CLASS (class);
+ factory_class->get_hash_key = book_backend_factory_get_hash_key;
+ factory_class->new_backend = book_backend_factory_new_backend;
+}
+
+static void
+e_book_backend_factory_init (EBookBackendFactory *factory)
+{
+}
diff --git a/src/addressbook/libedata-book/e-book-backend-factory.h b/src/addressbook/libedata-book/e-book-backend-factory.h
new file mode 100644
index 000000000..0f2d4fd79
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-factory.h
@@ -0,0 +1,93 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/* e-book-backend-factory.h
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Chris Toshok <toshok@ximian.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_BACKEND_FACTORY_H
+#define E_BOOK_BACKEND_FACTORY_H
+
+#include <libebackend/libebackend.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_BACKEND_FACTORY \
+ (e_book_backend_factory_get_type ())
+#define E_BOOK_BACKEND_FACTORY(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_BACKEND_FACTORY, EBookBackendFactory))
+#define E_BOOK_BACKEND_FACTORY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_BACKEND_FACTORY, EBookBackendFactoryClass))
+#define E_IS_BOOK_BACKEND_FACTORY(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_BACKEND_FACTORY))
+#define E_IS_BOOK_BACKEND_FACTORY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_BACKEND_FACTORY))
+#define E_BOOK_BACKEND_FACTORY_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_BACKEND_FACTORY, EBookBackendFactoryClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookBackendFactory EBookBackendFactory;
+typedef struct _EBookBackendFactoryClass EBookBackendFactoryClass;
+typedef struct _EBookBackendFactoryPrivate EBookBackendFactoryPrivate;
+
+/**
+ * EBookBackendFactory:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ */
+struct _EBookBackendFactory {
+ /*< private >*/
+ EBackendFactory parent;
+ EBookBackendFactoryPrivate *priv;
+};
+
+/**
+ * EBookBackendFactoryClass:
+ * @factory_name: The string identifier for this book backend type
+ * @backend_type: The #GType to use to build #EBookBackends for this factory
+ *
+ * Class structure for the #EBookBackendFactory class.
+ *
+ * Subclasses need to set the factory name and backend type
+ * at initialization, the base class will take care of creating
+ * backends of the specified type on demand.
+ */
+struct _EBookBackendFactoryClass {
+ /*< private >*/
+ EBackendFactoryClass parent_class;
+
+ /*< public >*/
+ /* Subclasses just need to set these
+ * class members, we handle the rest. */
+ const gchar *factory_name;
+ GType backend_type;
+};
+
+GType e_book_backend_factory_get_type (void) G_GNUC_CONST;
+
+G_END_DECLS
+
+#endif /* E_BOOK_BACKEND_FACTORY_H */
diff --git a/src/addressbook/libedata-book/e-book-backend-sexp.c b/src/addressbook/libedata-book/e-book-backend-sexp.c
new file mode 100644
index 000000000..0f3ab69ec
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-sexp.c
@@ -0,0 +1,1261 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2012 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Chris Lahey <clahey@ximian.com>
+ * Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+/**
+ * SECTION: e-book-backend-sexp
+ * @include: libedata-book/libedata-book.h
+ * @short_description: A utility for comparing #EContacts or vcards with search expressions.
+ *
+ * This API is an all purpose utility for comparing #EContacts with search expressions generated by #EBookQuery.
+ */
+#include "e-book-backend-sexp.h"
+
+#include <glib/gi18n.h>
+#include <locale.h>
+#include <string.h>
+
+#define E_BOOK_BACKEND_SEXP_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_BOOK_BACKEND_SEXP, EBookBackendSExpPrivate))
+
+G_DEFINE_TYPE (EBookBackendSExp, e_book_backend_sexp, G_TYPE_OBJECT)
+
+typedef struct _SearchContext SearchContext;
+typedef gboolean (*CompareFunc) (const gchar *, const gchar *, const gchar *);
+
+struct _EBookBackendSExpPrivate {
+ ESExp *search_sexp;
+ gchar *text;
+ SearchContext *search_context;
+};
+
+struct _SearchContext {
+ EContact *contact;
+};
+
+static gboolean
+compare_im (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare,
+ EContactField im_field)
+{
+ GList *aims, *l;
+ gboolean found_it = FALSE;
+
+ aims = e_contact_get (contact, im_field);
+
+ for (l = aims; l != NULL; l = l->next) {
+ gchar *im = (gchar *) l->data;
+
+ if (im && compare (im, str, region)) {
+ found_it = TRUE;
+ break;
+ }
+ }
+
+ e_contact_attr_list_free (aims);
+
+ return found_it;
+}
+
+static gboolean
+compare_im_aim (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ return compare_im (contact, str, region, compare, E_CONTACT_IM_AIM);
+}
+
+static gboolean
+compare_im_msn (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ return compare_im (contact, str, region, compare, E_CONTACT_IM_MSN);
+}
+
+static gboolean
+compare_im_skype (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ return compare_im (contact, str, region, compare, E_CONTACT_IM_SKYPE);
+}
+
+static gboolean
+compare_im_google_talk (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ return compare_im (contact, str, region, compare, E_CONTACT_IM_GOOGLE_TALK);
+}
+
+static gboolean
+compare_im_icq (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ return compare_im (contact, str, region, compare, E_CONTACT_IM_ICQ);
+}
+
+static gboolean
+compare_im_yahoo (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ return compare_im (contact, str, region, compare, E_CONTACT_IM_YAHOO);
+}
+
+static gboolean
+compare_im_gadugadu (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ return compare_im (contact, str, region, compare, E_CONTACT_IM_GADUGADU);
+}
+
+static gboolean
+compare_im_jabber (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ return compare_im (contact, str, region, compare, E_CONTACT_IM_JABBER);
+}
+
+static gboolean
+compare_im_groupwise (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ return compare_im (contact, str, region, compare, E_CONTACT_IM_GROUPWISE);
+}
+
+static gboolean
+compare_email (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ gboolean rv = FALSE;
+ GList *list, *l;
+
+ list = e_contact_get (contact, E_CONTACT_EMAIL);
+
+ for (l = list; l; l = l->next) {
+ const gchar *email = l->data;
+
+ rv = email && compare (email, str, region);
+
+ if (rv)
+ break;
+ }
+
+ e_contact_attr_list_free (list);
+
+ return rv;
+}
+
+static gboolean
+compare_phone (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ GList *list, *l;
+ gboolean rv = FALSE;
+
+ list = e_contact_get (contact, E_CONTACT_TEL);
+
+ for (l = list; l; l = l->next) {
+ const gchar *phone = l->data;
+
+ rv = phone && compare (phone, str, region);
+
+ if (rv)
+ break;
+ }
+
+ e_contact_attr_list_free (list);
+
+ return rv;
+}
+
+static gboolean
+compare_name (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ const gchar *name;
+
+ name = e_contact_get_const (contact, E_CONTACT_FULL_NAME);
+ if (name && compare (name, str, region))
+ return TRUE;
+
+ name = e_contact_get_const (contact, E_CONTACT_FAMILY_NAME);
+ if (name && compare (name, str, region))
+ return TRUE;
+
+ name = e_contact_get_const (contact, E_CONTACT_GIVEN_NAME);
+ if (name && compare (name, str, region))
+ return TRUE;
+
+ name = e_contact_get_const (contact, E_CONTACT_NICKNAME);
+ if (name && compare (name, str, region))
+ return TRUE;
+
+ return FALSE;
+}
+
+static gboolean
+compare_photo_uri (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ EContactPhoto *photo;
+ gboolean ret_val = FALSE;
+
+ photo = e_contact_get (contact, E_CONTACT_PHOTO);
+
+ if (photo) {
+ /* Compare the photo uri with the string */
+ if ((photo->type == E_CONTACT_PHOTO_TYPE_URI)
+ && compare (photo->data.uri, str, region)) {
+ ret_val = TRUE;
+ }
+ e_contact_photo_free (photo);
+ }
+ return ret_val;
+}
+
+static gboolean
+compare_address (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+
+ gint i;
+ gboolean rv = FALSE;
+
+ for (i = E_CONTACT_FIRST_ADDRESS_ID; i <= E_CONTACT_LAST_ADDRESS_ID; i++) {
+ EContactAddress *address = e_contact_get (contact, i);
+ if (address) {
+ rv = (address->po && compare (address->po, str, region)) ||
+ (address->street && compare (address->street, str, region)) ||
+ (address->ext && compare (address->ext, str, region)) ||
+ (address->locality && compare (address->locality, str, region)) ||
+ (address->region && compare (address->region, str, region)) ||
+ (address->code && compare (address->code, str, region)) ||
+ (address->country && compare (address->country, str, region));
+
+ e_contact_address_free (address);
+
+ if (rv)
+ break;
+ }
+ }
+
+ return rv;
+
+}
+
+static gboolean
+compare_category (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ GList *categories;
+ GList *iterator;
+ gboolean ret_val = FALSE;
+
+ categories = e_contact_get (contact, E_CONTACT_CATEGORY_LIST);
+
+ for (iterator = categories; iterator; iterator = iterator->next) {
+ const gchar *category = iterator->data;
+
+ if (compare (category, str, region)) {
+ ret_val = TRUE;
+ break;
+ }
+ }
+
+ g_list_foreach (categories, (GFunc) g_free, NULL);
+ g_list_free (categories);
+
+ return ret_val;
+}
+
+static gboolean
+compare_date (EContactDate *date,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare)
+{
+ gchar *date_str = e_contact_date_to_string (date);
+ gboolean ret_val = FALSE;
+
+ if (date_str) {
+ if (compare (date_str, str, region)) {
+ ret_val = TRUE;
+ }
+ g_free (date_str);
+ }
+ return ret_val;
+}
+
+enum prop_type {
+ PROP_TYPE_NORMAL,
+ PROP_TYPE_LIST,
+ PROP_TYPE_DATE
+};
+
+static struct prop_info {
+ EContactField field_id;
+ const gchar *query_prop;
+ enum prop_type prop_type;
+ gboolean (*list_compare) (EContact *contact,
+ const gchar *str,
+ const gchar *region,
+ CompareFunc compare);
+
+} prop_info_table[] = {
+#define NORMAL_PROP(f,q) {f, q, PROP_TYPE_NORMAL, NULL}
+#define LIST_PROP(q,c) {0, q, PROP_TYPE_LIST, c}
+#define DATE_PROP(f,q) {f, q, PROP_TYPE_DATE, NULL}
+
+ /* query prop, type, list compare function */
+ NORMAL_PROP ( E_CONTACT_FILE_AS, "file_as" ),
+ NORMAL_PROP ( E_CONTACT_UID, "id" ),
+ LIST_PROP ( "full_name", compare_name), /* not really a list, but we need to compare both full and surname */
+ LIST_PROP ( "photo", compare_photo_uri ), /* not really a list, but we need to compare the uri in the struct */
+ DATE_PROP ( E_CONTACT_BIRTH_DATE, "birth_date" ),
+ DATE_PROP ( E_CONTACT_ANNIVERSARY, "anniversary" ),
+ NORMAL_PROP ( E_CONTACT_GIVEN_NAME, "given_name"),
+ NORMAL_PROP ( E_CONTACT_FAMILY_NAME, "family_name"),
+ NORMAL_PROP ( E_CONTACT_HOMEPAGE_URL, "url"),
+ NORMAL_PROP ( E_CONTACT_BLOG_URL, "blog_url"),
+ NORMAL_PROP ( E_CONTACT_CALENDAR_URI, "calurl"),
+ NORMAL_PROP ( E_CONTACT_FREEBUSY_URL, "fburl"),
+ NORMAL_PROP ( E_CONTACT_ICS_CALENDAR, "icscalendar"),
+ NORMAL_PROP ( E_CONTACT_VIDEO_URL, "video_url"),
+
+ NORMAL_PROP ( E_CONTACT_MAILER, "mailer"),
+ NORMAL_PROP ( E_CONTACT_ORG, "org"),
+ NORMAL_PROP ( E_CONTACT_ORG_UNIT, "org_unit"),
+ NORMAL_PROP ( E_CONTACT_OFFICE, "office"),
+ NORMAL_PROP ( E_CONTACT_TITLE, "title"),
+ NORMAL_PROP ( E_CONTACT_ROLE, "role"),
+ NORMAL_PROP ( E_CONTACT_MANAGER, "manager"),
+ NORMAL_PROP ( E_CONTACT_ASSISTANT, "assistant"),
+ NORMAL_PROP ( E_CONTACT_NICKNAME, "nickname"),
+ NORMAL_PROP ( E_CONTACT_SPOUSE, "spouse" ),
+ NORMAL_PROP ( E_CONTACT_NOTE, "note"),
+ LIST_PROP ( "im_aim", compare_im_aim ),
+ LIST_PROP ( "im_msn", compare_im_msn ),
+ LIST_PROP ( "im_skype", compare_im_skype ),
+ LIST_PROP ( "im_google_talk", compare_im_google_talk ),
+ LIST_PROP ( "im_icq", compare_im_icq ),
+ LIST_PROP ( "im_jabber", compare_im_jabber ),
+ LIST_PROP ( "im_yahoo", compare_im_yahoo ),
+ LIST_PROP ( "im_gadugadu", compare_im_gadugadu ),
+ LIST_PROP ( "im_groupwise", compare_im_groupwise ),
+ LIST_PROP ( "email", compare_email ),
+ LIST_PROP ( "phone", compare_phone ),
+ LIST_PROP ( "address", compare_address ),
+ LIST_PROP ( "category_list", compare_category ),
+};
+
+static ESExpResult *
+entry_compare (SearchContext *ctx,
+ struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ CompareFunc compare)
+{
+ ESExpResult *r;
+ gint truth = FALSE;
+
+ if ((argc == 2
+ && argv[0]->type == ESEXP_RES_STRING
+ && argv[1]->type == ESEXP_RES_STRING) ||
+ (argc == 3
+ && argv[0]->type == ESEXP_RES_STRING
+ && argv[1]->type == ESEXP_RES_STRING
+ && argv[2]->type == ESEXP_RES_STRING)) {
+ gchar *propname;
+ struct prop_info *info = NULL;
+ const gchar *region = NULL;
+ gint i;
+ gboolean any_field;
+ gboolean saw_any = FALSE;
+
+ if (argc > 2)
+ region = argv[2]->value.string;
+
+ propname = argv[0]->value.string;
+
+ any_field = !strcmp (propname, "x-evolution-any-field");
+ for (i = 0; i < G_N_ELEMENTS (prop_info_table); i++) {
+ if (any_field
+ || !strcmp (prop_info_table[i].query_prop, propname)) {
+ saw_any = TRUE;
+ info = &prop_info_table[i];
+
+ if (any_field && info->field_id == E_CONTACT_UID) {
+ /* We need to skip UID from any field contains search
+ * any-field search should be supported for the
+ * visible fields only.
+ */
+ truth = FALSE;
+ }
+ else if (info->prop_type == PROP_TYPE_NORMAL) {
+ const gchar *prop = NULL;
+ /* straight string property matches */
+
+ prop = e_contact_get_const (ctx->contact, info->field_id);
+
+ if (prop && compare (prop, argv[1]->value.string, region)) {
+ truth = TRUE;
+ }
+ if ((!prop) && compare ("", argv[1]->value.string, region)) {
+ truth = TRUE;
+ }
+ }
+ else if (info->prop_type == PROP_TYPE_LIST) {
+ /* the special searches that match any of the list elements */
+ truth = info->list_compare (ctx->contact, argv[1]->value.string, region, compare);
+ }
+ else if (info->prop_type == PROP_TYPE_DATE) {
+ /* the special searches that match dates */
+ EContactDate *date;
+
+ date = e_contact_get (ctx->contact, info->field_id);
+
+ if (date) {
+ truth = compare_date (date, argv[1]->value.string, region, compare);
+ e_contact_date_free (date);
+ }
+ } else {
+ g_warn_if_reached ();
+
+ saw_any = FALSE;
+ break;
+ }
+
+ /* if we're looking at all fields and find a match,
+ * or if we're just looking at this one field,
+ * break. */
+ if ((any_field && truth)
+ || !any_field)
+ break;
+ }
+ }
+
+ if (!saw_any) {
+ /* propname didn't match to any of our known "special" properties,
+ * so try to find if it isn't a real field and if so, then compare
+ * against value in this field only */
+ EContactField fid = e_contact_field_id (propname);
+
+ if (fid >= E_CONTACT_FIELD_FIRST && fid < E_CONTACT_FIELD_LAST) {
+ const gchar *prop = e_contact_get_const (ctx->contact, fid);
+
+ if (prop && compare (prop, argv[1]->value.string, region)) {
+ truth = TRUE;
+ }
+
+ if ((!prop) && compare ("", argv[1]->value.string, region)) {
+ truth = TRUE;
+ }
+ } else {
+ /* it is not direct EContact known field, so try to find
+ * it in EVCard attributes */
+ GList *a, *attrs = e_vcard_get_attributes (E_VCARD (ctx->contact));
+ for (a = attrs; a && !truth; a = a->next) {
+ EVCardAttribute *attr = (EVCardAttribute *) a->data;
+ if (g_ascii_strcasecmp (e_vcard_attribute_get_name (attr), propname) == 0) {
+ GList *l, *values = e_vcard_attribute_get_values (attr);
+
+ for (l = values; l && !truth; l = l->next) {
+ const gchar *value = l->data;
+
+ if (value && compare (value, argv[1]->value.string, region)) {
+ truth = TRUE;
+ } else if ((!value) && compare ("", argv[1]->value.string, region)) {
+ truth = TRUE;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ r = e_sexp_result_new (f, ESEXP_RES_BOOL);
+ r->value.boolean = truth;
+
+ return r;
+}
+
+static void
+contains_helper_free_word (gpointer data,
+ gpointer user_data)
+{
+ if (data) {
+ g_string_free ((GString *) data, TRUE);
+ }
+}
+
+static gboolean
+try_contains_word (const gchar *s1,
+ GSList *word)
+{
+ const gchar *o, *p;
+ gunichar unival, first_w_char;
+ GString *w;
+
+ if (s1 == NULL)
+ return FALSE;
+ if (word == NULL)
+ return TRUE; /* previous was last word */
+ if (word->data == NULL)
+ return FALSE; /* illegal structure */
+
+ w = word->data;
+ first_w_char = g_utf8_get_char (w->str);
+
+ o = s1;
+ for (p = e_util_unicode_get_utf8 (o, &unival); p && unival; p = e_util_unicode_get_utf8 (p, &unival)) {
+ if (unival == first_w_char) {
+ gunichar unival2;
+ const gchar *q = p;
+ const gchar *r = e_util_unicode_get_utf8 (w->str, &unival2);
+ while (q && r && unival && unival2) {
+ q = e_util_unicode_get_utf8 (q, &unival);
+ if (!q)
+ break;
+ r = e_util_unicode_get_utf8 (r, &unival2);
+ if (!r)
+ break;
+ if (unival != unival2)
+ break;
+ }
+ if (!unival2 && r && q) {
+ /* we read whole word and no illegal character has been found */
+ if (word->next == NULL ||
+ try_contains_word (e_util_unicode_get_utf8 (o, &unival), word->next)) {
+ return TRUE;
+ }
+ }
+ }
+ o = p;
+ }
+
+ return FALSE;
+}
+
+/* first space between words is treated as wildcard character;
+ * we are looking for s2 in s1, so s2 will be breaked into words
+*/
+static gboolean
+contains_helper (const gchar *s1,
+ const gchar *s2,
+ const gchar *region)
+{
+ gchar *s1uni;
+ gchar *s2uni;
+ GSList *words;
+ gchar *next;
+ gboolean have_nonspace;
+ gboolean have_space;
+ GString *last_word, *w;
+ gboolean res;
+ gunichar unich;
+ glong len1, len2;
+
+ if (!s2)
+ return FALSE;
+
+ /* the initial word contains an empty string for sure */
+ if (!*s2)
+ return TRUE;
+
+ s1uni = e_util_utf8_normalize (s1);
+ if (s1uni == NULL)
+ return FALSE;
+
+ s2uni = e_util_utf8_normalize (s2);
+ if (s2uni == NULL) {
+ g_free (s1uni);
+ return FALSE;
+ }
+
+ len1 = g_utf8_strlen (s1uni, -1);
+ len2 = g_utf8_strlen (s2uni, -1);
+ if (len1 == 0 || len2 == 0) {
+ g_free (s1uni);
+ g_free (s2uni);
+
+ /* both are empty strings */
+ if (len1 == len2)
+ return TRUE;
+
+ return FALSE;
+ }
+
+ /* breaking s2 into words */
+ words = NULL;
+ have_nonspace = FALSE;
+ have_space = FALSE;
+ last_word = NULL;
+ w = g_string_new ("");
+ for (next = e_util_unicode_get_utf8 (s2uni, &unich); next && unich; next = e_util_unicode_get_utf8 (next, &unich)) {
+ if (unich == ' ') {
+ if (have_nonspace && !have_space) {
+ /* treat only first space after nonspace character as wildcard,
+ * so we will start new word here
+ */
+ have_space = TRUE;
+ words = g_slist_append (words, w);
+ last_word = w;
+ w = g_string_new ("");
+ } else {
+ g_string_append_unichar (w, unich);
+ }
+ } else {
+ have_nonspace = TRUE;
+ have_space = FALSE;
+ g_string_append_unichar (w, unich);
+ }
+ }
+
+ if (have_space) {
+ /* there was one or more spaces at the end of string,
+ * concat actual word with that last one
+ */
+ g_string_append_len (last_word, w->str, w->len);
+ g_string_free (w, TRUE);
+ } else {
+ /* append actual word into words list */
+ words = g_slist_append (words, w);
+ }
+
+ res = try_contains_word (s1uni, words);
+
+ g_free (s1uni);
+ g_free (s2uni);
+ g_slist_foreach (words, contains_helper_free_word, NULL);
+ g_slist_free (words);
+
+ return res;
+}
+
+static ESExpResult *
+func_contains (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+
+ return entry_compare (ctx, f, argc, argv, contains_helper);
+}
+
+static gboolean
+is_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region)
+{
+ gchar *s1, *s2;
+ gboolean res = FALSE;
+
+ s1 = e_util_utf8_remove_accents (ps1);
+ s2 = e_util_utf8_remove_accents (ps2);
+
+ if (!e_util_utf8_strcasecmp (s1, s2))
+ res = TRUE;
+
+ g_free (s1);
+ g_free (s2);
+
+ return res;
+}
+
+static ESExpResult *
+func_is (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+
+ return entry_compare (ctx, f, argc, argv, is_helper);
+}
+
+static gboolean
+endswith_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region)
+{
+ gchar *s1 = e_util_utf8_remove_accents (ps1);
+ gchar *s2 = e_util_utf8_remove_accents (ps2);
+ gboolean res = FALSE;
+ glong s1len = g_utf8_strlen (s1, -1);
+ glong s2len = g_utf8_strlen (s2, -1);
+
+ if (s1len < s2len)
+ res = FALSE;
+ else
+ res = e_util_utf8_strstrcase (g_utf8_offset_to_pointer (s1, s1len - s2len), s2) != NULL;
+
+ g_free (s1);
+ g_free (s2);
+
+ return res;
+}
+
+static ESExpResult *
+func_endswith (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+
+ return entry_compare (ctx, f, argc, argv, endswith_helper);
+}
+
+static gboolean
+beginswith_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region)
+{
+ gchar *p;
+ gboolean res = FALSE;
+ gchar *s1 = e_util_utf8_remove_accents (ps1);
+ gchar *s2 = e_util_utf8_remove_accents (ps2);
+
+ if ((p = (gchar *) e_util_utf8_strstrcase (s1, s2))
+ && (p == s1))
+ res = TRUE;
+
+ g_free (s1);
+ g_free (s2);
+
+ return res;
+}
+
+static ESExpResult *
+func_beginswith (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+
+ return entry_compare (ctx, f, argc, argv, beginswith_helper);
+}
+
+static gboolean
+eqphone_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region,
+ EPhoneNumberMatch required_match)
+{
+ const EPhoneNumberMatch actual_match =
+ e_phone_number_compare_strings_with_region (
+ ps1, ps2, region, NULL);
+
+ return actual_match >= E_PHONE_NUMBER_MATCH_EXACT
+ && actual_match <= required_match;
+}
+
+static gboolean
+eqphone_exact_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region)
+{
+ return eqphone_helper (ps1, ps2, region, E_PHONE_NUMBER_MATCH_EXACT);
+}
+
+static gboolean
+eqphone_national_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region)
+{
+ return eqphone_helper (ps1, ps2, region, E_PHONE_NUMBER_MATCH_NATIONAL);
+}
+
+static gboolean
+eqphone_short_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region)
+{
+ return eqphone_helper (ps1, ps2, region, E_PHONE_NUMBER_MATCH_SHORT);
+}
+
+static ESExpResult *
+func_eqphone (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+
+ return entry_compare (ctx, f, argc, argv, eqphone_exact_helper);
+}
+
+static ESExpResult *
+func_eqphone_national (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+
+ return entry_compare (ctx, f, argc, argv, eqphone_national_helper);
+}
+
+static ESExpResult *
+func_eqphone_short (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+
+ return entry_compare (ctx, f, argc, argv, eqphone_short_helper);
+}
+
+static gboolean
+regex_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region,
+ gboolean normalize)
+{
+ const gchar *field_data = ps1;
+ const gchar *expression = ps2;
+ GRegex *regex;
+ GError *error = NULL;
+ gboolean match = FALSE;
+
+ regex = g_regex_new (expression, 0, 0, &error);
+ if (!regex) {
+ g_warning (
+ "Failed to parse regular expression '%s': %s",
+ expression, error ? error->message : _("Unknown error"));
+ g_clear_error (&error);
+ return FALSE;
+ }
+
+ if (normalize) {
+ gchar *normal = e_util_utf8_normalize (field_data);
+
+ match = g_regex_match (regex, normal, 0, NULL);
+
+ g_free (normal);
+ } else
+ match = g_regex_match (regex, field_data, 0, NULL);
+
+ g_regex_unref (regex);
+
+ return match;
+}
+
+static gboolean
+regex_normal_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region)
+{
+ return regex_helper (ps1, ps2, region, TRUE);
+}
+
+static gboolean
+regex_raw_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region)
+{
+ return regex_helper (ps1, ps2, region, FALSE);
+}
+
+static ESExpResult *
+func_regex_normal (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+
+ return entry_compare (ctx, f, argc, argv, regex_normal_helper);
+}
+
+static ESExpResult *
+func_regex_raw (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+
+ return entry_compare (ctx, f, argc, argv, regex_raw_helper);
+}
+
+static gboolean
+exists_helper (const gchar *ps1,
+ const gchar *ps2,
+ const gchar *region)
+{
+ gboolean res = FALSE;
+ gchar *s1 = e_util_utf8_remove_accents (ps1);
+ gchar *s2 = e_util_utf8_remove_accents (ps2);
+
+ if (e_util_utf8_strstrcase (s1, s2))
+ res = TRUE;
+
+ g_free (s1);
+ g_free (s2);
+
+ return res;
+}
+
+static ESExpResult *
+func_exists (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+ ESExpResult *r;
+ gint truth = FALSE;
+
+ if (argc == 1
+ && argv[0]->type == ESEXP_RES_STRING) {
+ const gchar *propname;
+ struct prop_info *info = NULL;
+ gint i;
+ gboolean saw_any = FALSE;
+
+ propname = argv[0]->value.string;
+
+ for (i = 0; i < G_N_ELEMENTS (prop_info_table); i++) {
+ if (!strcmp (prop_info_table[i].query_prop, propname)) {
+ saw_any = TRUE;
+ info = &prop_info_table[i];
+
+ if (info->prop_type == PROP_TYPE_NORMAL) {
+ const gchar *prop = NULL;
+ /* searches where the query's property
+ * maps directly to an ecard property */
+
+ prop = e_contact_get_const (ctx->contact, info->field_id);
+
+ if (prop && *prop)
+ truth = TRUE;
+ }
+ else if (info->prop_type == PROP_TYPE_LIST) {
+ /* the special searches that match any of the list elements */
+ truth = info->list_compare (ctx->contact, "", NULL, exists_helper);
+ }
+ else if (info->prop_type == PROP_TYPE_DATE) {
+ EContactDate *date;
+
+ date = e_contact_get (ctx->contact, info->field_id);
+
+ if (date) {
+ truth = TRUE;
+ e_contact_date_free (date);
+ }
+ } else {
+ g_warn_if_reached ();
+
+ saw_any = FALSE;
+ }
+
+ break;
+ }
+ }
+
+ if (!saw_any) {
+ /* propname didn't match to any of our known "special" properties,
+ * so try to find if it isn't a real field and if so, then check
+ * against value in this field only */
+ EContactField fid = e_contact_field_id (propname);
+
+ if (fid >= E_CONTACT_FIELD_FIRST && fid < E_CONTACT_FIELD_LAST &&
+ e_contact_field_is_string (fid)) {
+ const gchar *prop = e_contact_get_const (ctx->contact, fid);
+
+ if (prop && *prop)
+ truth = TRUE;
+ } else {
+ /* is is not a known EContact field, try with EVCard attributes */
+ EVCardAttribute *attr;
+ GList *l, *values;
+
+ if (fid >= E_CONTACT_FIELD_FIRST && fid < E_CONTACT_FIELD_LAST)
+ propname = e_contact_vcard_attribute (fid);
+
+ attr = e_vcard_get_attribute (E_VCARD (ctx->contact), propname);
+ values = attr ? e_vcard_attribute_get_values (attr) : NULL;
+
+ for (l = values; l && !truth; l = l->next) {
+ const gchar *value = l->data;
+
+ if (value && *value)
+ truth = TRUE;
+ }
+ }
+ }
+ }
+ r = e_sexp_result_new (f, ESEXP_RES_BOOL);
+ r->value.boolean = truth;
+
+ return r;
+}
+
+static ESExpResult *
+func_exists_vcard (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ SearchContext *ctx = data;
+ ESExpResult *r;
+ gint truth = FALSE;
+
+ if (argc == 1 && argv[0]->type == ESEXP_RES_STRING) {
+ const gchar *attr_name;
+ EVCardAttribute *attr;
+ GList *values;
+ gchar *s;
+
+ attr_name = argv[0]->value.string;
+ attr = e_vcard_get_attribute (E_VCARD (ctx->contact), attr_name);
+ if (attr) {
+ values = e_vcard_attribute_get_values (attr);
+ if (g_list_length (values) > 0) {
+ s = values->data;
+ if (s[0] != '\0') {
+ truth = TRUE;
+ }
+ }
+ }
+ }
+
+ r = e_sexp_result_new (f, ESEXP_RES_BOOL);
+ r->value.boolean = truth;
+
+ return r;
+}
+
+static void
+book_backend_sexp_finalize (GObject *object)
+{
+ EBookBackendSExpPrivate *priv;
+
+ priv = E_BOOK_BACKEND_SEXP_GET_PRIVATE (object);
+
+ g_object_unref (priv->search_sexp);
+ g_free (priv->text);
+ g_free (priv->search_context);
+
+ /* Chain up to parent's finalize() method. */
+ G_OBJECT_CLASS (e_book_backend_sexp_parent_class)->finalize (object);
+}
+
+static void
+e_book_backend_sexp_class_init (EBookBackendSExpClass *class)
+{
+ GObjectClass *object_class;
+
+ g_type_class_add_private (class, sizeof (EBookBackendSExpPrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->finalize = book_backend_sexp_finalize;
+}
+
+static void
+e_book_backend_sexp_init (EBookBackendSExp *sexp)
+{
+ sexp->priv = E_BOOK_BACKEND_SEXP_GET_PRIVATE (sexp);
+ sexp->priv->search_context = g_new (SearchContext, 1);
+}
+
+/* 'builtin' functions */
+static struct {
+ const gchar *name;
+ ESExpFunc *func;
+ gint type; /* 1 if a function can perform shortcut evaluation,
+ * or doesn't execute everything, 0 otherwise */
+} symbols[] = {
+ { "contains", func_contains, 0 },
+ { "is", func_is, 0 },
+ { "beginswith", func_beginswith, 0 },
+ { "endswith", func_endswith, 0 },
+ { "eqphone", func_eqphone, 0 },
+ { "eqphone_national", func_eqphone_national, 0 },
+ { "eqphone_short", func_eqphone_short, 0 },
+ { "regex_normal", func_regex_normal, 0 },
+ { "regex_raw", func_regex_raw, 0 },
+ { "exists", func_exists, 0 },
+ { "exists_vcard", func_exists_vcard, 0 },
+};
+
+/**
+ * e_book_backend_sexp_new:
+ * @text: an s-expression to parse
+ *
+ * Creates a new #EBookBackendSExp from @text.
+ *
+ * Returns: a new #EBookBackendSExp
+ **/
+EBookBackendSExp *
+e_book_backend_sexp_new (const gchar *text)
+{
+ EBookBackendSExp *sexp;
+ gint ii;
+
+ g_return_val_if_fail (text != NULL, NULL);
+
+ sexp = g_object_new (E_TYPE_BOOK_BACKEND_SEXP, NULL);
+ sexp->priv->search_sexp = e_sexp_new ();
+ sexp->priv->text = g_strdup (text);
+
+ for (ii = 0; ii < G_N_ELEMENTS (symbols); ii++) {
+ if (symbols[ii].type == 1) {
+ e_sexp_add_ifunction (
+ sexp->priv->search_sexp, 0,
+ symbols[ii].name,
+ (ESExpIFunc *) symbols[ii].func,
+ sexp->priv->search_context);
+ } else {
+ e_sexp_add_function (
+ sexp->priv->search_sexp, 0,
+ symbols[ii].name,
+ symbols[ii].func,
+ sexp->priv->search_context);
+ }
+ }
+
+ e_sexp_input_text (sexp->priv->search_sexp, text, strlen (text));
+
+ if (e_sexp_parse (sexp->priv->search_sexp) == -1) {
+ g_warning (
+ "%s: Error in parsing: %s",
+ G_STRFUNC, e_sexp_get_error (sexp->priv->search_sexp));
+ g_object_unref (sexp);
+ sexp = NULL;
+ }
+
+ return sexp;
+}
+
+/**
+ * e_book_backend_sexp_text:
+ * @sexp: an #EBookBackendSExp
+ *
+ * Retrieve the text expression for the given #EBookBackendSExp object.
+ *
+ * Returns: the text expression
+ *
+ * Since: 3.8
+ **/
+const gchar *
+e_book_backend_sexp_text (EBookBackendSExp *sexp)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SEXP (sexp), NULL);
+
+ return sexp->priv->text;
+}
+
+/**
+ * e_book_backend_sexp_match_contact:
+ * @sexp: an #EBookBackendSExp
+ * @contact: an #EContact
+ *
+ * Checks if @contact matches @sexp.
+ *
+ * Returns: %TRUE if the contact matches, %FALSE otherwise
+ **/
+gboolean
+e_book_backend_sexp_match_contact (EBookBackendSExp *sexp,
+ EContact *contact)
+{
+ ESExpResult *r;
+ gboolean retval;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SEXP (sexp), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ sexp->priv->search_context->contact = g_object_ref (contact);
+
+ r = e_sexp_eval (sexp->priv->search_sexp);
+
+ retval = (r && r->type == ESEXP_RES_BOOL && r->value.boolean);
+
+ g_object_unref (sexp->priv->search_context->contact);
+
+ e_sexp_result_free (sexp->priv->search_sexp, r);
+
+ return retval;
+}
+
+/**
+ * e_book_backend_sexp_match_vcard:
+ * @sexp: an #EBookBackendSExp
+ * @vcard: a vCard string
+ *
+ * Checks if @vcard matches @sexp.
+ *
+ * Returns: %TRUE if the vCard matches, %FALSE otherwise
+ **/
+gboolean
+e_book_backend_sexp_match_vcard (EBookBackendSExp *sexp,
+ const gchar *vcard)
+{
+ EContact *contact;
+ gboolean retval;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SEXP (sexp), FALSE);
+ g_return_val_if_fail (vcard != NULL, FALSE);
+
+ contact = e_contact_new_from_vcard (vcard);
+
+ retval = e_book_backend_sexp_match_contact (sexp, contact);
+
+ g_object_unref (contact);
+
+ return retval;
+}
+
diff --git a/src/addressbook/libedata-book/e-book-backend-sexp.h b/src/addressbook/libedata-book/e-book-backend-sexp.h
new file mode 100644
index 000000000..42a9d6fb2
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-sexp.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2012 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Chris Lahey <clahey@ximian.com>
+ * Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_BACKEND_SEXP_H
+#define E_BOOK_BACKEND_SEXP_H
+
+#include <libebook-contacts/libebook-contacts.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_BACKEND_SEXP \
+ (e_book_backend_sexp_get_type ())
+#define E_BOOK_BACKEND_SEXP(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_BACKEND_SEXP, EBookBackendSExp))
+#define E_BOOK_BACKEND_SEXP_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_BACKEND_SEXP, EBookBackendSExpClass))
+#define E_IS_BOOK_BACKEND_SEXP(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_BACKEND_SEXP))
+#define E_IS_BOOK_BACKEND_SEXP_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_BACKEND_SEXP))
+#define E_BOOK_BACKEND_SEXP_GET_CLASS(cls) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_BACKEND_SEXP, EBookBackendSExpClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookBackendSExp EBookBackendSExp;
+typedef struct _EBookBackendSExpClass EBookBackendSExpClass;
+typedef struct _EBookBackendSExpPrivate EBookBackendSExpPrivate;
+
+/**
+ * EBookBackendSexp:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ */
+struct _EBookBackendSExp {
+ /*< private >*/
+ GObject parent;
+ EBookBackendSExpPrivate *priv;
+};
+
+/**
+ * EBookBackendSexpClass:
+ *
+ * Class structure for the #EBookBackendSexp class.
+ */
+struct _EBookBackendSExpClass {
+ /*< private >*/
+ GObjectClass parent_class;
+};
+
+GType e_book_backend_sexp_get_type (void) G_GNUC_CONST;
+EBookBackendSExp *
+ e_book_backend_sexp_new (const gchar *text);
+const gchar * e_book_backend_sexp_text (EBookBackendSExp *sexp);
+gboolean e_book_backend_sexp_match_vcard (EBookBackendSExp *sexp,
+ const gchar *vcard);
+gboolean e_book_backend_sexp_match_contact
+ (EBookBackendSExp *sexp,
+ EContact *contact);
+
+G_END_DECLS
+
+#endif /* E_BOOK_BACKEND_SEXP_H */
diff --git a/src/addressbook/libedata-book/e-book-backend-sqlitedb-test.c b/src/addressbook/libedata-book/e-book-backend-sqlitedb-test.c
new file mode 100644
index 000000000..2a957e9fe
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-sqlitedb-test.c
@@ -0,0 +1,211 @@
+/*-*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* e-book-backend-sqlitedb.c
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * This program 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.
+ *
+ * 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 Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Chenthill Palanisamy <pchenthill@novell.com>
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <libebook-contacts/libebook-contacts.h>
+#include "e-book-backend-sqlitedb.h"
+
+static GMainLoop *main_loop;
+static gchar *cache_path;
+const gchar *op;
+GError *error;
+
+const gchar *email="test@localhost";
+const gchar *folderid = "test_folder_id";
+const gchar *folder_name = "test_folder";
+const gchar *uid ="pas-id-4DCB9FF200000000";
+
+const gchar *vcard_str =
+"BEGIN:VCARD\n"
+"VERSION:3.0\n"
+"URL:test.com\n"
+"TITLE:\n"
+"ROLE:\n"
+"X-EVOLUTION-MANAGER:\n"
+"X-EVOLUTION-ASSISTANT:\n"
+"NICKNAME:\n"
+"X-EVOLUTION-SPOUSE:\n"
+"NOTE:\n"
+"FN:test\n"
+"N:;test;;;\n"
+"X-EVOLUTION-BLOG-URL:test.wordpress.com\n"
+"CALURI:\n"
+"FBURL:\n"
+"X-EVOLUTION-VIDEO-URL:\n"
+"X-MOZILLA-HTML:FALSE\n"
+"X-EVOLUTION-FILE-AS:test\n"
+"EMAIL;X-EVOLUTION-UI-SLOT=1;TYPE=WORK:test@localhost.com\n"
+"EMAIL;X-EVOLUTION-UI-SLOT=2;TYPE=HOME:test@localhome.com\n"
+"UID:pas-id-4DCB9FF200000000\n"
+"REV:2011-05-12T08:53:06Z\n"
+"END:VCARD";
+
+static void
+quit_tests (void)
+{
+
+ if (error != NULL) {
+ g_print ("Tests failed: %s - %s \n", op, error->message);
+ g_clear_error (&error);
+ }
+
+ g_main_loop_quit (main_loop);
+}
+
+static void
+add_contacts (EBookBackendSqliteDB *ebsdb)
+{
+ GSList *contacts = NULL;
+ EContact *con;
+
+ g_print ("Adding contact \n");
+ op = "add contact";
+
+ con = e_contact_new_from_vcard (vcard_str);
+ contacts = g_slist_append (contacts, con);
+ e_book_backend_sqlitedb_add_contacts (ebsdb, folderid, contacts, FALSE, &error);
+
+ g_object_unref (con);
+}
+
+static void
+search_db (EBookBackendSqliteDB *ebsdb,
+ const gchar *type,
+ const gchar *sexp)
+{
+ GSList *vcards;
+ EbSdbSearchData *s_data;
+
+ g_print ("%s - query: %s \n", type, sexp);
+ op = type;
+ vcards = e_book_backend_sqlitedb_search (ebsdb, folderid, sexp, NULL, NULL, NULL, &error);
+ if (error || !vcards)
+ return;
+
+ s_data = vcards->data;
+ g_print ("Result: %s \n", s_data->vcard);
+ e_book_backend_sqlitedb_search_data_free (s_data);
+}
+
+static gboolean
+start_tests (gpointer data)
+{
+ EBookBackendSqliteDB *ebsdb;
+ gboolean populated = FALSE;
+ gchar *vcard_str = NULL, *sexp;
+ EBookQuery *q;
+ GSList *uids = NULL;
+
+ g_print ("Creating the sqlitedb \n");
+ op = "create sqlitedb";
+ ebsdb = e_book_backend_sqlitedb_new
+ (cache_path, email, folderid, folder_name,
+ FALSE, &error);
+ if (error)
+ goto exit;
+
+ add_contacts (ebsdb);
+ if (error)
+ goto exit;
+
+ g_print ("Getting is_populated \n");
+ op = "set is_populated";
+ e_book_backend_sqlitedb_set_is_populated (ebsdb, folderid, TRUE, &error);
+ if (error)
+ goto exit;
+
+ g_print ("Setting is_populated \n");
+ op = "set is_populated";
+ populated = e_book_backend_sqlitedb_get_is_populated (ebsdb, folderid, &error);
+ if (error)
+ goto exit;
+ g_print ("Populated: %d \n", populated);
+
+ g_print ("Setting key value \n");
+ op = "set key/value";
+ e_book_backend_sqlitedb_set_key_value (ebsdb, folderid, "customkey", "stored", &error);
+ if (error)
+ goto exit;
+
+ g_print ("Get Vcard string \n");
+ op = "get vcard string";
+ vcard_str = e_book_backend_sqlitedb_get_vcard_string (ebsdb, folderid, uid, NULL, NULL, &error);
+ if (error)
+ goto exit;
+ g_print ("VCard: %s \n", vcard_str);
+ g_free (vcard_str);
+
+ q = e_book_query_field_test (E_CONTACT_FULL_NAME, E_BOOK_QUERY_CONTAINS, "test");
+ sexp = e_book_query_to_string (q);
+ search_db (ebsdb, "summary query", sexp);
+ e_book_query_unref (q);
+ g_free (sexp);
+ if (error)
+ goto exit;
+
+ /* if (store_vcard) {
+ q = e_book_query_any_field_contains ("word");
+ sexp = e_book_query_to_string (q);
+ search_db (ebsdb, "full_search query", sexp);
+ e_book_query_unref (q);
+ g_free (sexp);
+ if (error)
+ goto exit;
+ } */
+
+ g_print ("Delete contact \n");
+ op = "delete contact";
+ uids = g_slist_append (uids, (gchar *) uid);
+ e_book_backend_sqlitedb_remove_contacts (ebsdb, folderid, uids, &error);
+ g_slist_free (uids);
+ if (error)
+ goto exit;
+
+ g_print ("Delete addressbook \n");
+ op = "delete addressbook";
+ e_book_backend_sqlitedb_delete_addressbook (ebsdb, folderid, &error);
+
+exit:
+ g_object_unref (ebsdb);
+ quit_tests ();
+ return FALSE;
+}
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ if (argc != 2) {
+ g_print ("Please enter a path to store the cache \n");
+ return -1;
+ }
+
+ cache_path = argv[1];
+
+ main_loop = g_main_loop_new (NULL, TRUE);
+ g_idle_add ((GSourceFunc) start_tests, NULL);
+ g_main_loop_run (main_loop);
+
+ /* terminate */
+ g_main_loop_unref (main_loop);
+
+ return 0;
+}
diff --git a/src/addressbook/libedata-book/e-book-backend-sqlitedb.c b/src/addressbook/libedata-book/e-book-backend-sqlitedb.c
new file mode 100644
index 000000000..2f51d2d8a
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-sqlitedb.c
@@ -0,0 +1,6632 @@
+/*-*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* e-book-backend-sqlitedb.c
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2012 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Chenthill Palanisamy <pchenthill@novell.com>
+ * Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+/**
+ * SECTION: e-book-backend-sqlitedb
+ * @include: libedata-book/libedata-book.h
+ * @short_description: An SQLite storage facility for addressbooks
+ *
+ * The #EBookBackendSqliteDB is deprecated, use #EBookSqlite instead.
+ */
+
+#include "e-book-backend-sqlitedb.h"
+
+#include <locale.h>
+#include <string.h>
+#include <errno.h>
+
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+
+#include <sqlite3.h>
+#include <libebackend/libebackend.h>
+
+#include "e-book-backend-sexp.h"
+
+#define E_BOOK_BACKEND_SQLITEDB_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_BOOK_BACKEND_SQLITEDB, EBookBackendSqliteDBPrivate))
+
+#define d(x)
+
+#if d(1)+0
+# define LOCK_MUTEX(mutex) \
+ G_STMT_START { \
+ g_message ("%s: DB Locking ", G_STRFUNC); \
+ g_mutex_lock (mutex); \
+ g_message ("%s: DB Locked ", G_STRFUNC); \
+ } G_STMT_END
+
+# define UNLOCK_MUTEX(mutex) \
+ G_STMT_START { \
+ g_message ("%s: Unlocking ", G_STRFUNC); \
+ g_mutex_unlock (mutex); \
+ g_message ("%s: DB Unlocked ", G_STRFUNC); \
+ } G_STMT_END
+#else
+# define LOCK_MUTEX(mutex) g_mutex_lock (mutex)
+# define UNLOCK_MUTEX(mutex) g_mutex_unlock (mutex)
+#endif
+
+#define DB_FILENAME "contacts.db"
+
+/* WARNING:
+ *
+ * FOLDER_VERSION can NEVER be incremented again.
+ *
+ * This class is deprecated and the continuation of this
+ * very same folder version can be found in EBookSqlite,
+ * use EBookSqlite instead.
+ */
+#define FOLDER_VERSION 7
+
+typedef enum {
+ INDEX_PREFIX = (1 << 0),
+ INDEX_SUFFIX = (1 << 1),
+ INDEX_PHONE = (1 << 2)
+} IndexFlags;
+
+typedef struct {
+ EContactField field; /* The EContact field */
+ GType type; /* The GType (only support string or gboolean) */
+ const gchar *dbname; /* The key for this field in the sqlite3 table */
+ IndexFlags index; /* Whether this summary field should have an index in the SQLite DB */
+} SummaryField;
+
+struct _EBookBackendSqliteDBPrivate {
+ sqlite3 *db;
+ gchar *path;
+ gchar *hash_key;
+
+ GMutex lock;
+ GMutex updates_lock; /* This is for deprecated e_book_backend_sqlitedb_lock_updates () */
+
+ gboolean store_vcard;
+ guint32 in_transaction;
+
+ SummaryField *summary_fields;
+ gint n_summary_fields;
+ guint have_attr_list : 1;
+ IndexFlags attr_list_indexes;
+
+ ECollator *collator; /* The ECollator to create sort keys for all fields */
+ gchar *locale; /* The current locale */
+};
+
+G_DEFINE_TYPE (EBookBackendSqliteDB, e_book_backend_sqlitedb, G_TYPE_OBJECT)
+
+static GHashTable *db_connections = NULL;
+static GMutex dbcon_lock;
+
+static EContactField default_summary_fields[] = {
+ E_CONTACT_UID,
+ E_CONTACT_REV,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_NICKNAME,
+ E_CONTACT_FULL_NAME,
+ E_CONTACT_GIVEN_NAME,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_EMAIL,
+ E_CONTACT_IS_LIST,
+ E_CONTACT_LIST_SHOW_ADDRESSES,
+ E_CONTACT_WANTS_HTML
+};
+
+/* Create indexes on full_name and email fields as autocompletion queries would mainly
+ * rely on this.
+ */
+static EContactField default_indexed_fields[] = {
+ E_CONTACT_FULL_NAME,
+ E_CONTACT_EMAIL
+};
+
+static EBookIndexType default_index_types[] = {
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX
+};
+
+static void
+destroy_search_data (gpointer data)
+{
+ e_book_backend_sqlitedb_search_data_free (data);
+}
+
+static SummaryField * append_summary_field (GArray *array,
+ EContactField field,
+ gboolean *have_attr_list,
+ GError **error);
+
+static gboolean upgrade_contacts_table (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *region,
+ const gchar *lc_collate,
+ GError **error);
+static gboolean sqlitedb_set_locale_internal (EBookBackendSqliteDB *ebsdb,
+ const gchar *locale,
+ GError **error);
+
+static const gchar *
+summary_dbname_from_field (EBookBackendSqliteDB *ebsdb,
+ EContactField field)
+{
+ gint i;
+
+ for (i = 0; i < ebsdb->priv->n_summary_fields; i++) {
+ if (ebsdb->priv->summary_fields[i].field == field)
+ return ebsdb->priv->summary_fields[i].dbname;
+ }
+
+ return NULL;
+}
+
+static gint
+summary_index_from_field (EBookBackendSqliteDB *ebsdb,
+ EContactField field)
+{
+ gint i;
+
+ for (i = 0; i < ebsdb->priv->n_summary_fields; i++) {
+ if (ebsdb->priv->summary_fields[i].field == field)
+ return i;
+ }
+
+ return -1;
+}
+
+static gint
+summary_index_from_field_name (EBookBackendSqliteDB *ebsdb,
+ const gchar *field_name)
+{
+ EContactField field;
+
+ field = e_contact_field_id (field_name);
+
+ return summary_index_from_field (ebsdb, field);
+}
+
+typedef struct {
+ EBookBackendSqliteDB *ebsdb;
+ GSList *list;
+} StoreVCardData;
+
+G_DEFINE_QUARK (
+ e-book-backend-sqlitedb-error-quark,
+ e_book_backend_sqlitedb_error)
+
+static void
+e_book_backend_sqlitedb_dispose (GObject *object)
+{
+ EBookBackendSqliteDBPrivate *priv;
+
+ priv = E_BOOK_BACKEND_SQLITEDB_GET_PRIVATE (object);
+
+ g_mutex_lock (&dbcon_lock);
+ if (db_connections != NULL) {
+ if (priv->hash_key != NULL) {
+ g_hash_table_remove (db_connections, priv->hash_key);
+
+ if (g_hash_table_size (db_connections) == 0) {
+ g_hash_table_destroy (db_connections);
+ db_connections = NULL;
+ }
+
+ g_free (priv->hash_key);
+ priv->hash_key = NULL;
+ }
+ }
+ g_mutex_unlock (&dbcon_lock);
+
+ /* Chain up to parent's dispose() method. */
+ G_OBJECT_CLASS (e_book_backend_sqlitedb_parent_class)->dispose (object);
+}
+
+static void
+e_book_backend_sqlitedb_finalize (GObject *object)
+{
+ EBookBackendSqliteDBPrivate *priv;
+
+ priv = E_BOOK_BACKEND_SQLITEDB_GET_PRIVATE (object);
+
+ sqlite3_close (priv->db);
+
+ g_free (priv->path);
+ g_free (priv->summary_fields);
+ g_free (priv->locale);
+
+ if (priv->collator)
+ e_collator_unref (priv->collator);
+
+ g_mutex_clear (&priv->lock);
+ g_mutex_clear (&priv->updates_lock);
+
+ /* Chain up to parent's finalize() method. */
+ G_OBJECT_CLASS (e_book_backend_sqlitedb_parent_class)->finalize (object);
+}
+
+static void
+e_book_backend_sqlitedb_class_init (EBookBackendSqliteDBClass *class)
+{
+ GObjectClass *object_class;
+
+ g_type_class_add_private (class, sizeof (EBookBackendSqliteDBPrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->dispose = e_book_backend_sqlitedb_dispose;
+ object_class->finalize = e_book_backend_sqlitedb_finalize;
+}
+
+static void
+e_book_backend_sqlitedb_init (EBookBackendSqliteDB *ebsdb)
+{
+ ebsdb->priv = E_BOOK_BACKEND_SQLITEDB_GET_PRIVATE (ebsdb);
+
+ ebsdb->priv->store_vcard = TRUE;
+
+ ebsdb->priv->in_transaction = 0;
+ g_mutex_init (&ebsdb->priv->lock);
+ g_mutex_init (&ebsdb->priv->updates_lock);
+}
+
+static gint
+get_string_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ gchar **ret = ref;
+
+ *ret = g_strdup (cols [0]);
+
+ return 0;
+}
+
+static gint
+get_bool_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ gboolean *ret = ref;
+
+ *ret = cols [0] ? strtoul (cols [0], NULL, 10) : 0;
+
+ return 0;
+}
+
+static gboolean
+book_backend_sql_exec_real (sqlite3 *db,
+ const gchar *stmt,
+ gint (*callback)(gpointer ,gint,gchar **,gchar **),
+ gpointer data,
+ GError **error)
+{
+ gchar *errmsg = NULL;
+ gint ret = -1, retries = 0;
+
+ ret = sqlite3_exec (db, stmt, callback, data, &errmsg);
+ while (ret == SQLITE_BUSY || ret == SQLITE_LOCKED || ret == -1) {
+ /* try for ~15 seconds, then give up */
+ if (retries > 150)
+ break;
+ retries++;
+
+ if (errmsg) {
+ sqlite3_free (errmsg);
+ errmsg = NULL;
+ }
+ g_thread_yield ();
+ g_usleep (100 * 1000); /* Sleep for 100 ms */
+
+ ret = sqlite3_exec (db, stmt, callback, data, &errmsg);
+ }
+
+ if (ret != SQLITE_OK) {
+ d (g_printerr ("Error in SQL EXEC statement: %s [%s].\n", stmt, errmsg));
+ g_set_error_literal (
+ error, E_BOOK_SDB_ERROR,
+ ret == SQLITE_CONSTRAINT ?
+ E_BOOK_SDB_ERROR_CONSTRAINT : E_BOOK_SDB_ERROR_OTHER,
+ errmsg);
+ sqlite3_free (errmsg);
+ errmsg = NULL;
+ return FALSE;
+ }
+
+ if (errmsg) {
+ sqlite3_free (errmsg);
+ errmsg = NULL;
+ }
+
+ return TRUE;
+}
+
+static gint
+print_debug_cb (gpointer ref,
+ gint n_cols,
+ gchar **cols,
+ gchar **name)
+{
+ gint i;
+
+ g_printerr (" DEBUG BEGIN:\n");
+
+ for (i = 0; i < n_cols; i++)
+ g_printerr (" NAME: '%s' VALUE: %s\n", name[i], cols[i]);
+
+ g_printerr (" DEBUG END\n");
+
+ return 0;
+}
+
+static gint G_GNUC_CONST
+booksql_debug (void)
+{
+ static gint booksql_debug = -1;
+
+ if (booksql_debug == -1) {
+ const gchar *const tmp = g_getenv ("BOOKSQL_DEBUG");
+ booksql_debug = (tmp != NULL ? MAX (0, atoi (tmp)) : 0);
+ }
+
+ return booksql_debug;
+}
+
+static void
+book_backend_sql_debug (sqlite3 *db,
+ const gchar *stmt,
+ gint (*callback)(gpointer ,gint,gchar **,gchar **),
+ gpointer data,
+ GError **error)
+{
+ GError *local_error = NULL;
+
+ g_printerr ("DEBUG STATEMENT: %s\n", stmt);
+
+ if (booksql_debug () > 1) {
+ gchar *debug = g_strconcat ("EXPLAIN QUERY PLAN ", stmt, NULL);
+ book_backend_sql_exec_real (db, debug, print_debug_cb, NULL, &local_error);
+ g_free (debug);
+ }
+
+ if (local_error) {
+ g_printerr ("DEBUG STATEMENT END: Error: %s\n", local_error->message);
+ } else if (booksql_debug () > 1) {
+ g_printerr ("DEBUG STATEMENT END: Success\n");
+ }
+
+ g_clear_error (&local_error);
+}
+
+static gboolean
+book_backend_sql_exec (sqlite3 *db,
+ const gchar *stmt,
+ gint (*callback)(gpointer ,gint,gchar **,gchar **),
+ gpointer data,
+ GError **error)
+{
+ if (booksql_debug ())
+ book_backend_sql_debug (db, stmt, callback, data, error);
+
+ return book_backend_sql_exec_real (db, stmt, callback, data, error);
+}
+
+/* This function must always be called with the priv->lock held */
+static gboolean
+book_backend_sqlitedb_start_transaction (EBookBackendSqliteDB *ebsdb,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (ebsdb != NULL, FALSE);
+ g_return_val_if_fail (ebsdb->priv != NULL, FALSE);
+ g_return_val_if_fail (ebsdb->priv->db != NULL, FALSE);
+
+ ebsdb->priv->in_transaction++;
+ g_return_val_if_fail (ebsdb->priv->in_transaction > 0, FALSE);
+
+ if (ebsdb->priv->in_transaction == 1) {
+
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, "BEGIN", NULL, NULL, error);
+ }
+
+ return success;
+}
+
+/* This function must always be called with the priv->lock held */
+static gboolean
+book_backend_sqlitedb_commit_transaction (EBookBackendSqliteDB *ebsdb,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (ebsdb != NULL, FALSE);
+ g_return_val_if_fail (ebsdb->priv != NULL, FALSE);
+ g_return_val_if_fail (ebsdb->priv->db != NULL, FALSE);
+
+ g_return_val_if_fail (ebsdb->priv->in_transaction > 0, FALSE);
+
+ ebsdb->priv->in_transaction--;
+
+ if (ebsdb->priv->in_transaction == 0) {
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, "COMMIT", NULL, NULL, error);
+ }
+
+ return success;
+}
+
+/* This function must always be called with the priv->lock held */
+static gboolean
+book_backend_sqlitedb_rollback_transaction (EBookBackendSqliteDB *ebsdb,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (ebsdb != NULL, FALSE);
+ g_return_val_if_fail (ebsdb->priv != NULL, FALSE);
+ g_return_val_if_fail (ebsdb->priv->db != NULL, FALSE);
+
+ g_return_val_if_fail (ebsdb->priv->in_transaction > 0, FALSE);
+
+ ebsdb->priv->in_transaction--;
+
+ if (ebsdb->priv->in_transaction == 0) {
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, "ROLLBACK", NULL, NULL, error);
+
+ }
+ return success;
+}
+
+static gint
+collect_versions_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ gint *ret = ref;
+
+ /* Just collect the first result, all folders
+ * should always have the same DB version. */
+ *ret = cols [0] ? strtoul (cols [0], NULL, 10) : 0;
+
+ return 0;
+}
+
+typedef struct {
+ gboolean has_countrycode;
+ gboolean has_lc_collate;
+} LocaleColumns;
+
+static gint
+find_locale_columns (gpointer data,
+ gint n_cols,
+ gchar **cols,
+ gchar **name)
+{
+ LocaleColumns *columns = (LocaleColumns *) data;
+ gint i;
+
+ for (i = 0; i < n_cols; i++) {
+ if (g_strcmp0 (cols[i], "countrycode") == 0)
+ columns->has_countrycode = TRUE;
+ else if (g_strcmp0 (cols[i], "lc_collate") == 0)
+ columns->has_lc_collate = TRUE;
+ }
+
+ return 0;
+}
+
+static gboolean
+create_folders_table (EBookBackendSqliteDB *ebsdb,
+ gint *previous_schema,
+ GError **error)
+{
+ gboolean success;
+ gint version = 0;
+ LocaleColumns locale_columns = { FALSE, FALSE };
+
+ /* sync_data points to syncronization data, it could be last_modified
+ * time or a sequence number or some text depending on the backend.
+ *
+ * partial_content says whether the contents are partially downloaded
+ * for auto-completion or if it has the complete content.
+ *
+ * Have not included a bdata here since the keys table should suffice
+ * any additional need that arises.
+ */
+ const gchar *stmt =
+ "CREATE TABLE IF NOT EXISTS folders"
+ "( folder_id TEXT PRIMARY KEY,"
+ " folder_name TEXT,"
+ " sync_data TEXT,"
+ " is_populated INTEGER DEFAULT 0,"
+ " partial_content INTEGER DEFAULT 0,"
+ " version INTEGER,"
+ " revision TEXT,"
+ " multivalues TEXT )";
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error))
+ return FALSE;
+
+ if (!book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error))
+ goto rollback;
+
+ /* Create a child table to store key/value pairs for a folder. */
+ stmt = "CREATE TABLE IF NOT EXISTS keys"
+ "( key TEXT PRIMARY KEY, value TEXT,"
+ " folder_id TEXT REFERENCES folders)";
+ if (!book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error))
+ goto rollback;
+
+ stmt = "CREATE INDEX IF NOT EXISTS keysindex ON keys(folder_id)";
+ if (!book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error))
+ goto rollback;
+
+ /* Fetch the version, it should be the
+ * same for all folders (hence the LIMIT). */
+ stmt = "SELECT version FROM folders LIMIT 1";
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, collect_versions_cb, &version, error);
+
+ if (!success)
+ goto rollback;
+
+ /* Upgrade DB to version 2, add revision column
+ *
+ * (version = 0 indicates that it did not exist and we just
+ * created the table)
+ */
+ if (version >= 1 && version < 2) {
+ stmt = "ALTER TABLE folders ADD COLUMN revision TEXT";
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+
+ if (!success)
+ goto rollback;
+ }
+
+ /* Upgrade DB to version 3, add multivalues introspection columns
+ */
+ if (version >= 1 && version < 3) {
+ stmt = "ALTER TABLE folders ADD COLUMN multivalues TEXT";
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+
+ if (!success)
+ goto rollback;
+ }
+
+ /* Upgrade DB to version 4: Nothing to do. The country-code column it
+ * added got redundant already.
+ */
+
+ /* Upgrade DB to version 5: Drop the reverse_multivalues column, but
+ * wait with converting phone summary values to new format until
+ * create_contacts_table() as we need introspection details for doing
+ * that.
+ */
+ if (version >= 3 && version < 5) {
+ stmt = "UPDATE folders SET "
+ "multivalues = REPLACE(RTRIM(REPLACE("
+ "multivalues || ':', ':', "
+ "CASE reverse_multivalues "
+ "WHEN 0 THEN ';prefix ' "
+ "ELSE ';prefix;suffix ' "
+ "END)), ' ', ':'), "
+ "reverse_multivalues = NULL";
+
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+
+ if (!success)
+ goto rollback;
+ }
+
+ /* Finish the eventual upgrade by storing the current schema version.
+ */
+ if (version >= 1 && version < FOLDER_VERSION) {
+ gchar *version_update_stmt =
+ sqlite3_mprintf ("UPDATE folders SET version = %d", FOLDER_VERSION);
+
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, version_update_stmt, NULL, NULL, error);
+
+ sqlite3_free (version_update_stmt);
+ }
+
+ if (!success)
+ goto rollback;
+
+ /* Ensure countrycode column exists to track the addressbook's country code,
+ * these statements are safe regardless of the previous schema version.
+ */
+ stmt = "PRAGMA table_info(folders)";
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, find_locale_columns, &locale_columns, error);
+
+ if (!success)
+ goto rollback;
+
+ if (!locale_columns.has_countrycode) {
+ stmt = "ALTER TABLE folders ADD COLUMN countrycode VARCHAR(2)";
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+
+ if (!success)
+ goto rollback;
+
+ }
+
+ if (!locale_columns.has_lc_collate) {
+ stmt = "ALTER TABLE folders ADD COLUMN lc_collate TEXT";
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+
+ if (!success)
+ goto rollback;
+ }
+
+ /* Remember the schema version for later use and finish the transaction. */
+ *previous_schema = version;
+ return book_backend_sqlitedb_commit_transaction (ebsdb, error);
+
+rollback:
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ *previous_schema = 0;
+ return FALSE;
+}
+
+static gchar *
+format_multivalues (EBookBackendSqliteDB *ebsdb)
+{
+ gint i;
+ GString *string;
+ gboolean first = TRUE;
+
+ string = g_string_new (NULL);
+
+ for (i = 0; i < ebsdb->priv->n_summary_fields; i++) {
+ if (ebsdb->priv->summary_fields[i].type == E_TYPE_CONTACT_ATTR_LIST) {
+ if (first)
+ first = FALSE;
+ else
+ g_string_append_c (string, ':');
+
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+
+ if ((ebsdb->priv->summary_fields[i].index & INDEX_PREFIX) != 0)
+ g_string_append (string, ";prefix");
+ if ((ebsdb->priv->summary_fields[i].index & INDEX_SUFFIX) != 0)
+ g_string_append (string, ";suffix");
+ if ((ebsdb->priv->summary_fields[i].index & INDEX_PHONE) != 0)
+ g_string_append (string, ";phone");
+ }
+ }
+
+ return g_string_free (string, FALSE);
+}
+
+static gint
+get_count_cb (gpointer ref,
+ gint n_cols,
+ gchar **cols,
+ gchar **name)
+{
+ gint64 count = 0;
+ gint *ret = ref;
+ gint i;
+
+ for (i = 0; i < n_cols; i++) {
+ if (name[i] && strncmp (name[i], "count", 5) == 0) {
+ count = g_ascii_strtoll (cols[i], NULL, 10);
+ }
+ }
+
+ *ret = count;
+
+ return 0;
+}
+
+static gboolean
+check_folderid_exists (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gboolean *exists,
+ GError **error)
+{
+ gboolean success;
+ gint count = 0;
+ gchar *stmt;
+
+ stmt = sqlite3_mprintf ("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=%Q;", folderid);
+
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, get_count_cb, &count, error);
+ sqlite3_free (stmt);
+
+ *exists = (count > 0);
+
+ return success;
+}
+
+static gboolean
+add_folder_into_db (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *folder_name,
+ gboolean *already_exists,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+ gchar *multivalues;
+ gboolean exists = FALSE;
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error))
+ return FALSE;
+
+ success = check_folderid_exists (ebsdb, folderid, &exists, error);
+ if (!success)
+ goto rollback;
+
+ if (!exists) {
+ const gchar *lc_collate;
+
+ multivalues = format_multivalues (ebsdb);
+
+ lc_collate = setlocale (LC_COLLATE, NULL);
+
+ stmt = sqlite3_mprintf (
+ "INSERT OR IGNORE INTO "
+ "folders ( folder_id, folder_name, version, multivalues, lc_collate ) "
+ "VALUES ( %Q, %Q, %d, %Q, %Q ) ",
+ folderid, folder_name, FOLDER_VERSION, multivalues, lc_collate);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ g_free (multivalues);
+
+ if (!success)
+ goto rollback;
+ }
+
+ if (already_exists)
+ *already_exists = exists;
+
+ return book_backend_sqlitedb_commit_transaction (ebsdb, error);
+
+rollback:
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ return FALSE;
+}
+
+static gint
+collect_columns_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ GList **columns = (GList **) ref;
+ gint i;
+
+ for (i = 0; i < col; i++) {
+
+ if (strcmp (name[i], "name") == 0) {
+
+ if (strcmp (cols[i], "vcard") != 0 &&
+ strcmp (cols[i], "bdata") != 0) {
+
+ gchar *column = g_strdup (cols[i]);
+
+ *columns = g_list_prepend (*columns, column);
+ }
+
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static gboolean
+introspect_summary (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error)
+{
+ gboolean success, have_attr_list;
+ gchar *stmt;
+ GList *summary_columns = NULL, *l;
+ GArray *summary_fields = NULL;
+ gchar *multivalues = NULL;
+ gint i, j;
+
+ stmt = sqlite3_mprintf ("PRAGMA table_info (%Q);", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, collect_columns_cb, &summary_columns, error);
+ sqlite3_free (stmt);
+
+ if (!success)
+ goto introspect_summary_finish;
+
+ summary_columns = g_list_reverse (summary_columns);
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+
+ /* Introspect the normal summary fields */
+ for (l = summary_columns; l; l = l->next) {
+ EContactField field;
+ gchar *col = l->data;
+ gchar *p;
+ IndexFlags computed = 0;
+
+ /* Ignore the 'localized' columns */
+ if (g_str_has_suffix (col, "_localized"))
+ continue;
+
+ /* Check if we're parsing a reverse field */
+ if ((p = strstr (col, "_reverse")) != NULL) {
+ computed = INDEX_SUFFIX;
+ *p = '\0';
+ } else if ((p = strstr (col, "_phone")) != NULL) {
+ computed = INDEX_PHONE;
+ *p = '\0';
+ }
+
+ /* First check exception fields */
+ if (g_ascii_strcasecmp (col, "uid") == 0)
+ field = E_CONTACT_UID;
+ else if (g_ascii_strcasecmp (col, "is_list") == 0)
+ field = E_CONTACT_IS_LIST;
+ else
+ field = e_contact_field_id (col);
+
+ /* Check for parse error */
+ if (field == 0) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
+ _("Error introspecting unknown summary field '%s'"), col);
+ success = FALSE;
+ break;
+ }
+
+ /* Computed columns are always declared after the normal columns,
+ * if a reverse field is encountered we need to set the suffix
+ * index on the coresponding summary field
+ */
+ if (computed) {
+ for (i = 0; i < summary_fields->len; i++) {
+ SummaryField *iter = &g_array_index (summary_fields, SummaryField, i);
+
+ if (iter->field == field) {
+ iter->index |= computed;
+ break;
+ }
+ }
+ } else {
+ append_summary_field (summary_fields, field, NULL, NULL);
+ }
+ }
+
+ if (!success)
+ goto introspect_summary_finish;
+
+ /* Introspect the multivalied summary fields */
+ stmt = sqlite3_mprintf (
+ "SELECT multivalues FROM folders WHERE folder_id = %Q", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, get_string_cb, &multivalues, error);
+ sqlite3_free (stmt);
+
+ if (!success)
+ goto introspect_summary_finish;
+
+ ebsdb->priv->attr_list_indexes = 0;
+ ebsdb->priv->have_attr_list = have_attr_list = FALSE;
+
+ if (multivalues) {
+ gchar **fields = g_strsplit (multivalues, ":", 0);
+
+ for (i = 0; fields[i] != NULL; i++) {
+ EContactField field;
+ SummaryField *iter;
+ gchar **params;
+
+ params = g_strsplit (fields[i], ";", 0);
+ field = e_contact_field_id (params[0]);
+ iter = append_summary_field (summary_fields, field, &have_attr_list, NULL);
+
+ if (iter) {
+ for (j = 1; params[j]; ++j) {
+ if (strcmp (params[j], "prefix") == 0) {
+ iter->index |= INDEX_PREFIX;
+ } else if (strcmp (params[j], "suffix") == 0) {
+ iter->index |= INDEX_SUFFIX;
+ } else if (strcmp (params[j], "phone") == 0) {
+ iter->index |= INDEX_PHONE;
+ }
+ }
+
+ ebsdb->priv->attr_list_indexes |= iter->index;
+ }
+
+ g_strfreev (params);
+ }
+
+ ebsdb->priv->have_attr_list = have_attr_list;
+
+ g_strfreev (fields);
+ }
+
+ introspect_summary_finish:
+
+ g_list_free_full (summary_columns, (GDestroyNotify) g_free);
+ g_free (multivalues);
+
+ /* Apply the introspected summary fields */
+ if (success) {
+ g_free (ebsdb->priv->summary_fields);
+ ebsdb->priv->n_summary_fields = summary_fields->len;
+ ebsdb->priv->summary_fields = (SummaryField *) g_array_free (summary_fields, FALSE);
+ } else if (summary_fields) {
+ g_array_free (summary_fields, TRUE);
+ }
+
+ return success;
+}
+
+/* The column names match the fields used in book-backend-sexp */
+static gboolean
+create_contacts_table (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gint previous_schema,
+ gboolean already_exists,
+ GError **error)
+{
+ gint i;
+ gboolean success = TRUE;
+ gchar *stmt, *tmp;
+ GString *string;
+ gboolean relocalized = FALSE;
+ gchar *current_region = NULL;
+ const gchar *lc_collate = NULL;
+ gchar *stored_lc_collate = NULL;
+
+ if (e_phone_number_is_supported ()) {
+ current_region = e_phone_number_get_default_region (error);
+
+ if (current_region == NULL)
+ return FALSE;
+ }
+
+ /* Introspect the summary if the table already exists */
+ if (already_exists) {
+ success = introspect_summary (ebsdb, folderid, error);
+
+ if (!success)
+ return FALSE;
+ }
+
+ string = g_string_new (
+ "CREATE TABLE IF NOT EXISTS %Q ( uid TEXT PRIMARY KEY, ");
+
+ /* Add column creation statements for each summary field.
+ *
+ * Start looping over the summary fields only starting with the second element,
+ * the first element (which is always the UID), is already specified in the
+ * CREATE TABLE statement which we are building.
+ */
+ for (i = 1; i < ebsdb->priv->n_summary_fields; i++) {
+ if (ebsdb->priv->summary_fields[i].type == G_TYPE_STRING) {
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ g_string_append (string, " TEXT, ");
+
+ /* For any string columns (not multivalued columns), also create a localized
+ * data column for sort ordering
+ */
+ if (ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ g_string_append (string, "_localized TEXT, ");
+ }
+
+ } else if (ebsdb->priv->summary_fields[i].type == G_TYPE_BOOLEAN) {
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ g_string_append (string, " INTEGER, ");
+ } else if (ebsdb->priv->summary_fields[i].type != E_TYPE_CONTACT_ATTR_LIST)
+ g_warn_if_reached ();
+
+ /* Additional columns holding normalized reverse values for suffix matching */
+ if (ebsdb->priv->summary_fields[i].type == G_TYPE_STRING) {
+ if (ebsdb->priv->summary_fields[i].index & INDEX_SUFFIX) {
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ g_string_append (string, "_reverse TEXT, ");
+ }
+
+ if (ebsdb->priv->summary_fields[i].index & INDEX_PHONE) {
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ g_string_append (string, "_phone TEXT, ");
+ }
+ }
+ }
+ g_string_append (string, "vcard TEXT, bdata TEXT)");
+
+ stmt = sqlite3_mprintf (string->str, folderid);
+ g_string_free (string, TRUE);
+
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL , error);
+
+ sqlite3_free (stmt);
+
+ /* Now, if we're upgrading from < version 7, we need to add the _localized columns */
+ if (success && previous_schema >= 1 && previous_schema < 7) {
+
+ tmp = sqlite3_mprintf ("ALTER TABLE %Q ADD COLUMN ", folderid);
+
+ /* UID and REV are always the first two summary fields, in this
+ * case we want to localize all strings EXCEPT these two, so
+ * we start iterating with the third element.
+ */
+ for (i = 2; i < ebsdb->priv->n_summary_fields && success; i++) {
+
+ if (ebsdb->priv->summary_fields[i].type == G_TYPE_STRING) {
+ string = g_string_new (tmp);
+
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ g_string_append (string, "_localized TEXT");
+
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, string->str, NULL, NULL , error);
+
+ g_string_free (string, TRUE);
+ }
+ }
+ sqlite3_free (tmp);
+ }
+
+ /* Construct the create statement from the attribute list summary table */
+ if (success && ebsdb->priv->have_attr_list) {
+ string = g_string_new ("CREATE TABLE IF NOT EXISTS %Q ( uid TEXT NOT NULL REFERENCES %Q(uid), "
+ "field TEXT, value TEXT");
+
+ if ((ebsdb->priv->attr_list_indexes & INDEX_SUFFIX) != 0)
+ g_string_append (string, ", value_reverse TEXT");
+ if ((ebsdb->priv->attr_list_indexes & INDEX_PHONE) != 0)
+ g_string_append (string, ", value_phone TEXT");
+
+ g_string_append_c (string, ')');
+
+ tmp = g_strdup_printf ("%s_lists", folderid);
+ stmt = sqlite3_mprintf (string->str, tmp, folderid);
+ g_string_free (string, TRUE);
+
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ /* Give the UID an index in this table, always */
+ stmt = sqlite3_mprintf ("CREATE INDEX IF NOT EXISTS LISTINDEX ON %Q (uid)", tmp);
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ /* Create indexes if specified */
+ if (success && (ebsdb->priv->attr_list_indexes & INDEX_PREFIX) != 0) {
+ stmt = sqlite3_mprintf ("CREATE INDEX IF NOT EXISTS VALINDEX ON %Q (value)", tmp);
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ }
+
+ if (success && (ebsdb->priv->attr_list_indexes & INDEX_SUFFIX) != 0) {
+ stmt = sqlite3_mprintf ("CREATE INDEX IF NOT EXISTS RVALINDEX ON %Q (value_reverse)", tmp);
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ }
+
+ if (success && (ebsdb->priv->attr_list_indexes & INDEX_PHONE) != 0) {
+ stmt = sqlite3_mprintf ("CREATE INDEX IF NOT EXISTS PVALINDEX ON %Q (value_phone)", tmp);
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ }
+
+ g_free (tmp);
+ }
+
+ /* Create indexes on the summary fields configured for indexing */
+ for (i = 0; success && i < ebsdb->priv->n_summary_fields; i++) {
+ if ((ebsdb->priv->summary_fields[i].index & INDEX_PREFIX) != 0 &&
+ ebsdb->priv->summary_fields[i].type != E_TYPE_CONTACT_ATTR_LIST) {
+ /* Derive index name from field & folder */
+ tmp = g_strdup_printf (
+ "INDEX_%s_%s",
+ summary_dbname_from_field (ebsdb, ebsdb->priv->summary_fields[i].field),
+ folderid);
+ stmt = sqlite3_mprintf (
+ "CREATE INDEX IF NOT EXISTS %Q ON %Q (%s)", tmp, folderid,
+ summary_dbname_from_field (ebsdb, ebsdb->priv->summary_fields[i].field));
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ g_free (tmp);
+
+ /* For any indexed column, also index the localized column */
+ if (ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+ tmp = g_strdup_printf (
+ "INDEX_%s_localized_%s",
+ summary_dbname_from_field (ebsdb, ebsdb->priv->summary_fields[i].field),
+ folderid);
+ stmt = sqlite3_mprintf (
+ "CREATE INDEX IF NOT EXISTS %Q ON %Q (%s_localized)", tmp, folderid,
+ summary_dbname_from_field (ebsdb, ebsdb->priv->summary_fields[i].field));
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ g_free (tmp);
+ }
+ }
+
+ if (success &&
+ (ebsdb->priv->summary_fields[i].index & INDEX_SUFFIX) != 0 &&
+ ebsdb->priv->summary_fields[i].type != E_TYPE_CONTACT_ATTR_LIST) {
+ /* Derive index name from field & folder */
+ tmp = g_strdup_printf (
+ "RINDEX_%s_%s",
+ summary_dbname_from_field (ebsdb, ebsdb->priv->summary_fields[i].field),
+ folderid);
+ stmt = sqlite3_mprintf (
+ "CREATE INDEX IF NOT EXISTS %Q ON %Q (%s_reverse)", tmp, folderid,
+ summary_dbname_from_field (ebsdb, ebsdb->priv->summary_fields[i].field));
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ g_free (tmp);
+ }
+
+ if ((ebsdb->priv->summary_fields[i].index & INDEX_PHONE) != 0 &&
+ ebsdb->priv->summary_fields[i].type != E_TYPE_CONTACT_ATTR_LIST) {
+ /* Derive index name from field & folder */
+ tmp = g_strdup_printf (
+ "PINDEX_%s_%s",
+ summary_dbname_from_field (ebsdb, ebsdb->priv->summary_fields[i].field),
+ folderid);
+ stmt = sqlite3_mprintf (
+ "CREATE INDEX IF NOT EXISTS %Q ON %Q (%s_phone)", tmp, folderid,
+ summary_dbname_from_field (ebsdb, ebsdb->priv->summary_fields[i].field));
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ g_free (tmp);
+ }
+ }
+
+ /* Get the locale setting for this addressbook */
+ if (success && already_exists) {
+ stmt = sqlite3_mprintf ("SELECT lc_collate FROM folders WHERE folder_id = %Q", folderid);
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, get_string_cb, &stored_lc_collate, error);
+ sqlite3_free (stmt);
+
+ lc_collate = stored_lc_collate;
+
+ }
+
+ if (!lc_collate)
+ /* When creating a new addressbook, or upgrading from a version
+ * where we did not have any locale setting; default to system locale
+ */
+ lc_collate = setlocale (LC_COLLATE, NULL);
+
+ /* Before touching any data, make sure we have a valid ECollator */
+ if (success) {
+ success = sqlitedb_set_locale_internal (ebsdb, lc_collate, error);
+ }
+
+ /* Need to relocalize the whole thing if the schema has been upgraded to version 7 */
+ if (success && previous_schema >= 1 && previous_schema < 7) {
+ success = upgrade_contacts_table (ebsdb, folderid, current_region, lc_collate, error);
+ relocalized = TRUE;
+ }
+
+ /* We may need to relocalize for a country code change */
+ if (success && relocalized == FALSE && e_phone_number_is_supported ()) {
+ gchar *stored_region = NULL;
+
+ stmt = sqlite3_mprintf ("SELECT countrycode FROM folders WHERE folder_id = %Q", folderid);
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, get_string_cb, &stored_region, error);
+ sqlite3_free (stmt);
+
+ if (success && g_strcmp0 (current_region, stored_region) != 0) {
+ success = upgrade_contacts_table (ebsdb, folderid, current_region, lc_collate, error);
+ relocalized = TRUE;
+ }
+
+ g_free (stored_region);
+ }
+
+ g_free (current_region);
+ g_free (stored_lc_collate);
+
+ return success;
+}
+
+typedef struct {
+ sqlite3 *db;
+ const gchar *collation;
+ const gchar *table;
+} CollationInfo;
+
+static gint
+create_phone_indexes_for_columns (gpointer data,
+ gint n_cols,
+ gchar **cols,
+ gchar **name)
+{
+ const gchar *column_name = cols[1];
+ CollationInfo *info = data;
+
+ if (g_str_has_suffix (column_name, "_phone")) {
+ gchar *index_name, *stmt;
+ GError *error = NULL;
+
+ index_name = g_strdup_printf (
+ "PINDEX_%s_ON_%s_WITH_%s", column_name, info->table, info->collation);
+ stmt = sqlite3_mprintf (
+ "CREATE INDEX IF NOT EXISTS %Q ON %Q (%s COLLATE %Q)",
+ index_name, info->table, column_name, info->collation);
+
+ if (!book_backend_sql_exec (info->db, stmt, NULL, NULL, &error)) {
+ g_warning ("%s: %s", G_STRFUNC, error->message);
+ g_error_free (error);
+ }
+
+ sqlite3_free (stmt);
+ g_free (index_name);
+ }
+
+ return 0;
+}
+
+static gint
+create_phone_indexes_for_tables (gpointer data,
+ gint n_cols,
+ gchar **cols,
+ gchar **name)
+{
+ CollationInfo *info = data;
+ GError *error = NULL;
+ gchar *tmp, *stmt;
+
+ info->table = cols[0];
+ stmt = sqlite3_mprintf ("PRAGMA table_info(%Q)", info->table);
+
+ if (!book_backend_sql_exec (
+ info->db, stmt, create_phone_indexes_for_columns, info, &error)) {
+ g_warning ("%s: %s", G_STRFUNC, error->message);
+ g_clear_error (&error);
+ }
+
+ sqlite3_free (stmt);
+
+ info->table = tmp = g_strconcat (info->table, "_lists", NULL);
+ stmt = sqlite3_mprintf ("PRAGMA table_info(%Q)", info->table);
+
+ if (!book_backend_sql_exec (
+ info->db, stmt, create_phone_indexes_for_columns, info, &error)) {
+ g_warning ("%s: %s", G_STRFUNC, error->message);
+ g_clear_error (&error);
+ }
+
+ sqlite3_free (stmt);
+ g_free (tmp);
+
+ return 0;
+}
+
+static GString *
+ixphone_str (gint country_code,
+ const gchar *const national_str,
+ gint national_len)
+{
+ GString *const str = g_string_sized_new (6 + national_len);
+ g_string_append_printf (str, "+%d|", country_code);
+ g_string_append_len (str, national_str, national_len);
+ return str;
+}
+
+static gint
+e_strcmp2n (const gchar *str1,
+ size_t len1,
+ const gchar *str2,
+ size_t len2)
+{
+ const gint cmp = memcmp (str1, str2, MIN (len1, len2));
+
+ return (cmp != 0 ? cmp :
+ len1 == len2 ? 0 :
+ len1 < len2 ? -1 : 1);
+}
+
+static gint
+ixphone_compare_for_country (gpointer data,
+ gint len1,
+ gconstpointer arg1,
+ gint len2,
+ gconstpointer arg2)
+{
+ const gchar *const str1 = arg1;
+ const gchar *const str2 = arg2;
+ const gchar *const sep1 = memchr (str1, '|', len1);
+ const gchar *const sep2 = memchr (str2, '|', len2);
+ const gint country_code = GPOINTER_TO_INT (data);
+
+ g_return_val_if_fail (sep1 != NULL, 0);
+ g_return_val_if_fail (sep2 != NULL, 0);
+
+ if ((str1 == sep1) == (str2 == sep2))
+ return e_strcmp2n (str1, len1, str2, len2);
+
+ if (str1 == sep1) {
+ GString *const tmp = ixphone_str (country_code, str1, len1);
+ const gint cmp = e_strcmp2n (tmp->str, tmp->len, str2, len2);
+ g_string_free (tmp, TRUE);
+ return cmp;
+ } else {
+ GString *const tmp = ixphone_str (country_code, str2, len2);
+ const gint cmp = e_strcmp2n (str1, len1, tmp->str, tmp->len);
+ g_string_free (tmp, TRUE);
+ return cmp;
+ }
+}
+
+static gint
+ixphone_compare_national (gpointer data,
+ gint len1,
+ gconstpointer arg1,
+ gint len2,
+ gconstpointer arg2)
+{
+ const gchar *const country_code = data;
+ const gchar *const str1 = arg1;
+ const gchar *const str2 = arg2;
+ const gchar *sep1 = memchr (str1, '|', len1);
+ const gchar *sep2 = memchr (str2, '|', len2);
+
+ gint cmp;
+
+ g_return_val_if_fail (sep1 != NULL, 0);
+ g_return_val_if_fail (sep2 != NULL, 0);
+
+ /* First only check national portions */
+ cmp = e_strcmp2n (
+ sep1 + 1, len1 - (sep1 + 1 - str1),
+ sep2 + 1, len2 - (sep2 + 1 - str2));
+
+ /* On match we also have to check for potential country codes.
+ * Note that we can't just compare the full phone number string
+ * in the case that both numbers have a country code, because
+ * this would break the collations sorting order. As a result
+ * the binary search performed on the index would miss matches.
+ * Consider the index contains "|2215423789" and "+31|2215423789"
+ * while we look for "+1|2215423789". By performing full string
+ * compares in case of fully qualified numbers, we might check
+ * "+31|2215423789" first and then miss "|2215423789" because
+ * we traverse the binary tree in wrong direction.
+ */
+ if (cmp == 0) {
+ if (sep1 == str1) {
+ if (sep2 != str2)
+ cmp = e_strcmp2n (country_code, strlen (country_code), str2, sep2 - str2);
+ } else if (sep2 == str2) {
+ cmp = e_strcmp2n (str1, sep1 - str1, country_code, strlen (country_code));
+ } else {
+ /* Also compare the country code if the national number
+ * matches and both numbers have a country code. */
+ cmp = e_strcmp2n (str1, sep1 - str1, str2, sep2 - str2);
+ }
+ }
+
+ if (booksql_debug ()) {
+ gchar *const tmp1 = g_strndup (str1, len1);
+ gchar *const tmp2 = g_strndup (str2, len2);
+
+ g_printerr
+ (" DEBUG %s('%s', '%s') = %d\n",
+ G_STRFUNC, tmp1, tmp2, cmp);
+
+ g_free (tmp2);
+ g_free (tmp1);
+ }
+
+ return cmp;
+}
+
+static void
+create_collation (gpointer data,
+ sqlite3 *db,
+ gint encoding,
+ const gchar *name)
+{
+ gint ret = SQLITE_DONE;
+ gint country_code;
+
+ g_warn_if_fail (encoding == SQLITE_UTF8);
+
+ if (1 == sscanf (name, "ixphone_%d", &country_code)) {
+ ret = sqlite3_create_collation (
+ db, name, SQLITE_UTF8, GINT_TO_POINTER (country_code),
+ ixphone_compare_for_country);
+ } else if (strcmp (name, "ixphone_national") == 0) {
+ country_code = e_phone_number_get_country_code_for_region (NULL, NULL);
+
+ ret = sqlite3_create_collation_v2 (
+ db, name, SQLITE_UTF8,
+ g_strdup_printf ("+%d", country_code),
+ ixphone_compare_national, g_free);
+ }
+
+ if (ret == SQLITE_OK) {
+ CollationInfo info = { db, name };
+ GError *error = NULL;
+
+ if (!book_backend_sql_exec (
+ db, "SELECT folder_id FROM folders",
+ create_phone_indexes_for_tables, &info, &error)) {
+ g_warning ("%s(%s): %s", G_STRFUNC, name, error->message);
+ g_error_free (error);
+ }
+ } else if (ret != SQLITE_DONE) {
+ g_warning ("%s(%s): %s", G_STRFUNC, name, sqlite3_errmsg (db));
+ }
+}
+
+static void
+ebsdb_regexp (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ GRegex *regex;
+ const gchar *expression;
+ const gchar *text;
+
+ /* Reuse the same GRegex for all REGEXP queries with the same expression */
+ regex = sqlite3_get_auxdata (context, 0);
+ if (!regex) {
+ GError *error = NULL;
+
+ expression = (const gchar *) sqlite3_value_text (argv[0]);
+
+ regex = g_regex_new (expression, 0, 0, &error);
+
+ if (!regex) {
+ sqlite3_result_error (
+ context,
+ error ?
+ error->message :
+ _("Error parsing regular expression"),
+ -1);
+ g_clear_error (&error);
+ return;
+ }
+
+ /* SQLite will take care of freeing the GRegex when we're done with the query */
+ sqlite3_set_auxdata (context, 0, regex, (void (*)(gpointer)) g_regex_unref);
+ }
+
+ /* Now perform the comparison */
+ text = (const gchar *) sqlite3_value_text (argv[1]);
+ if (text != NULL) {
+ gboolean match;
+
+ match = g_regex_match (regex, text, 0, NULL);
+ sqlite3_result_int (context, match ? 1 : 0);
+ }
+}
+
+static gboolean
+book_backend_sqlitedb_load (EBookBackendSqliteDB *ebsdb,
+ const gchar *filename,
+ gint *previous_schema,
+ GError **error)
+{
+ gint ret;
+
+ e_sqlite3_vfs_init ();
+
+ ret = sqlite3_open (filename, &ebsdb->priv->db);
+
+ if (ret == SQLITE_OK)
+ ret = sqlite3_collation_needed (ebsdb->priv->db, ebsdb, create_collation);
+
+ if (ret == SQLITE_OK)
+ ret = sqlite3_create_function (
+ ebsdb->priv->db, "regexp", 2, SQLITE_UTF8, ebsdb,
+ ebsdb_regexp, NULL, NULL);
+
+ if (ret != SQLITE_OK) {
+ if (!ebsdb->priv->db) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR,
+ E_BOOK_SDB_ERROR_OTHER,
+ _("Insufficient memory"));
+ } else {
+ const gchar *errmsg;
+ errmsg = sqlite3_errmsg (ebsdb->priv->db);
+ d (g_printerr ("Can't open database %s: %s\n", path, errmsg));
+ g_set_error_literal (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER, errmsg);
+ sqlite3_close (ebsdb->priv->db);
+ }
+ return FALSE;
+ }
+
+ book_backend_sql_exec (
+ ebsdb->priv->db,
+ "ATTACH DATABASE ':memory:' AS mem",
+ NULL, NULL, NULL);
+ book_backend_sql_exec (
+ ebsdb->priv->db,
+ "PRAGMA foreign_keys = ON",
+ NULL, NULL, NULL);
+ book_backend_sql_exec (
+ ebsdb->priv->db,
+ "PRAGMA case_sensitive_like = ON",
+ NULL, NULL, NULL);
+
+ return create_folders_table (ebsdb, previous_schema, error);
+}
+
+static EBookBackendSqliteDB *
+e_book_backend_sqlitedb_new_internal (const gchar *path,
+ const gchar *emailid,
+ const gchar *folderid,
+ const gchar *folder_name,
+ gboolean store_vcard,
+ SummaryField *fields,
+ gint n_fields,
+ gboolean have_attr_list,
+ IndexFlags attr_list_indexes,
+ GError **error)
+{
+ EBookBackendSqliteDB *ebsdb;
+ gchar *hash_key, *filename;
+ gint previous_schema = 0;
+ gboolean already_exists = FALSE;
+
+ g_return_val_if_fail (path != NULL, NULL);
+ g_return_val_if_fail (emailid != NULL, NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+ g_return_val_if_fail (folder_name != NULL, NULL);
+
+ g_mutex_lock (&dbcon_lock);
+
+ hash_key = g_strdup_printf ("%s@%s", emailid, path);
+ if (db_connections != NULL) {
+ ebsdb = g_hash_table_lookup (db_connections, hash_key);
+
+ if (ebsdb) {
+ g_object_ref (ebsdb);
+ g_free (hash_key);
+ goto exit;
+ }
+ }
+
+ ebsdb = g_object_new (E_TYPE_BOOK_BACKEND_SQLITEDB, NULL);
+ ebsdb->priv->path = g_strdup (path);
+ ebsdb->priv->summary_fields = fields;
+ ebsdb->priv->n_summary_fields = n_fields;
+ ebsdb->priv->have_attr_list = have_attr_list;
+ ebsdb->priv->attr_list_indexes = attr_list_indexes;
+ ebsdb->priv->store_vcard = store_vcard;
+
+ if (g_mkdir_with_parents (path, 0777) < 0) {
+ g_mutex_unlock (&dbcon_lock);
+ g_object_unref (ebsdb);
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
+ "Can not make parent directory: errno %d", errno);
+ return NULL;
+ }
+ filename = g_build_filename (path, DB_FILENAME, NULL);
+
+ if (!book_backend_sqlitedb_load (ebsdb, filename, &previous_schema, error)) {
+ g_mutex_unlock (&dbcon_lock);
+ g_object_unref (ebsdb);
+ g_free (filename);
+ return NULL;
+ }
+ g_free (filename);
+
+ if (db_connections == NULL)
+ db_connections = g_hash_table_new_full (
+ (GHashFunc) g_str_hash,
+ (GEqualFunc) g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) NULL);
+ g_hash_table_insert (db_connections, hash_key, ebsdb);
+ ebsdb->priv->hash_key = g_strdup (hash_key);
+
+ exit:
+ /* While the global dbcon_lock is held, hold the ebsdb specific lock and
+ * prolong the locking on that instance until the folders are all set up
+ */
+ LOCK_MUTEX (&ebsdb->priv->lock);
+ g_mutex_unlock (&dbcon_lock);
+
+ if (!add_folder_into_db (ebsdb, folderid, folder_name,
+ &already_exists, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ g_object_unref (ebsdb);
+ return NULL;
+ }
+
+ if (!create_contacts_table (ebsdb, folderid, previous_schema, already_exists, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ g_object_unref (ebsdb);
+ return NULL;
+ }
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return ebsdb;
+}
+
+static SummaryField *
+append_summary_field (GArray *array,
+ EContactField field,
+ gboolean *have_attr_list,
+ GError **error)
+{
+ const gchar *dbname = NULL;
+ GType type = G_TYPE_INVALID;
+ gint i;
+ SummaryField new_field = { 0, };
+
+ if (field < 1 || field >= E_CONTACT_FIELD_LAST) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
+ _("Invalid contact field '%d' specified in summary"), field);
+ return NULL;
+ }
+
+ /* Avoid including the same field twice in the summary */
+ for (i = 0; i < array->len; i++) {
+ SummaryField *iter = &g_array_index (array, SummaryField, i);
+ if (field == iter->field)
+ return iter;
+ }
+
+ /* Resolve some exceptions, we store these
+ * specific contact fields with different names
+ * than those found in the EContactField table
+ */
+ switch (field) {
+ case E_CONTACT_UID:
+ dbname = "uid";
+ break;
+ case E_CONTACT_IS_LIST:
+ dbname = "is_list";
+ break;
+ default:
+ dbname = e_contact_field_name (field);
+ break;
+ }
+
+ type = e_contact_field_type (field);
+
+ if (type != G_TYPE_STRING &&
+ type != G_TYPE_BOOLEAN &&
+ type != E_TYPE_CONTACT_ATTR_LIST) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
+ _("Contact field '%s' of type '%s' specified in summary, "
+ "but only boolean, string and string list field types are supported"),
+ e_contact_pretty_name (field), g_type_name (type));
+ return NULL;
+ }
+
+ if (type == E_TYPE_CONTACT_ATTR_LIST && have_attr_list)
+ *have_attr_list = TRUE;
+
+ new_field.field = field;
+ new_field.dbname = dbname;
+ new_field.type = type;
+ new_field.index = 0;
+ g_array_append_val (array, new_field);
+
+ return &g_array_index (array, SummaryField, array->len - 1);
+}
+
+static void
+summary_fields_add_indexes (GArray *array,
+ EContactField *indexes,
+ EBookIndexType *index_types,
+ gint n_indexes,
+ IndexFlags *attr_list_indexes)
+{
+ gint i, j;
+
+ for (i = 0; i < array->len; i++) {
+ SummaryField *sfield = &g_array_index (array, SummaryField, i);
+
+ for (j = 0; j < n_indexes; j++) {
+ if (sfield->field == indexes[j]) {
+ switch (index_types[j]) {
+ case E_BOOK_INDEX_PREFIX:
+ sfield->index |= INDEX_PREFIX;
+
+ if (sfield->type == E_TYPE_CONTACT_ATTR_LIST)
+ *attr_list_indexes |= INDEX_PREFIX;
+ break;
+ case E_BOOK_INDEX_SUFFIX:
+ sfield->index |= INDEX_SUFFIX;
+
+ if (sfield->type == E_TYPE_CONTACT_ATTR_LIST)
+ *attr_list_indexes |= INDEX_SUFFIX;
+ break;
+ case E_BOOK_INDEX_PHONE:
+ sfield->index |= INDEX_PHONE;
+
+ if (sfield->type == E_TYPE_CONTACT_ATTR_LIST)
+ *attr_list_indexes |= INDEX_PHONE;
+ break;
+ default:
+ g_warn_if_reached ();
+ break;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * e_book_backend_sqlitedb_new_full:
+ * @path: location where the db would be created
+ * @emailid: email id of the user
+ * @folderid: folder id of the address-book
+ * @folder_name: name of the address-book
+ * @store_vcard: True if the vcard should be stored inside db, if FALSE only the summary fields would be stored inside db.
+ * @setup: an #ESourceBackendSummarySetup describing how the summary should be setup
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Like e_book_backend_sqlitedb_new(), but allows configuration of which contact fields
+ * will be stored for quick reference in the summary. The configuration indicated by
+ * @setup will only be taken into account when initially creating the underlying table,
+ * further configurations will be ignored.
+ *
+ * The fields %E_CONTACT_UID and %E_CONTACT_REV are not optional,
+ * they will be stored in the summary regardless of this function's parameters
+ *
+ * <note><para>Only #EContactFields with the type #G_TYPE_STRING, #G_TYPE_BOOLEAN or
+ * #E_TYPE_CONTACT_ATTR_LIST are currently supported.</para></note>
+ *
+ * Returns: (transfer full): The newly created #EBookBackendSqliteDB
+ *
+ * Since: 3.8
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+EBookBackendSqliteDB *
+e_book_backend_sqlitedb_new_full (const gchar *path,
+ const gchar *emailid,
+ const gchar *folderid,
+ const gchar *folder_name,
+ gboolean store_vcard,
+ ESourceBackendSummarySetup *setup,
+ GError **error)
+{
+ EBookBackendSqliteDB *ebsdb = NULL;
+ EContactField *fields;
+ EContactField *indexed_fields;
+ EBookIndexType *index_types = NULL;
+ gboolean have_attr_list = FALSE;
+ IndexFlags attr_list_indexes = 0;
+ gboolean had_error = FALSE;
+ GArray *summary_fields;
+ gint n_fields = 0, n_indexed_fields = 0, i;
+
+ fields = e_source_backend_summary_setup_get_summary_fields (setup, &n_fields);
+ indexed_fields = e_source_backend_summary_setup_get_indexed_fields (setup, &index_types, &n_indexed_fields);
+
+ /* No specified summary fields indicates the default summary configuration should be used */
+ if (n_fields <= 0 || !fields) {
+ ebsdb = e_book_backend_sqlitedb_new (path, emailid, folderid, folder_name, store_vcard, error);
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+
+ return ebsdb;
+ }
+
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+
+ /* Ensure the non-optional fields first */
+ append_summary_field (summary_fields, E_CONTACT_UID, &have_attr_list, error);
+ append_summary_field (summary_fields, E_CONTACT_REV, &have_attr_list, error);
+
+ for (i = 0; i < n_fields; i++) {
+ if (!append_summary_field (summary_fields, fields[i], &have_attr_list, error)) {
+ had_error = TRUE;
+ break;
+ }
+ }
+
+ if (had_error) {
+ g_array_free (summary_fields, TRUE);
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+ return NULL;
+ }
+
+ /* Add the 'indexed' flag to the SummaryField structs */
+ summary_fields_add_indexes (
+ summary_fields, indexed_fields, index_types, n_indexed_fields,
+ &attr_list_indexes);
+
+ ebsdb = e_book_backend_sqlitedb_new_internal (
+ path, emailid, folderid, folder_name,
+ store_vcard,
+ (SummaryField *) summary_fields->data,
+ summary_fields->len,
+ have_attr_list,
+ attr_list_indexes,
+ error);
+
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+ g_array_free (summary_fields, FALSE);
+
+ return ebsdb;
+}
+
+/**
+ * e_book_backend_sqlitedb_new:
+ * @path: location where the db would be created
+ * @emailid: email id of the user
+ * @folderid: folder id of the address-book
+ * @folder_name: name of the address-book
+ * @store_vcard: True if the vcard should be stored inside db, if FALSE only the summary fields would be stored inside db.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * If the path for multiple addressbooks are same, the contacts from all addressbooks
+ * would be stored in same db in different tables.
+ *
+ * Returns: (transfer full): A reference to a #EBookBackendSqliteDB
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+EBookBackendSqliteDB *
+e_book_backend_sqlitedb_new (const gchar *path,
+ const gchar *emailid,
+ const gchar *folderid,
+ const gchar *folder_name,
+ gboolean store_vcard,
+ GError **error)
+{
+ EBookBackendSqliteDB *ebsdb;
+ GArray *summary_fields;
+ gboolean have_attr_list = FALSE;
+ IndexFlags attr_list_indexes = 0;
+ gint i;
+
+ /* Create the default summary structs */
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+ for (i = 0; i < G_N_ELEMENTS (default_summary_fields); i++)
+ append_summary_field (summary_fields, default_summary_fields[i], &have_attr_list, NULL);
+
+ /* Add the default index flags */
+ summary_fields_add_indexes (
+ summary_fields,
+ default_indexed_fields,
+ default_index_types,
+ G_N_ELEMENTS (default_indexed_fields),
+ &attr_list_indexes);
+
+ ebsdb = e_book_backend_sqlitedb_new_internal (
+ path, emailid, folderid, folder_name,
+ store_vcard,
+ (SummaryField *) summary_fields->data,
+ summary_fields->len,
+ have_attr_list,
+ attr_list_indexes,
+ error);
+
+ g_array_free (summary_fields, FALSE);
+
+ return ebsdb;
+}
+
+gboolean
+e_book_backend_sqlitedb_lock_updates (EBookBackendSqliteDB *ebsdb,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->updates_lock);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+ success = book_backend_sqlitedb_start_transaction (ebsdb, error);
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+gboolean
+e_book_backend_sqlitedb_unlock_updates (EBookBackendSqliteDB *ebsdb,
+ gboolean do_commit,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+ success = do_commit ?
+ book_backend_sqlitedb_commit_transaction (ebsdb, error) :
+ book_backend_sqlitedb_rollback_transaction (ebsdb, error);
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ UNLOCK_MUTEX (&ebsdb->priv->updates_lock);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_ref_collator:
+ * @ebsdb: An #EBookBackendSqliteDB
+ *
+ * References the currently active #ECollator for @ebsdb,
+ * use e_collator_unref() when finished using the returned collator.
+ *
+ * Note that the active collator will change with the active locale setting.
+ *
+ * Returns: (transfer full): A reference to the active collator.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+ECollator *
+e_book_backend_sqlitedb_ref_collator (EBookBackendSqliteDB *ebsdb)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+
+ return e_collator_ref (ebsdb->priv->collator);
+}
+
+static gchar *
+mprintf_suffix (const gchar *normal)
+{
+ gchar *reverse = normal ? g_utf8_strreverse (normal, -1) : NULL;
+ gchar *stmt = sqlite3_mprintf ("%Q", reverse);
+
+ g_free (reverse);
+ return stmt;
+}
+
+static EPhoneNumber *
+phone_number_from_string (const gchar *normal,
+ const gchar *default_region)
+{
+ EPhoneNumber *number = NULL;
+
+ /* Don't warn about erronous phone number strings, it's a perfectly normal
+ * use case for users to enter notes instead of phone numbers in the phone
+ * number contact fields, such as "Ask Jenny for Lisa's phone number"
+ */
+ if (normal && e_phone_number_is_supported ())
+ number = e_phone_number_from_string (normal, default_region, NULL);
+
+ return number;
+}
+
+static gchar *
+convert_phone (const gchar *normal,
+ const gchar *default_region)
+{
+ EPhoneNumber *number = phone_number_from_string (normal, default_region);
+ gchar *indexed_phone_number = NULL;
+ gchar *national_number = NULL;
+ gint country_code = 0;
+
+ if (number) {
+ EPhoneNumberCountrySource source;
+
+ national_number = e_phone_number_get_national_number (number);
+ country_code = e_phone_number_get_country_code (number, &source);
+ e_phone_number_free (number);
+
+ if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT)
+ country_code = 0;
+ }
+
+ if (national_number) {
+ indexed_phone_number = country_code
+ ? g_strdup_printf ("+%d|%s", country_code, national_number)
+ : g_strconcat ("|", national_number, NULL);
+
+ g_free (national_number);
+ }
+
+ return indexed_phone_number;
+}
+
+static gchar *
+mprintf_phone (const gchar *normal,
+ const gchar *default_region)
+{
+ gchar *phone = convert_phone (normal, default_region);
+ gchar *stmt = NULL;
+
+ if (phone) {
+ stmt = sqlite3_mprintf ("%Q", phone);
+ g_free (phone);
+ }
+
+ return stmt;
+}
+
+/* Add Contact (free the result with g_free() ) */
+static gchar *
+insert_stmt_from_contact (EBookBackendSqliteDB *ebsdb,
+ EContact *contact,
+ const gchar *folderid,
+ gboolean store_vcard,
+ gboolean replace_existing,
+ const gchar *default_region)
+{
+ GString *string;
+ gchar *str, *vcard_str;
+ gint i;
+
+ str = sqlite3_mprintf (
+ "INSERT or %s INTO %Q (",
+ replace_existing ? "REPLACE" : "FAIL", folderid);
+ string = g_string_new (str);
+ sqlite3_free (str);
+
+ /*
+ * First specify the column names for the insert, since it's possible we
+ * upgraded the DB and cannot be sure the order of the columns are ordered
+ * just how we like them to be.
+ */
+ for (i = 0; i < ebsdb->priv->n_summary_fields; i++) {
+
+ /* Multi values go into a separate table/statement */
+ if (ebsdb->priv->summary_fields[i].type != E_TYPE_CONTACT_ATTR_LIST) {
+
+ /* Only add a ", " before every field except the first,
+ * this will not break because the first 2 fields (UID & REV)
+ * are string fields.
+ */
+ if (i > 0)
+ g_string_append (string, ", ");
+
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ }
+
+ if (ebsdb->priv->summary_fields[i].type == G_TYPE_STRING) {
+
+ if (ebsdb->priv->summary_fields[i].field != E_CONTACT_UID &&
+ ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+ g_string_append (string, ", ");
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ g_string_append (string, "_localized");
+ }
+
+ if ((ebsdb->priv->summary_fields[i].index & INDEX_SUFFIX) != 0) {
+ g_string_append (string, ", ");
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ g_string_append (string, "_reverse");
+ }
+
+ if ((ebsdb->priv->summary_fields[i].index & INDEX_PHONE) != 0) {
+ g_string_append (string, ", ");
+ g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+ g_string_append (string, "_phone");
+ }
+ }
+ }
+ g_string_append (string, ", vcard, bdata)");
+
+ /*
+ * Now specify values for all of the column names we specified.
+ */
+ g_string_append (string, " VALUES (");
+ for (i = 0; i < ebsdb->priv->n_summary_fields; i++) {
+
+ if (ebsdb->priv->summary_fields[i].type != E_TYPE_CONTACT_ATTR_LIST) {
+ /* Only add a ", " before every field except the first,
+ * this will not break because the first 2 fields (UID & REV)
+ * are string fields.
+ */
+ if (i > 0)
+ g_string_append (string, ", ");
+ }
+
+ if (ebsdb->priv->summary_fields[i].type == G_TYPE_STRING) {
+ gchar *val;
+ gchar *normal;
+ gchar *localized = NULL;
+
+ val = e_contact_get (contact, ebsdb->priv->summary_fields[i].field);
+
+ /* Special exception, never normalize/localize the UID or REV string */
+ if (ebsdb->priv->summary_fields[i].field != E_CONTACT_UID &&
+ ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+ normal = e_util_utf8_normalize (val);
+
+ if (val)
+ localized = e_collator_generate_key (ebsdb->priv->collator, val, NULL);
+ else
+ localized = g_strdup ("");
+ } else
+ normal = g_strdup (val);
+
+ str = sqlite3_mprintf ("%Q", normal);
+ g_string_append (string, str);
+ sqlite3_free (str);
+
+ if (ebsdb->priv->summary_fields[i].field != E_CONTACT_UID &&
+ ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+ str = sqlite3_mprintf ("%Q", localized);
+ g_string_append (string, ", ");
+ g_string_append (string, str);
+ sqlite3_free (str);
+ }
+
+ if ((ebsdb->priv->summary_fields[i].index & INDEX_SUFFIX) != 0) {
+ str = mprintf_suffix (normal);
+ g_string_append (string, ", ");
+ g_string_append (string, str);
+ sqlite3_free (str);
+ }
+
+ if ((ebsdb->priv->summary_fields[i].index & INDEX_PHONE) != 0) {
+ str = mprintf_phone (normal, default_region);
+ g_string_append (string, ", ");
+ g_string_append (string, str ? str : "NULL");
+ sqlite3_free (str);
+ }
+
+ g_free (normal);
+ g_free (val);
+ g_free (localized);
+ } else if (ebsdb->priv->summary_fields[i].type == G_TYPE_BOOLEAN) {
+ gboolean val;
+
+ val = e_contact_get (contact, ebsdb->priv->summary_fields[i].field) ? TRUE : FALSE;
+ g_string_append_printf (string, "%d", val ? 1 : 0);
+
+ } else if (ebsdb->priv->summary_fields[i].type != E_TYPE_CONTACT_ATTR_LIST)
+ g_warn_if_reached ();
+ }
+
+ vcard_str = store_vcard ? e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30) : NULL;
+ str = sqlite3_mprintf (", %Q, %Q)", vcard_str, NULL);
+
+ g_string_append (string, str);
+
+ sqlite3_free (str);
+ g_free (vcard_str);
+
+ return g_string_free (string, FALSE);
+}
+
+static void
+update_e164_attribute_params (EVCard *vcard,
+ const gchar *default_region)
+{
+ GList *attr_list;
+
+ for (attr_list = e_vcard_get_attributes (vcard); attr_list; attr_list = attr_list->next) {
+ EVCardAttribute *const attr = attr_list->data;
+ EVCardAttributeParam *param = NULL;
+ gchar *e164 = NULL, *cc, *nn;
+ GList *param_list, *values;
+
+ /* We only attach E164 parameters to TEL attributes. */
+ if (strcmp (e_vcard_attribute_get_name (attr), EVC_TEL) != 0)
+ continue;
+
+ /* Compute E164 number. */
+ values = e_vcard_attribute_get_values (attr);
+
+ e164 = values && values->data
+ ? convert_phone (values->data, default_region)
+ : NULL;
+
+ if (e164 == NULL) {
+ e_vcard_attribute_remove_param (attr, EVC_X_E164);
+ continue;
+ }
+
+ /* Find already exisiting parameter, so that we can reuse it. */
+ for (param_list = e_vcard_attribute_get_params (attr); param_list; param_list = param_list->next) {
+ if (strcmp (e_vcard_attribute_param_get_name (param_list->data), EVC_X_E164) == 0) {
+ param = param_list->data;
+ break;
+ }
+ }
+
+ /* Create a new parameter instance if needed. Otherwise clean
+ * the existing parameter's values: This is much cheaper than
+ * checking for modifications. */
+ if (param == NULL) {
+ param = e_vcard_attribute_param_new (EVC_X_E164);
+ e_vcard_attribute_add_param (attr, param);
+ } else {
+ e_vcard_attribute_param_remove_values (param);
+ }
+
+ /* Split the phone number into country calling code and
+ * national number code. */
+ nn = strchr (e164, '|');
+
+ if (nn == NULL) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ *nn++ = '\0';
+ cc = e164;
+
+ /* Assign the parameter values. It seems odd that we revert
+ * the order of NN and CC, but at least EVCard's parser doesn't
+ * permit an empty first param value. Which of course could be
+ * fixed - in order to create a nice potential IOP problem with
+ ** other vCard parsers. */
+ e_vcard_attribute_param_add_values (param, nn, cc, NULL);
+
+ g_free (e164);
+ }
+}
+
+static gboolean
+insert_contact (EBookBackendSqliteDB *ebsdb,
+ EContact *contact,
+ const gchar *folderid,
+ gboolean replace_existing,
+ const gchar *default_region,
+ GError **error)
+{
+ EBookBackendSqliteDBPrivate *priv;
+ gboolean success;
+ gchar *stmt;
+
+ priv = ebsdb->priv;
+
+ /* Update E.164 parameters in vcard if needed */
+ if (priv->store_vcard)
+ update_e164_attribute_params (E_VCARD (contact), default_region);
+
+ /* Update main summary table */
+ stmt = insert_stmt_from_contact (ebsdb, contact, folderid, priv->store_vcard, replace_existing, default_region);
+ success = book_backend_sql_exec (priv->db, stmt, NULL, NULL, error);
+ g_free (stmt);
+
+ /* Update attribute list table */
+ if (success && priv->have_attr_list) {
+ gchar *list_folder = g_strdup_printf ("%s_lists", folderid);
+ gchar *uid;
+ gint i;
+ GList *values, *l;
+
+ /* First remove all entries for this UID */
+ uid = e_contact_get (contact, E_CONTACT_UID);
+ stmt = sqlite3_mprintf ("DELETE FROM %Q WHERE uid = %Q", list_folder, uid);
+ success = book_backend_sql_exec (priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ for (i = 0; success && i < priv->n_summary_fields; i++) {
+ if (priv->summary_fields[i].type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ values = e_contact_get (contact, priv->summary_fields[i].field);
+
+ for (l = values; success && l != NULL; l = l->next) {
+ gchar *value = (gchar *) l->data;
+ gchar *normal = e_util_utf8_normalize (value);
+ gchar *stmt_suffix = NULL;
+ gchar *stmt_phone = NULL;
+
+ if ((priv->attr_list_indexes & INDEX_SUFFIX) != 0
+ && (priv->summary_fields[i].index & INDEX_SUFFIX) != 0)
+ stmt_suffix = mprintf_suffix (normal);
+
+ if ((priv->attr_list_indexes & INDEX_PHONE) != 0
+ && (priv->summary_fields[i].index & INDEX_PHONE) != 0)
+ stmt_phone = mprintf_phone (normal, default_region);
+
+ stmt = sqlite3_mprintf (
+ "INSERT INTO %Q (uid, field, value%s%s) "
+ "VALUES (%Q, %Q, %Q%s%s%s%s)",
+ list_folder,
+ stmt_suffix ? ", value_reverse" : "",
+ stmt_phone ? ", value_phone" : "",
+ uid, priv->summary_fields[i].dbname, normal,
+ stmt_suffix ? ", " : "",
+ stmt_suffix ? stmt_suffix : "",
+ stmt_phone ? ", " : "",
+ stmt_phone ? stmt_phone : "");
+
+ if (stmt_suffix)
+ sqlite3_free (stmt_suffix);
+ if (stmt_phone)
+ sqlite3_free (stmt_phone);
+
+ success = book_backend_sql_exec (priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ g_free (normal);
+ }
+
+ /* Free the list of allocated strings */
+ e_contact_attr_list_free (values);
+ }
+
+ g_free (list_folder);
+ g_free (uid);
+ }
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_new_contact
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id
+ * @contact: EContact to be added
+ * @replace_existing: Whether this contact should replace another contact with the same UID.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * This is a convenience wrapper for e_book_backend_sqlitedb_new_contacts,
+ * which is the preferred means to add or modify multiple contacts when possible.
+ *
+ * Returns: TRUE on success.
+ *
+ * Since: 3.8
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_new_contact (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ EContact *contact,
+ gboolean replace_existing,
+ GError **error)
+{
+ GSList l;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ l.data = contact;
+ l.next = NULL;
+
+ return e_book_backend_sqlitedb_new_contacts (
+ ebsdb, folderid, &l,
+ replace_existing, error);
+}
+
+/**
+ * e_book_backend_sqlitedb_new_contacts
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id
+ * @contacts: list of EContacts
+ * @replace_existing: Whether this contact should replace another contact with the same UID.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Adds or replaces contacts in @ebsdb. If @replace_existing is specified then existing
+ * contacts with the same UID will be replaced, otherwise adding an existing contact
+ * will return an error.
+ *
+ * Returns: TRUE on success.
+ *
+ * Since: 3.8
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_new_contacts (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GSList *contacts,
+ gboolean replace_existing,
+ GError **error)
+{
+ GSList *l;
+ gboolean success = TRUE;
+ gchar *default_region = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+ g_return_val_if_fail (contacts != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ if (e_phone_number_is_supported ()) {
+ default_region = e_phone_number_get_default_region (error);
+
+ if (default_region == NULL)
+ success = FALSE;
+ }
+
+ for (l = contacts; success && l != NULL; l = g_slist_next (l)) {
+ EContact *contact = (EContact *) l->data;
+
+ success = insert_contact (
+ ebsdb, contact, folderid, replace_existing,
+ default_region, error);
+ }
+
+ g_free (default_region);
+
+ if (success)
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ else
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_add_contact:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id
+ * @contact: EContact to be added
+ * @partial_content: contact does not contain full information. Used when
+ * the backend cache's partial information for auto-completion.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * This is a convenience wrapper for e_book_backend_sqlitedb_add_contacts,
+ * which is the preferred means to add multiple contacts when possible.
+ *
+ * Returns: TRUE on success.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.8: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_add_contact (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ EContact *contact,
+ gboolean partial_content,
+ GError **error)
+{
+ return e_book_backend_sqlitedb_new_contact (ebsdb, folderid, contact, TRUE, error);
+}
+
+/**
+ * e_book_backend_sqlitedb_add_contacts:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id
+ * @contacts: list of EContacts
+ * @partial_content: contact does not contain full information. Used when
+ * the backend cache's partial information for auto-completion.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.8: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_add_contacts (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GSList *contacts,
+ gboolean partial_content,
+ GError **error)
+{
+ return e_book_backend_sqlitedb_new_contacts (ebsdb, folderid, contacts, TRUE, error);
+}
+
+/**
+ * e_book_backend_sqlitedb_remove_contact:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id
+ * @uid: the uid of the contact to remove
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Removes the contact indicated by @uid from the folder @folderid in @ebsdb.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_remove_contact (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ GError **error)
+{
+ GSList l;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ l.data = (gchar *) uid; /* Won't modify it, I promise :) */
+ l.next = NULL;
+
+ return e_book_backend_sqlitedb_remove_contacts (
+ ebsdb, folderid, &l, error);
+}
+
+static gchar *
+generate_uid_list_for_stmt (GSList *uids)
+{
+ GString *str = g_string_new (NULL);
+ GSList *l;
+ gboolean first = TRUE;
+
+ for (l = uids; l; l = l->next) {
+ gchar *uid = (gchar *) l->data;
+ gchar *tmp;
+
+ /* First uid with no comma */
+ if (!first)
+ g_string_append_printf (str, ", ");
+ else
+ first = FALSE;
+
+ tmp = sqlite3_mprintf ("%Q", uid);
+ g_string_append (str, tmp);
+ sqlite3_free (tmp);
+ }
+
+ return g_string_free (str, FALSE);
+}
+
+static gchar *
+generate_delete_stmt (const gchar *table,
+ GSList *uids)
+{
+ GString *str = g_string_new (NULL);
+ gchar *tmp;
+
+ tmp = sqlite3_mprintf ("DELETE FROM %Q WHERE uid IN (", table);
+ g_string_append (str, tmp);
+ sqlite3_free (tmp);
+
+ tmp = generate_uid_list_for_stmt (uids);
+ g_string_append (str, tmp);
+ g_free (tmp);
+ g_string_append_c (str, ')');
+
+ return g_string_free (str, FALSE);
+}
+
+/**
+ * e_book_backend_sqlitedb_remove_contacts:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id
+ * @uids: a #GSList of uids indicating which contacts to remove
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Removes the contacts indicated by @uids from the folder @folderid in @ebsdb.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_remove_contacts (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GSList *uids,
+ GError **error)
+{
+ gboolean success = TRUE;
+ gchar *stmt;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+ g_return_val_if_fail (uids != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ /* Delete the auxillary contact infos first */
+ if (success && ebsdb->priv->have_attr_list) {
+ gchar *lists_folder = g_strdup_printf ("%s_lists", folderid);
+
+ stmt = generate_delete_stmt (lists_folder, uids);
+ g_free (lists_folder);
+
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ g_free (stmt);
+ }
+
+ if (success) {
+ stmt = generate_delete_stmt (folderid, uids);
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+ g_free (stmt);
+ }
+
+ if (success)
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ else
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+static gint
+contact_found_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ gboolean *exists = ref;
+
+ *exists = TRUE;
+
+ return 0;
+}
+
+/**
+ * e_book_backend_sqlitedb_has_contact:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id
+ * @uid: The uid of the contact to check for
+ * @partial_content: This parameter is deprecated and unused.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Checks if a contact bearing the UID indicated by @uid is stored
+ * in @folderid of @ebsdb.
+ *
+ * Returns: %TRUE if the the contact exists and there was no error, otherwise %FALSE.
+ *
+ * <note><para>In order to differentiate an error from a contact which simply
+ * is not stored in @ebsdb, you must pass the @error parameter and check whether
+ * it was set by this function.</para></note>
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_has_contact (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ gboolean *partial_content,
+ GError **error)
+{
+ gboolean exists = FALSE;
+ gboolean success;
+ gchar *stmt;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf (
+ "SELECT uid FROM %Q WHERE uid = %Q",
+ folderid, uid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, contact_found_cb, &exists, error);
+ sqlite3_free (stmt);
+
+ if (partial_content)
+ *partial_content = FALSE;
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ /* FIXME Returning FALSE can mean either "contact not found" or
+ * "error occurred". Add a boolean (out) "exists" parameter. */
+ return success && exists;
+}
+
+static gint
+get_vcard_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ gchar **vcard_str = ref;
+
+ if (cols[0])
+ *vcard_str = g_strdup (cols [0]);
+
+ return 0;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_contact:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id
+ * @uid: The uid of the contact to fetch
+ * @fields_of_interest: (allow-none): A #GHashTable indicating which fields should be included in returned contacts
+ * @with_all_required_fields: (out) (allow-none): Whether all of the fields of interest were available
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetch the #EContact specified by @uid in @folderid of @ebsdb.
+ *
+ * <note><para>The @fields_of_interest parameter is a legacy parameter which can
+ * be used to specify that #EContacts with only the %E_CONTACT_UID
+ * and %E_CONTACT_REV fields. The hash table must use g_str_hash()
+ * and g_str_equal() and the keys 'uid' and 'rev' must be present.</para></note>
+ *
+ * <note><para>In order to differentiate an error from a contact which simply
+ * is not stored in @ebsdb, you must pass the @error parameter and check whether
+ * it was set by this function.</para></note>
+ *
+ * Returns: On success the #EContact corresponding to @uid is returned, otherwise %NULL is
+ * returned if there was an error or if no contact was found for @uid.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+EContact *
+e_book_backend_sqlitedb_get_contact (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ GHashTable *fields_of_interest,
+ gboolean *with_all_required_fields,
+ GError **error)
+{
+ EContact *contact = NULL;
+ gchar *vcard;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ vcard = e_book_backend_sqlitedb_get_vcard_string (
+ ebsdb, folderid, uid,
+ fields_of_interest, with_all_required_fields, error);
+
+ if (vcard != NULL) {
+ contact = e_contact_new_from_vcard_with_uid (vcard, uid);
+ g_free (vcard);
+ }
+
+ return contact;
+}
+
+static gboolean
+uid_rev_fields (GHashTable *fields_of_interest)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+
+ if (!fields_of_interest || g_hash_table_size (fields_of_interest) > 2)
+ return FALSE;
+
+ g_hash_table_iter_init (&iter, fields_of_interest);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *field_name = key;
+ EContactField field = e_contact_field_id (field_name);
+
+ if (field != E_CONTACT_UID &&
+ field != E_CONTACT_REV)
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_sqlitedb_is_summary_fields:
+ * @fields_of_interest: A hash table containing the fields of interest
+ *
+ * This only checks if all the fields are part of the default summary fields,
+ * not part of the configured summary fields.
+ *
+ * Returns: Whether all @fields_of_interest are part of the default summary fields
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.8: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_is_summary_fields (GHashTable *fields_of_interest)
+{
+ gboolean summary_fields = TRUE;
+ GHashTableIter iter;
+ gpointer key, value;
+ gint i;
+
+ if (!fields_of_interest)
+ return FALSE;
+
+ g_hash_table_iter_init (&iter, fields_of_interest);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *field_name = key;
+ EContactField field = e_contact_field_id (field_name);
+ gboolean found = FALSE;
+
+ for (i = 0; i < G_N_ELEMENTS (default_summary_fields); i++) {
+ if (field == default_summary_fields[i]) {
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (!found) {
+ summary_fields = FALSE;
+ break;
+ }
+ }
+
+ return summary_fields;
+}
+
+/**
+ * e_book_backend_sqlitedb_check_summary_fields:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @fields_of_interest: A #GHashTable containing the fields of interest
+ *
+ * Checks if all the specified fields are part of the configured summary
+ * fields for @ebsdb
+ *
+ * <note><para>The @fields_of_interest parameter must use g_str_hash()
+ * and g_str_equal() and the keys in the hash table, specifying contact
+ * fields, should be derived using e_contact_field_name().</para></note>
+ *
+ * Returns: %TRUE if the fields specified in @fields_of_interest are configured
+ * in the summary.
+ *
+ * Since: 3.8
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_check_summary_fields (EBookBackendSqliteDB *ebsdb,
+ GHashTable *fields_of_interest)
+{
+ gboolean summary_fields = TRUE;
+ GHashTableIter iter;
+ gpointer key, value;
+
+ if (!fields_of_interest)
+ return FALSE;
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ g_hash_table_iter_init (&iter, fields_of_interest);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *field_name = key;
+ EContactField field = e_contact_field_id (field_name);
+
+ if (summary_dbname_from_field (ebsdb, field) == NULL) {
+ summary_fields = FALSE;
+ break;
+ }
+ }
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return summary_fields;
+}
+
+/* free return value with g_free */
+static gchar *
+summary_select_stmt (GHashTable *fields_of_interest,
+ gboolean distinct)
+{
+ GString *string;
+
+ if (distinct)
+ string = g_string_new ("SELECT DISTINCT summary.uid");
+ else
+ string = g_string_new ("SELECT summary.uid");
+
+ /* Add the E_CONTACT_REV field if they are both requested */
+ if (g_hash_table_size (fields_of_interest) == 2)
+ g_string_append (string, ", Rev");
+
+ return g_string_free (string, FALSE);
+}
+
+static gint
+store_data_to_vcard (gpointer ref,
+ gint ncol,
+ gchar **cols,
+ gchar **name)
+{
+ GSList **vcard_data = ref;
+ EbSdbSearchData *search_data = g_slice_new0 (EbSdbSearchData);
+ EContact *contact = e_contact_new ();
+ gchar *vcard;
+ gint i;
+
+ /* parse through cols, this will be useful if the api starts supporting field restrictions */
+ for (i = 0; i < ncol; i++)
+ {
+ if (!name[i] || !cols[i])
+ continue;
+
+ /* Only UID & REV can be used to create contacts from the summary columns */
+ if (!g_ascii_strcasecmp (name[i], "uid")) {
+ e_contact_set (contact, E_CONTACT_UID, cols[i]);
+ search_data->uid = g_strdup (cols[i]);
+ } else if (!g_ascii_strcasecmp (name[i], "Rev")) {
+ e_contact_set (contact, E_CONTACT_REV, cols[i]);
+ } else if (!g_ascii_strcasecmp (name[i], "bdata"))
+ search_data->bdata = g_strdup (cols[i]);
+ }
+
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+ search_data->vcard = vcard;
+ *vcard_data = g_slist_prepend (*vcard_data, search_data);
+
+ g_object_unref (contact);
+ return 0;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_vcard_string:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: The folder id
+ * @uid: The uid to fetch a vcard for
+ * @fields_of_interest: The required fields for this vcard, or %NULL to require all fields.
+ * @with_all_required_fields: (allow-none) (out): Whether all the required fields are present in the returned vcard.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Searches @ebsdb in the context of @folderid for @uid.
+ *
+ * If @ebsdb is configured to store the whole vcards, the whole vcard will be returned.
+ * Otherwise the summary cache will be searched and the virtual vcard will be built
+ * from the summary cache.
+ *
+ * In either case, @with_all_required_fields if specified, will be updated to reflect whether
+ * the returned vcard string satisfies the passed 'fields_of_interest' parameter.
+ *
+ * Returns: (transfer full): The vcard string for @uid or %NULL if @uid was not found.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+gchar *
+e_book_backend_sqlitedb_get_vcard_string (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ GHashTable *fields_of_interest,
+ gboolean *with_all_required_fields,
+ GError **error)
+{
+ gchar *stmt;
+ gchar *vcard_str = NULL;
+ gboolean local_with_all_required_fields = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ /* Try constructing contacts from only UID/REV first if that's requested */
+ if (uid_rev_fields (fields_of_interest)) {
+ GSList *vcards = NULL;
+ gchar *select_portion;
+
+ select_portion = summary_select_stmt (fields_of_interest, FALSE);
+
+ stmt = sqlite3_mprintf (
+ "%s FROM %Q AS summary WHERE summary.uid = %Q",
+ select_portion, folderid, uid);
+ book_backend_sql_exec (ebsdb->priv->db, stmt, store_data_to_vcard, &vcards, error);
+ sqlite3_free (stmt);
+ g_free (select_portion);
+
+ if (vcards) {
+ EbSdbSearchData *s_data = (EbSdbSearchData *) vcards->data;
+
+ vcard_str = s_data->vcard;
+ s_data->vcard = NULL;
+
+ g_slist_free_full (vcards, destroy_search_data);
+ vcards = NULL;
+ }
+
+ local_with_all_required_fields = TRUE;
+ } else if (ebsdb->priv->store_vcard) {
+ stmt = sqlite3_mprintf (
+ "SELECT vcard FROM %Q WHERE uid = %Q", folderid, uid);
+ book_backend_sql_exec (
+ ebsdb->priv->db, stmt,
+ get_vcard_cb , &vcard_str, error);
+ sqlite3_free (stmt);
+
+ local_with_all_required_fields = TRUE;
+ } else {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
+ _("Full search_contacts are not stored in cache. vcards cannot be returned."));
+
+ }
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (with_all_required_fields)
+ *with_all_required_fields = local_with_all_required_fields;
+
+ if (!vcard_str && error && !*error)
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_CONTACT_NOT_FOUND,
+ _("Contact '%s' not found"), uid);
+
+ return vcard_str;
+}
+
+enum {
+ CHECK_IS_SUMMARY = (1 << 0),
+ CHECK_IS_LIST_ATTR = (1 << 1),
+ CHECK_UNSUPPORTED = (1 << 2),
+ CHECK_INVALID = (1 << 3)
+};
+
+static ESExpResult *
+func_check_subset (ESExp *f,
+ gint argc,
+ struct _ESExpTerm **argv,
+ gpointer data)
+{
+ ESExpResult *r, *r1;
+ gboolean one_non_summary_query = FALSE;
+ gint result = 0;
+ gint i;
+
+ for (i = 0; i < argc; i++) {
+ r1 = e_sexp_term_eval (f, argv[i]);
+
+ if (r1->type != ESEXP_RES_INT) {
+ e_sexp_result_free (f, r1);
+ continue;
+ }
+
+ result |= r1->value.number;
+
+ if ((r1->value.number & CHECK_IS_SUMMARY) == 0)
+ one_non_summary_query = TRUE;
+
+ e_sexp_result_free (f, r1);
+ }
+
+ /* If at least one subset is not a summary query,
+ * then the whole query is not a summary query and
+ * thus cannot be done with an SQL statement
+ */
+ if (one_non_summary_query)
+ result &= ~CHECK_IS_SUMMARY;
+
+ r = e_sexp_result_new (f, ESEXP_RES_INT);
+ r->value.number = result;
+
+ return r;
+}
+
+static gint
+func_check_field_test (EBookBackendSqliteDB *ebsdb,
+ const gchar *query_name,
+ const gchar *query_value)
+{
+ gint i;
+ gint ret_val = 0;
+
+ if (ebsdb) {
+ for (i = 0; i < ebsdb->priv->n_summary_fields; i++) {
+ if (!g_ascii_strcasecmp (e_contact_field_name (ebsdb->priv->summary_fields[i].field), query_name)) {
+ ret_val |= CHECK_IS_SUMMARY;
+
+ if (ebsdb->priv->summary_fields[i].type == E_TYPE_CONTACT_ATTR_LIST)
+ ret_val |= CHECK_IS_LIST_ATTR;
+
+ break;
+ }
+ }
+ } else {
+ for (i = 0; i < G_N_ELEMENTS (default_summary_fields); i++) {
+ if (!g_ascii_strcasecmp (e_contact_field_name (default_summary_fields[i]), query_name)) {
+ ret_val |= CHECK_IS_SUMMARY;
+
+ if (e_contact_field_type (default_summary_fields[i]) == E_TYPE_CONTACT_ATTR_LIST)
+ ret_val |= CHECK_IS_LIST_ATTR;
+
+ break;
+ }
+ }
+ }
+
+ return ret_val;
+}
+
+static ESExpResult *
+func_check (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ EBookBackendSqliteDB *ebsdb = data;
+ ESExpResult *r;
+ gint ret_val = 0;
+
+ if (argc == 2
+ && argv[0]->type == ESEXP_RES_STRING
+ && argv[1]->type == ESEXP_RES_STRING) {
+ const gchar *query_name = argv[0]->value.string;
+ const gchar *query_value = argv[1]->value.string;
+
+ /* Special case, when testing the special symbolic 'any field' we can
+ * consider it a summary query (it's similar to a 'no query'). */
+ if (g_strcmp0 (query_name, "x-evolution-any-field") == 0 &&
+ g_strcmp0 (query_value, "") == 0) {
+ ret_val |= CHECK_IS_SUMMARY;
+ goto check_finish;
+ }
+
+ ret_val |= func_check_field_test (ebsdb, query_name, query_value);
+ } else if (argc == 3
+ && argv[0]->type == ESEXP_RES_STRING
+ && argv[1]->type == ESEXP_RES_STRING
+ && argv[2]->type == ESEXP_RES_STRING) {
+ const gchar *query_name = argv[0]->value.string;
+ const gchar *query_value = argv[1]->value.string;
+ ret_val |= func_check_field_test (ebsdb, query_name, query_value);
+ }
+
+ check_finish:
+ r = e_sexp_result_new (f, ESEXP_RES_INT);
+ r->value.number = ret_val;
+
+ return r;
+}
+
+static ESExpResult *
+func_check_phone (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ ESExpResult *const r = func_check (f, argc, argv, data);
+ const gchar *const query_value = argv[1]->value.string;
+ EPhoneNumber *number;
+
+ /* Here we need to catch unsupported queries and invalid queries
+ * so we perform validity checks even if func_check() reports this
+ * as not a part of the summary.
+ */
+ if (!e_phone_number_is_supported ()) {
+ r->value.number |= CHECK_UNSUPPORTED;
+ return r;
+ }
+
+ number = e_phone_number_from_string (query_value, NULL, NULL);
+
+ if (number == NULL) {
+ /* Could not construct a phone number from the query input,
+ * an invalid query error will be propagated to the client.
+ */
+ r->value.number |= CHECK_INVALID;
+ } else {
+ e_phone_number_free (number);
+ }
+
+ return r;
+}
+
+static ESExpResult *
+func_check_regex_raw (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ /* Raw REGEX queries are not in the summary, we only keep
+ * normalized data in the summary
+ */
+ ESExpResult *r;
+
+ r = e_sexp_result_new (f, ESEXP_RES_INT);
+ r->value.number = 0;
+
+ return r;
+}
+
+/* 'builtin' functions */
+static const struct {
+ const gchar *name;
+ ESExpFunc *func;
+ gint type; /* set to 1 if a function can perform shortcut evaluation, or
+ doesn't execute everything, 0 otherwise */
+} check_symbols[] = {
+ { "and", (ESExpFunc *) func_check_subset, 1},
+ { "or", (ESExpFunc *) func_check_subset, 1},
+
+ { "contains", func_check, 0 },
+ { "is", func_check, 0 },
+ { "beginswith", func_check, 0 },
+ { "endswith", func_check, 0 },
+ { "exists", func_check, 0 },
+ { "eqphone", func_check_phone, 0 },
+ { "eqphone_national", func_check_phone, 0 },
+ { "eqphone_short", func_check_phone, 0 },
+ { "regex_normal", func_check, 0 },
+ { "regex_raw", func_check_regex_raw, 0 },
+};
+
+static gboolean
+e_book_backend_sqlitedb_check_summary_query_locked (EBookBackendSqliteDB *ebsdb,
+ const gchar *query,
+ gboolean *with_list_attrs,
+ gboolean *unsupported_query,
+ gboolean *invalid_query)
+{
+ ESExp *sexp;
+ ESExpResult *r;
+ gboolean retval = FALSE;
+ gint i;
+ gint esexp_error;
+
+ g_return_val_if_fail (query != NULL, FALSE);
+ g_return_val_if_fail (*query != '\0', FALSE);
+
+ sexp = e_sexp_new ();
+
+ for (i = 0; i < G_N_ELEMENTS (check_symbols); i++) {
+ if (check_symbols[i].type == 1) {
+ e_sexp_add_ifunction (
+ sexp, 0, check_symbols[i].name,
+ (ESExpIFunc *) check_symbols[i].func, ebsdb);
+ } else {
+ e_sexp_add_function (
+ sexp, 0, check_symbols[i].name,
+ check_symbols[i].func, ebsdb);
+ }
+ }
+
+ e_sexp_input_text (sexp, query, strlen (query));
+ esexp_error = e_sexp_parse (sexp);
+
+ if (esexp_error == -1) {
+ if (invalid_query)
+ *invalid_query = TRUE;
+
+ return FALSE;
+ }
+
+ r = e_sexp_eval (sexp);
+ if (r && r->type == ESEXP_RES_INT) {
+ retval = (r->value.number & CHECK_IS_SUMMARY) != 0;
+
+ if (with_list_attrs)
+ *with_list_attrs = (r->value.number & CHECK_IS_LIST_ATTR) != 0;
+
+ if (unsupported_query)
+ *unsupported_query = (r->value.number & CHECK_UNSUPPORTED) != 0;
+
+ if (invalid_query)
+ *invalid_query = (r->value.number & CHECK_INVALID) != 0;
+ }
+
+ e_sexp_result_free (sexp, r);
+ g_object_unref (sexp);
+
+ return retval;
+}
+
+/**
+ * e_book_backend_sqlitedb_check_summary_query:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @query: the query to check
+ * @with_list_attrs: Return location to store whether the query touches upon list attributes
+ *
+ * Checks whether @query contains only checks for the summary fields
+ * configured in @ebsdb
+ *
+ * Returns: %TRUE if @query contains only fields configured in the summary.
+ *
+ * Since: 3.8
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_check_summary_query (EBookBackendSqliteDB *ebsdb,
+ const gchar *query,
+ gboolean *with_list_attrs)
+{
+ gboolean is_summary;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+ is_summary = e_book_backend_sqlitedb_check_summary_query_locked (ebsdb, query, with_list_attrs, NULL, NULL);
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return is_summary;
+}
+
+/**
+ * e_book_backend_sqlitedb_is_summary_query:
+ * @query: the query to check
+ *
+ * Checks whether the query contains only checks for the default summary fields
+ *
+ * Returns: %TRUE if @query contains only fields configured in the summary.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.8: Use e_book_backend_sqlitedb_check_summary_query() instead
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_is_summary_query (const gchar *query)
+{
+ return e_book_backend_sqlitedb_check_summary_query_locked (NULL, query, NULL, NULL, NULL);
+}
+
+static ESExpResult *
+func_and (ESExp *f,
+ gint argc,
+ struct _ESExpTerm **argv,
+ gpointer data)
+{
+ ESExpResult *r, *r1;
+ GString *string;
+ gint i;
+
+ string = g_string_new ("( ");
+ for (i = 0; i < argc; i++) {
+ r1 = e_sexp_term_eval (f, argv[i]);
+
+ if (r1->type != ESEXP_RES_STRING) {
+ e_sexp_result_free (f, r1);
+ continue;
+ }
+ if (r1->value.string && *r1->value.string)
+ g_string_append_printf (string, "%s%s", r1->value.string, ((argc > 1) && (i != argc - 1)) ? " AND ":"");
+ e_sexp_result_free (f, r1);
+ }
+ g_string_append (string, " )");
+ r = e_sexp_result_new (f, ESEXP_RES_STRING);
+
+ if (strlen (string->str) == 4) {
+ r->value.string = g_strdup ("");
+ g_string_free (string, TRUE);
+ } else {
+ r->value.string = g_string_free (string, FALSE);
+ }
+
+ return r;
+}
+
+static ESExpResult *
+func_or (ESExp *f,
+ gint argc,
+ struct _ESExpTerm **argv,
+ gpointer data)
+{
+ ESExpResult *r, *r1;
+ GString *string;
+ gint i;
+
+ string = g_string_new ("( ");
+ for (i = 0; i < argc; i++) {
+ r1 = e_sexp_term_eval (f, argv[i]);
+
+ if (r1->type != ESEXP_RES_STRING) {
+ e_sexp_result_free (f, r1);
+ continue;
+ }
+ if (r1->value.string && *r1->value.string)
+ g_string_append_printf (string, "%s%s", r1->value.string, ((argc > 1) && (i != argc - 1)) ? " OR ":"");
+ e_sexp_result_free (f, r1);
+ }
+ g_string_append (string, " )");
+
+ r = e_sexp_result_new (f, ESEXP_RES_STRING);
+ if (strlen (string->str) == 4) {
+ r->value.string = g_strdup ("");
+ g_string_free (string, TRUE);
+ } else {
+ r->value.string = g_string_free (string, FALSE);
+ }
+
+ return r;
+}
+
+typedef enum {
+ MATCH_CONTAINS,
+ MATCH_IS,
+ MATCH_BEGINS_WITH,
+ MATCH_ENDS_WITH,
+ MATCH_PHONE_NUMBER,
+ MATCH_NATIONAL_PHONE_NUMBER,
+ MATCH_SHORT_PHONE_NUMBER,
+ MATCH_REGEX
+} MatchType;
+
+typedef enum {
+ CONVERT_NOTHING = 0,
+ CONVERT_NORMALIZE = (1 << 0),
+ CONVERT_REVERSE = (1 << 1),
+ CONVERT_PHONE = (1 << 2)
+} ConvertFlags;
+
+static gchar *
+extract_digits (const gchar *normal)
+{
+ gchar *digits = g_new (char, strlen (normal) + 1);
+ const gchar *src = normal;
+ gchar *dst = digits;
+
+ /* extract digits also considering eastern arabic numerals */
+ for (src = normal; *src; src = g_utf8_next_char (src)) {
+ const gunichar uc = g_utf8_get_char_validated (src, -1);
+ const gint value = g_unichar_digit_value (uc);
+
+ if (uc == -1)
+ break;
+
+ if (value != -1)
+ *dst++ = '0' + value;
+ }
+
+ *dst = '\0';
+
+ return digits;
+}
+
+static gchar *
+convert_string_value (EBookBackendSqliteDB *ebsdb,
+ const gchar *value,
+ const gchar *region,
+ ConvertFlags flags,
+ MatchType match)
+{
+ GString *str;
+ size_t len;
+ gchar c;
+ gboolean escape_modifier_needed = FALSE;
+ const gchar *escape_modifier = " ESCAPE '^'";
+ gchar *computed = NULL;
+ gchar *normal;
+ const gchar *ptr;
+
+ g_return_val_if_fail (value != NULL, NULL);
+
+ if ((flags & CONVERT_NORMALIZE) && match != MATCH_REGEX)
+ normal = e_util_utf8_normalize (value);
+ else
+ normal = g_strdup (value);
+
+ /* Just assume each character must be escaped. The result of this function
+ * is discarded shortly after calling this function. Therefore it's
+ * acceptable to possibly allocate twice the memory needed.
+ */
+ len = strlen (normal);
+ str = g_string_sized_new (2 * len + 4 + strlen (escape_modifier) - 1);
+ g_string_append_c (str, '\'');
+
+ switch (match) {
+ case MATCH_CONTAINS:
+ case MATCH_ENDS_WITH:
+ case MATCH_SHORT_PHONE_NUMBER:
+ g_string_append_c (str, '%');
+ break;
+
+ case MATCH_BEGINS_WITH:
+ case MATCH_IS:
+ case MATCH_PHONE_NUMBER:
+ case MATCH_NATIONAL_PHONE_NUMBER:
+ case MATCH_REGEX:
+ break;
+ }
+
+ if (flags & CONVERT_REVERSE) {
+ computed = g_utf8_strreverse (normal, -1);
+ ptr = computed;
+ } else if (flags & CONVERT_PHONE) {
+ computed = convert_phone (normal, region);
+ ptr = computed;
+ } else {
+ ptr = normal;
+ }
+
+ while (ptr && (c = *ptr++)) {
+ if (c == '\'') {
+ g_string_append_c (str, '\'');
+ } else if ((c == '%' || c == '^') && match != MATCH_REGEX) {
+ g_string_append_c (str, '^');
+ escape_modifier_needed = TRUE;
+ }
+
+ g_string_append_c (str, c);
+ }
+
+ switch (match) {
+ case MATCH_CONTAINS:
+ case MATCH_BEGINS_WITH:
+ g_string_append_c (str, '%');
+ break;
+
+ case MATCH_ENDS_WITH:
+ case MATCH_IS:
+ case MATCH_PHONE_NUMBER:
+ case MATCH_NATIONAL_PHONE_NUMBER:
+ case MATCH_SHORT_PHONE_NUMBER:
+ case MATCH_REGEX:
+ break;
+ }
+
+ g_string_append_c (str, '\'');
+
+ if (escape_modifier_needed)
+ g_string_append (str, escape_modifier);
+
+ g_free (computed);
+ g_free (normal);
+
+ return g_string_free (str, FALSE);
+}
+
+static gchar *
+field_name_and_query_term (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *field_name_input,
+ const gchar *query_term_input,
+ const gchar *region,
+ MatchType match,
+ gboolean *is_list_attr,
+ gchar **query_term,
+ gchar **extra_term)
+{
+ gint summary_index;
+ gchar *field_name = NULL;
+ gchar *value = NULL;
+ gchar *extra = NULL;
+ gboolean list_attr = FALSE;
+
+ summary_index = summary_index_from_field_name (ebsdb, field_name_input);
+
+ if (summary_index < 0) {
+ g_critical ("Only summary field matches should be converted to sql queries");
+ field_name = g_strconcat (folderid, ".", field_name_input, NULL);
+ value = convert_string_value (
+ ebsdb, query_term_input, region,
+ CONVERT_NORMALIZE, match);
+ } else {
+ gboolean suffix_search = FALSE;
+ gboolean phone_search = FALSE;
+
+ /* If its a suffix search and we have reverse data to search... */
+ if (match == MATCH_ENDS_WITH &&
+ (ebsdb->priv->summary_fields[summary_index].index & INDEX_SUFFIX) != 0)
+ suffix_search = TRUE;
+
+ /* If its a phone-number search and we have E.164 data to search... */
+ else if ((match == MATCH_PHONE_NUMBER ||
+ match == MATCH_NATIONAL_PHONE_NUMBER ||
+ match == MATCH_SHORT_PHONE_NUMBER) &&
+ (ebsdb->priv->summary_fields[summary_index].index & INDEX_PHONE) != 0)
+ phone_search = TRUE;
+
+ /* Or also if its an exact match, and we *only* have reverse data which is indexed,
+ * then prefer the indexed reverse search. */
+ else if (match == MATCH_IS &&
+ (ebsdb->priv->summary_fields[summary_index].index & INDEX_SUFFIX) != 0 &&
+ (ebsdb->priv->summary_fields[summary_index].index & INDEX_PREFIX) == 0)
+ suffix_search = TRUE;
+
+ if (suffix_search) {
+ /* Special case for suffix matching:
+ * o Reverse the string
+ * o Check the reversed column instead
+ * o Make it a prefix search
+ */
+ if (ebsdb->priv->summary_fields[summary_index].type == E_TYPE_CONTACT_ATTR_LIST) {
+ field_name = g_strdup ("multi.value_reverse");
+ list_attr = TRUE;
+ } else
+ field_name = g_strconcat (
+ "summary.",
+ ebsdb->priv->summary_fields[summary_index].dbname,
+ "_reverse", NULL);
+
+ if (ebsdb->priv->summary_fields[summary_index].field == E_CONTACT_UID ||
+ ebsdb->priv->summary_fields[summary_index].field == E_CONTACT_REV)
+ value = convert_string_value (
+ ebsdb, query_term_input, region, CONVERT_REVERSE,
+ (match == MATCH_ENDS_WITH) ? MATCH_BEGINS_WITH : MATCH_IS);
+ else
+ value = convert_string_value (
+ ebsdb, query_term_input, region,
+ CONVERT_REVERSE | CONVERT_NORMALIZE,
+ (match == MATCH_ENDS_WITH) ? MATCH_BEGINS_WITH : MATCH_IS);
+ } else if (phone_search) {
+ /* Special case for E.164 matching:
+ * o Normalize the string
+ * o Check the E.164 column instead
+ */
+ const gint country_code = e_phone_number_get_country_code_for_region (region, NULL);
+
+ if (ebsdb->priv->summary_fields[summary_index].type == E_TYPE_CONTACT_ATTR_LIST) {
+ field_name = g_strdup ("multi.value_phone");
+ list_attr = TRUE;
+ } else {
+ field_name = g_strdup_printf (
+ "summary.%s_phone",
+ ebsdb->priv->summary_fields[summary_index].dbname);
+ }
+
+ if (match == MATCH_PHONE_NUMBER) {
+ value = convert_string_value (
+ ebsdb, query_term_input, region,
+ CONVERT_NORMALIZE | CONVERT_PHONE, match);
+
+ extra = sqlite3_mprintf (" COLLATE ixphone_%d", country_code);
+ } else {
+ if (match == MATCH_NATIONAL_PHONE_NUMBER) {
+ value = convert_string_value (
+ ebsdb, query_term_input, region,
+ CONVERT_PHONE, MATCH_NATIONAL_PHONE_NUMBER);
+
+ extra = sqlite3_mprintf (" COLLATE ixphone_national");
+ } else {
+ gchar *const digits = extract_digits (query_term_input);
+ value = convert_string_value (
+ ebsdb, digits, region,
+ CONVERT_NOTHING, MATCH_ENDS_WITH);
+ g_free (digits);
+
+ extra = sqlite3_mprintf (
+ " AND (%q LIKE '|%%' OR %q LIKE '+%d|%%')",
+ field_name, field_name, country_code);
+ }
+
+ }
+ } else {
+ if (ebsdb->priv->summary_fields[summary_index].type == E_TYPE_CONTACT_ATTR_LIST) {
+ field_name = g_strdup ("multi.value");
+ list_attr = TRUE;
+ } else
+ field_name = g_strconcat (
+ "summary.",
+ ebsdb->priv->summary_fields[summary_index].dbname, NULL);
+
+ if (ebsdb->priv->summary_fields[summary_index].field == E_CONTACT_UID ||
+ ebsdb->priv->summary_fields[summary_index].field == E_CONTACT_REV) {
+ value = convert_string_value (
+ ebsdb, query_term_input, region,
+ CONVERT_NOTHING, match);
+ } else {
+ value = convert_string_value (
+ ebsdb, query_term_input, region,
+ CONVERT_NORMALIZE, match);
+ }
+ }
+ }
+
+ if (is_list_attr)
+ *is_list_attr = list_attr;
+
+ *query_term = value;
+
+ if (extra_term)
+ *extra_term = extra;
+
+ return field_name;
+}
+
+typedef struct {
+ EBookBackendSqliteDB *ebsdb;
+ const gchar *folderid;
+} BuildQueryData;
+
+static const gchar *
+field_oper (MatchType match)
+{
+ switch (match) {
+ case MATCH_IS:
+ case MATCH_PHONE_NUMBER:
+ case MATCH_NATIONAL_PHONE_NUMBER:
+ return "=";
+
+ case MATCH_REGEX:
+ return "REGEXP";
+
+ case MATCH_CONTAINS:
+ case MATCH_BEGINS_WITH:
+ case MATCH_ENDS_WITH:
+ case MATCH_SHORT_PHONE_NUMBER:
+ break;
+ }
+
+ return "LIKE";
+}
+
+static ESExpResult *
+convert_match_exp (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data,
+ MatchType match)
+{
+ BuildQueryData *qdata = (BuildQueryData *) data;
+ EBookBackendSqliteDB *ebsdb = qdata->ebsdb;
+ ESExpResult *r;
+ gchar *str = NULL;
+
+ /* are we inside a match-all? */
+ if (argc > 1 && argv[0]->type == ESEXP_RES_STRING) {
+ const gchar *field;
+
+ /* only a subset of headers are supported .. */
+ field = argv[0]->value.string;
+
+ if (argv[1]->type == ESEXP_RES_STRING && argv[1]->value.string[0] != 0) {
+ const gchar *const oper = field_oper (match);
+ gchar *field_name, *query_term, *extra_term;
+
+ if (!g_ascii_strcasecmp (field, "full_name")) {
+ GString *names = g_string_new (NULL);
+
+ field_name = field_name_and_query_term (
+ ebsdb, qdata->folderid, "full_name",
+ argv[1]->value.string, NULL,
+ match, NULL, &query_term, NULL);
+ g_string_append_printf (
+ names, "(%s IS NOT NULL AND %s %s %s)",
+ field_name, field_name, oper, query_term);
+ g_free (field_name);
+ g_free (query_term);
+
+ if (summary_dbname_from_field (ebsdb, E_CONTACT_FAMILY_NAME)) {
+ field_name = field_name_and_query_term (
+ ebsdb, qdata->folderid, "family_name",
+ argv[1]->value.string, NULL,
+ match, NULL, &query_term, NULL);
+ g_string_append_printf (
+ names, " OR (%s IS NOT NULL AND %s %s %s)",
+ field_name, field_name, oper, query_term);
+ g_free (field_name);
+ g_free (query_term);
+ }
+
+ if (summary_dbname_from_field (ebsdb, E_CONTACT_GIVEN_NAME)) {
+ field_name = field_name_and_query_term (
+ ebsdb, qdata->folderid, "given_name",
+ argv[1]->value.string, NULL,
+ match, NULL, &query_term, NULL);
+ g_string_append_printf (
+ names, " OR (%s IS NOT NULL AND %s %s %s)",
+ field_name, field_name, oper, query_term);
+ g_free (field_name);
+ g_free (query_term);
+ }
+
+ if (summary_dbname_from_field (ebsdb, E_CONTACT_NICKNAME)) {
+ field_name = field_name_and_query_term (
+ ebsdb, qdata->folderid, "nickname",
+ argv[1]->value.string, NULL,
+ match, NULL, &query_term, NULL);
+ g_string_append_printf (
+ names, " OR (%s IS NOT NULL AND %s %s %s)",
+ field_name, field_name, oper, query_term);
+ g_free (field_name);
+ g_free (query_term);
+ }
+
+ str = names->str;
+ g_string_free (names, FALSE);
+
+ } else {
+ const gchar *const region =
+ argc > 2 && argv[2]->type == ESEXP_RES_STRING ?
+ argv[2]->value.string : NULL;
+
+ gboolean is_list = FALSE;
+
+ /* This should ideally be the only valid case from all the above special casing, but oh well... */
+ field_name = field_name_and_query_term (
+ ebsdb, qdata->folderid, field,
+ argv[1]->value.string, region,
+ match, &is_list, &query_term, &extra_term);
+
+ /* User functions like eqphone_national() cannot utilize indexes. Therefore we
+ * should reduce the result set first before applying any user functions. This
+ * is done by applying a seemingly redundant suffix match first.
+ */
+ if (is_list) {
+ gchar *tmp;
+
+ tmp = sqlite3_mprintf ("multi.field = %Q", field);
+ str = g_strdup_printf (
+ "(%s AND (%s %s %s%s))",
+ tmp, field_name, oper, query_term,
+ extra_term ? extra_term : "");
+ sqlite3_free (tmp);
+ } else
+ str = g_strdup_printf (
+ "(%s IS NOT NULL AND (%s %s %s%s))",
+ field_name, field_name, oper, query_term,
+ extra_term ? extra_term : "");
+
+ g_free (field_name);
+ g_free (query_term);
+
+ sqlite3_free (extra_term);
+ }
+ }
+ }
+
+ r = e_sexp_result_new (f, ESEXP_RES_STRING);
+ r->value.string = str;
+
+ return r;
+}
+
+static ESExpResult *
+func_contains (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ return convert_match_exp (f, argc, argv, data, MATCH_CONTAINS);
+}
+
+static ESExpResult *
+func_is (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ return convert_match_exp (f, argc, argv, data, MATCH_IS);
+}
+
+static ESExpResult *
+func_beginswith (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ return convert_match_exp (f, argc, argv, data, MATCH_BEGINS_WITH);
+}
+
+static ESExpResult *
+func_endswith (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ return convert_match_exp (f, argc, argv, data, MATCH_ENDS_WITH);
+}
+
+static ESExpResult *
+func_eqphone (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ return convert_match_exp (f, argc, argv, data, MATCH_PHONE_NUMBER);
+}
+
+static ESExpResult *
+func_eqphone_national (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ return convert_match_exp (f, argc, argv, data, MATCH_NATIONAL_PHONE_NUMBER);
+}
+
+static ESExpResult *
+func_eqphone_short (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ return convert_match_exp (f, argc, argv, data, MATCH_SHORT_PHONE_NUMBER);
+}
+
+static ESExpResult *
+func_regex (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ return convert_match_exp (f, argc, argv, data, MATCH_REGEX);
+}
+
+/* 'builtin' functions */
+static struct {
+ const gchar *name;
+ ESExpFunc *func;
+ guint immediate :1;
+} symbols[] = {
+ { "and", (ESExpFunc *) func_and, 1},
+ { "or", (ESExpFunc *) func_or, 1},
+
+ { "contains", func_contains, 0 },
+ { "is", func_is, 0 },
+ { "beginswith", func_beginswith, 0 },
+ { "endswith", func_endswith, 0 },
+ { "eqphone", func_eqphone, 0 },
+ { "eqphone_national", func_eqphone_national, 0 },
+ { "eqphone_short", func_eqphone_short, 0 },
+ { "regex_normal", func_regex, 0 }
+};
+
+static gchar *
+sexp_to_sql_query (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *query)
+{
+ BuildQueryData data = { ebsdb, folderid };
+ ESExp *sexp;
+ ESExpResult *r;
+ gint i;
+ gchar *res;
+
+ sexp = e_sexp_new ();
+
+ for (i = 0; i < G_N_ELEMENTS (symbols); i++) {
+ if (symbols[i].immediate)
+ e_sexp_add_ifunction (
+ sexp, 0, symbols[i].name,
+ (ESExpIFunc *) symbols[i].func, &data);
+ else
+ e_sexp_add_function (
+ sexp, 0, symbols[i].name,
+ symbols[i].func, &data);
+ }
+
+ e_sexp_input_text (sexp, query, strlen (query));
+
+ if (e_sexp_parse (sexp) == -1) {
+ g_object_unref (sexp);
+ return NULL;
+ }
+
+ r = e_sexp_eval (sexp);
+ if (!r) {
+ g_object_unref (sexp);
+ return NULL;
+ }
+
+ if (r->type == ESEXP_RES_STRING) {
+ if (r->value.string && *r->value.string)
+ res = g_strdup (r->value.string);
+ else
+ res = NULL;
+ } else {
+ g_warn_if_reached ();
+ res = NULL;
+ }
+
+ e_sexp_result_free (sexp, r);
+ g_object_unref (sexp);
+
+ return res;
+}
+
+static EbSdbSearchData *
+search_data_from_results (gchar **cols)
+{
+ EbSdbSearchData *data = g_slice_new0 (EbSdbSearchData);
+
+ if (cols[0])
+ data->uid = g_strdup (cols[0]);
+
+ if (cols[1])
+ data->vcard = g_strdup (cols[1]);
+
+ if (cols[2])
+ data->bdata = g_strdup (cols[2]);
+
+ return data;
+}
+
+static gint
+addto_vcard_list_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ GSList **vcard_data = ref;
+ EbSdbSearchData *s_data;
+
+ s_data = search_data_from_results (cols);
+
+ *vcard_data = g_slist_prepend (*vcard_data, s_data);
+
+ return 0;
+}
+
+static gint
+addto_slist_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ GSList **uids = ref;
+
+ if (cols[0])
+ *uids = g_slist_prepend (*uids, g_strdup (cols [0]));
+
+ return 0;
+}
+
+static GSList *
+book_backend_sqlitedb_search_query (EBookBackendSqliteDB *ebsdb,
+ const gchar *sql,
+ const gchar *folderid,
+ GHashTable *fields_of_interest,
+ gboolean *with_all_required_fields,
+ gboolean query_with_list_attrs,
+ GError **error)
+{
+ GSList *vcard_data = NULL;
+ gchar *stmt;
+ gboolean local_with_all_required_fields = FALSE;
+ gboolean success = TRUE;
+
+ /* Try constructing contacts from only UID/REV first if that's requested */
+ if (uid_rev_fields (fields_of_interest)) {
+ gchar *select_portion;
+
+ select_portion = summary_select_stmt (
+ fields_of_interest, query_with_list_attrs);
+
+ if (sql && sql[0]) {
+
+ if (query_with_list_attrs) {
+ gchar *list_table = g_strconcat (folderid, "_lists", NULL);
+
+ stmt = sqlite3_mprintf (
+ "%s FROM %Q AS summary "
+ "LEFT OUTER JOIN %Q AS multi ON summary.uid = multi.uid WHERE %s",
+ select_portion, folderid, list_table, sql);
+ g_free (list_table);
+ } else {
+ stmt = sqlite3_mprintf (
+ "%s FROM %Q AS summary WHERE %s",
+ select_portion, folderid, sql);
+ }
+
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt,
+ store_data_to_vcard, &vcard_data, error);
+
+ sqlite3_free (stmt);
+ } else {
+ stmt = sqlite3_mprintf ("%s FROM %Q AS summary", select_portion, folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt,
+ store_data_to_vcard, &vcard_data, error);
+ sqlite3_free (stmt);
+ }
+
+ local_with_all_required_fields = TRUE;
+ g_free (select_portion);
+
+ } else if (ebsdb->priv->store_vcard) {
+
+ if (sql && sql[0]) {
+
+ if (query_with_list_attrs) {
+ gchar *list_table = g_strconcat (folderid, "_lists", NULL);
+
+ stmt = sqlite3_mprintf (
+ "SELECT DISTINCT summary.uid, vcard, bdata FROM %Q AS summary "
+ "LEFT OUTER JOIN %Q AS multi ON summary.uid = multi.uid WHERE %s",
+ folderid, list_table, sql);
+ g_free (list_table);
+ } else {
+ stmt = sqlite3_mprintf (
+ "SELECT uid, vcard, bdata FROM %Q as summary WHERE %s", folderid, sql);
+ }
+
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt,
+ addto_vcard_list_cb, &vcard_data, error);
+
+ sqlite3_free (stmt);
+ } else {
+ stmt = sqlite3_mprintf (
+ "SELECT uid, vcard, bdata FROM %Q", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt,
+ addto_vcard_list_cb , &vcard_data, error);
+ sqlite3_free (stmt);
+ }
+
+ local_with_all_required_fields = TRUE;
+ } else {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
+ _("Full search_contacts are not stored in cache. vcards cannot be returned."));
+ }
+
+ if (!success) {
+ g_warn_if_fail (vcard_data == NULL);
+ return NULL;
+ }
+
+ if (with_all_required_fields)
+ *with_all_required_fields = local_with_all_required_fields;
+
+ return g_slist_reverse (vcard_data);
+}
+
+static GSList *
+book_backend_sqlitedb_search_full (EBookBackendSqliteDB *ebsdb,
+ const gchar *sexp,
+ const gchar *folderid,
+ gboolean return_uids,
+ GError **error)
+{
+ GSList *r_list = NULL, *all = NULL, *l;
+ EBookBackendSExp *bsexp = NULL;
+ gboolean success;
+ gchar *stmt;
+
+ stmt = sqlite3_mprintf ("SELECT uid, vcard, bdata FROM %Q", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, addto_vcard_list_cb , &all, error);
+ sqlite3_free (stmt);
+
+ if (!success) {
+ g_warn_if_fail (all == NULL);
+ return NULL;
+ }
+
+ bsexp = e_book_backend_sexp_new (sexp);
+
+ for (l = all; l != NULL; l = g_slist_next (l)) {
+ EbSdbSearchData *s_data = (EbSdbSearchData *) l->data;
+
+ if (e_book_backend_sexp_match_vcard (bsexp, s_data->vcard)) {
+ if (!return_uids)
+ r_list = g_slist_prepend (r_list, s_data);
+ else {
+ r_list = g_slist_prepend (r_list, g_strdup (s_data->uid));
+ e_book_backend_sqlitedb_search_data_free (s_data);
+ }
+ } else
+ e_book_backend_sqlitedb_search_data_free (s_data);
+ }
+
+ g_object_unref (bsexp);
+
+ g_slist_free (all);
+
+ return r_list;
+}
+
+/**
+ * e_book_backend_sqlitedb_search:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @sexp: (allow-none): search expression; use %NULL or an empty string to get all stored contacts.
+ * @fields_of_interest: (allow-none): A #GHashTable indicating which fields should be
+ * included in the returned contacts
+ * @searched: (allow-none) (out): Whether @ebsdb was capable of searching
+ * for the provided query @sexp.
+ * @with_all_required_fields: (allow-none) (out): Whether all the required
+ * fields are present in the returned vcards.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Searching with summary fields is always supported. Search expressions
+ * containing any other field is supported only if backend chooses to store
+ * the vcard inside the db.
+ *
+ * If not configured otherwise, the default summary fields include:
+ * uid, rev, file_as, nickname, full_name, given_name, family_name,
+ * email, is_list, list_show_addresses, wants_html.
+ *
+ * Summary fields can be configured at addressbook creation time using
+ * the #ESourceBackendSummarySetup source extension.
+ *
+ * <note><para>The @fields_of_interest parameter is a legacy parameter which can
+ * be used to specify that #EContacts with only the %E_CONTACT_UID
+ * and %E_CONTACT_REV fields. The hash table must use g_str_hash()
+ * and g_str_equal() and the keys 'uid' and 'rev' must be present.</para></note>
+ *
+ * The returned list should be freed with g_slist_free()
+ * and all elements freed with e_book_backend_sqlitedb_search_data_free().
+ *
+ * Returns: (transfer full): A #GSList of #EbSdbSearchData structures.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+GSList *
+e_book_backend_sqlitedb_search (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *sexp,
+ GHashTable *fields_of_interest,
+ gboolean *searched,
+ gboolean *with_all_required_fields,
+ GError **error)
+{
+ GSList *search_contacts = NULL;
+ gboolean local_searched = FALSE;
+ gboolean local_with_all_required_fields = FALSE;
+ gboolean query_with_list_attrs = FALSE;
+ gboolean query_unsupported = FALSE;
+ gboolean query_invalid = FALSE;
+ gboolean summary_query = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+
+ if (sexp && !*sexp)
+ sexp = NULL;
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (sexp)
+ summary_query = e_book_backend_sqlitedb_check_summary_query_locked (
+ ebsdb, sexp,
+ &query_with_list_attrs,
+ &query_unsupported, &query_invalid);
+
+ if (query_unsupported)
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_NOT_SUPPORTED,
+ _("Query contained unsupported elements"));
+ else if (query_invalid)
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+ _("Invalid Query"));
+ else if (!sexp || summary_query) {
+ gchar *sql_query;
+
+ sql_query = sexp ? sexp_to_sql_query (ebsdb, folderid, sexp) : NULL;
+ search_contacts = book_backend_sqlitedb_search_query (
+ ebsdb, sql_query, folderid,
+ fields_of_interest,
+ &local_with_all_required_fields,
+ query_with_list_attrs, error);
+ g_free (sql_query);
+
+ local_searched = TRUE;
+
+ } else if (ebsdb->priv->store_vcard) {
+ search_contacts = book_backend_sqlitedb_search_full (
+ ebsdb, sexp, folderid, FALSE, error);
+
+ local_searched = TRUE;
+ local_with_all_required_fields = TRUE;
+
+ } else {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
+ _("Full search_contacts are not stored in cache. "
+ "Hence only summary query is supported."));
+ }
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (searched)
+ *searched = local_searched;
+ if (with_all_required_fields)
+ *with_all_required_fields = local_with_all_required_fields;
+
+ return search_contacts;
+}
+
+/**
+ * e_book_backend_sqlitedb_search_uids:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @sexp: (allow-none): search expression; use %NULL or an empty string to get all stored contacts.
+ * @searched: (allow-none) (out): Whether @ebsdb was capable of searching for the provided query @sexp.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Similar to e_book_backend_sqlitedb_search(), but returns only a list of contact UIDs.
+ *
+ * The returned list should be freed with g_slist_free()
+ * and all elements freed with g_free().
+ *
+ * Returns: (transfer full): A #GSList of allocated contact UID strings.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+GSList *
+e_book_backend_sqlitedb_search_uids (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *sexp,
+ gboolean *searched,
+ GError **error)
+{
+ GSList *uids = NULL;
+ gboolean local_searched = FALSE;
+ gboolean query_with_list_attrs = FALSE;
+ gboolean query_unsupported = FALSE;
+ gboolean summary_query = FALSE;
+ gboolean query_invalid = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+
+ if (sexp && !*sexp)
+ sexp = NULL;
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (sexp)
+ summary_query = e_book_backend_sqlitedb_check_summary_query_locked (
+ ebsdb, sexp,
+ &query_with_list_attrs,
+ &query_unsupported,
+ &query_invalid);
+
+ if (query_unsupported)
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_NOT_SUPPORTED,
+ _("Query contained unsupported elements"));
+ else if (query_invalid)
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+ _("Invalid query"));
+ else if (!sexp || summary_query) {
+ gchar *stmt;
+ gchar *sql_query = sexp ? sexp_to_sql_query (ebsdb, folderid, sexp) : NULL;
+
+ if (sql_query && sql_query[0]) {
+
+ if (query_with_list_attrs) {
+ gchar *list_table = g_strconcat (folderid, "_lists", NULL);
+
+ stmt = sqlite3_mprintf (
+ "SELECT DISTINCT summary.uid FROM %Q AS summary "
+ "LEFT OUTER JOIN %Q AS multi ON summary.uid = multi.uid WHERE %s",
+ folderid, list_table, sql_query);
+
+ g_free (list_table);
+ } else
+ stmt = sqlite3_mprintf (
+ "SELECT summary.uid FROM %Q AS summary WHERE %s",
+ folderid, sql_query);
+
+ book_backend_sql_exec (ebsdb->priv->db, stmt, addto_slist_cb, &uids, error);
+ sqlite3_free (stmt);
+
+ } else {
+ stmt = sqlite3_mprintf ("SELECT uid FROM %Q", folderid);
+ book_backend_sql_exec (ebsdb->priv->db, stmt, addto_slist_cb, &uids, error);
+ sqlite3_free (stmt);
+ }
+
+ local_searched = TRUE;
+
+ g_free (sql_query);
+
+ } else if (ebsdb->priv->store_vcard) {
+ uids = book_backend_sqlitedb_search_full (
+ ebsdb, sexp, folderid, TRUE, error);
+
+ local_searched = TRUE;
+
+ } else {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
+ _("Full vcards are not stored in cache. "
+ "Hence only summary query is supported."));
+ }
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (searched)
+ *searched = local_searched;
+
+ return uids;
+}
+
+static gint
+get_uids_and_rev_cb (gpointer user_data,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ GHashTable *uids_and_rev = user_data;
+
+ if (col == 2 && cols[0])
+ g_hash_table_insert (uids_and_rev, g_strdup (cols[0]), g_strdup (cols[1] ? cols[1] : ""));
+
+ return 0;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_uids_and_rev:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Gets hash table of all uids (key) and rev (value) pairs stored
+ * for each contact in the cache. The hash table should be freed
+ * with g_hash_table_destroy(), if not needed anymore. Each key
+ * and value is a newly allocated string.
+ *
+ * Returns: (transfer full): A #GHashTable of all contact revisions by UID.
+ *
+ * Since: 3.4
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+GHashTable *
+e_book_backend_sqlitedb_get_uids_and_rev (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error)
+{
+ GHashTable *uids_and_rev;
+ gchar *stmt;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+
+ uids_and_rev = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf ("SELECT uid,rev FROM %Q", folderid);
+ book_backend_sql_exec (
+ ebsdb->priv->db, stmt,
+ get_uids_and_rev_cb, uids_and_rev, error);
+ sqlite3_free (stmt);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return uids_and_rev;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_is_populated:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Checks whether the 'is populated' flag is set for @folderid in @ebsdb.
+ *
+ * <note><para>In order to differentiate an error from the flag simply being
+ * %FALSE for @ebsdb, you must pass the @error parameter and check whether
+ * it was set by this function.</para></note>
+ *
+ * Returns: Whether the 'is populated' flag is set.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_get_is_populated (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean ret = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf (
+ "SELECT is_populated FROM folders WHERE folder_id = %Q",
+ folderid);
+ book_backend_sql_exec (
+ ebsdb->priv->db, stmt, get_bool_cb , &ret, error);
+ sqlite3_free (stmt);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return ret;
+
+}
+
+/**
+ * e_book_backend_sqlitedb_set_is_populated:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @populated: The new value for the 'is populated' flag.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Sets the value of the 'is populated' flag for @folderid in @ebsdb.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_set_is_populated (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gboolean populated,
+ GError **error)
+{
+ gchar *stmt = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ stmt = sqlite3_mprintf (
+ "UPDATE folders SET is_populated = %d "
+ "WHERE folder_id = %Q", populated, folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ if (success)
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ else
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_revision:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @revision_out: (out) (transfer full): The location to return the current
+ * revision
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetches the current revision for the address-book indicated by @folderid.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.8
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+gboolean
+e_book_backend_sqlitedb_get_revision (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gchar **revision_out,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid && folderid[0], FALSE);
+ g_return_val_if_fail (revision_out != NULL && *revision_out == NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf (
+ "SELECT revision FROM folders WHERE folder_id = %Q", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, get_string_cb, revision_out, error);
+ sqlite3_free (stmt);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_set_revision:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @revision: The new revision
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Sets the current revision for the address-book indicated by @folderid to be @revision.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.8
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+gboolean
+e_book_backend_sqlitedb_set_revision (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *revision,
+ GError **error)
+{
+ gchar *stmt = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid && folderid[0], FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ stmt = sqlite3_mprintf (
+ "UPDATE folders SET revision = %Q "
+ "WHERE folder_id = %Q", revision, folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ if (success)
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ else
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_has_partial_content
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetches the 'partial content' flag from @folderid in @ebsdb.
+ *
+ * This flag is intended to indicate whether the stored vcards contain the full data or
+ * whether they were downloaded only partially.
+ *
+ * <note><para>In order to differentiate an error from the flag simply being
+ * %FALSE for @ebsdb, you must pass the @error parameter and check whether
+ * it was set by this function.</para></note>
+ *
+ * Returns: %TRUE if the vcards stored in the db were downloaded partially.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_get_has_partial_content (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean ret = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf (
+ "SELECT partial_content FROM folders "
+ "WHERE folder_id = %Q", folderid);
+ book_backend_sql_exec (
+ ebsdb->priv->db, stmt, get_bool_cb , &ret, error);
+ sqlite3_free (stmt);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return ret;
+}
+
+/**
+ * e_book_backend_sqlitedb_set_has_partial_content:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @partial_content: new value for the 'partial content' flag
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Sets the value of the 'partial content' flag in @folderid of @ebsdb.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_set_has_partial_content (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gboolean partial_content,
+ GError **error)
+{
+ gchar *stmt = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ stmt = sqlite3_mprintf (
+ "UPDATE folders SET partial_content = %d "
+ "WHERE folder_id = %Q", partial_content, folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ if (success)
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ else
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_contact_bdata:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @uid: The UID of the contact to fetch extra data for.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetches extra auxiliary data previously set for @uid.
+ *
+ * <note><para>In order to differentiate an error from the @uid simply
+ * not being present in @ebsdb, you must pass the @error parameter and
+ * check whether it was set by this function.</para></note>
+ *
+ * Returns: (transfer full): The extra data previously set for @uid, or %NULL
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gchar *
+e_book_backend_sqlitedb_get_contact_bdata (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ GError **error)
+{
+ gchar *stmt, *ret = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf (
+ "SELECT bdata FROM %Q WHERE uid = %Q", folderid, uid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, get_string_cb , &ret, error);
+ sqlite3_free (stmt);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!success) {
+ g_warn_if_fail (ret == NULL);
+ return NULL;
+ }
+
+ return ret;
+}
+
+/**
+ * e_book_backend_sqlitedb_set_contact_bdata:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @uid: The UID of the contact to fetch extra data for.
+ * @value: The auxiliary data to set for @uid in @folderid.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Sets the extra auxiliary data for the contact indicated by @uid.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_set_contact_bdata (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ const gchar *value,
+ GError **error)
+{
+ gchar *stmt = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (value != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ stmt = sqlite3_mprintf (
+ "UPDATE %Q SET bdata = %Q WHERE uid = %Q",
+ folderid, value, uid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ if (success)
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ else
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_sync_data:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetches data previously set with e_book_backend_sqlitedb_set_sync_data() for the given @folderid.
+ *
+ * Returns: (transfer full): The data previously set with e_book_backend_sqlitedb_set_sync_data().
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gchar *
+e_book_backend_sqlitedb_get_sync_data (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error)
+{
+ gchar *stmt, *ret = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf (
+ "SELECT sync_data FROM folders WHERE folder_id = %Q",
+ folderid);
+ book_backend_sql_exec (
+ ebsdb->priv->db, stmt, get_string_cb , &ret, error);
+ sqlite3_free (stmt);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return ret;
+}
+
+/**
+ * e_book_backend_sqlitedb_set_sync_data:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @sync_data: The data to set.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Sets some auxiliary data for the given @folderid in @ebsdb.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_set_sync_data (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *sync_data,
+ GError **error)
+{
+ gchar *stmt = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+ g_return_val_if_fail (sync_data != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ stmt = sqlite3_mprintf (
+ "UPDATE folders SET sync_data = %Q "
+ "WHERE folder_id = %Q", sync_data, folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ if (success)
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ else
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_key_value:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @key: the key to fetch a value for
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetches data previously set with e_book_backend_sqlitedb_set_key_value() for the
+ * given @key in @folderid.
+ *
+ * Returns: (transfer full): The data previously set with e_book_backend_sqlitedb_set_key_value().
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gchar *
+e_book_backend_sqlitedb_get_key_value (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *key,
+ GError **error)
+{
+ gchar *stmt, *ret = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf (
+ "SELECT value FROM keys WHERE folder_id = %Q AND key = %Q",
+ folderid, key);
+ book_backend_sql_exec (
+ ebsdb->priv->db, stmt, get_string_cb , &ret, error);
+ sqlite3_free (stmt);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return ret;
+}
+
+/**
+ * e_book_backend_sqlitedb_set_key_value:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @key: the key to fetch a value for
+ * @value: the value to story for @key in @folderid
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Sets the auxiliary data @value to be stored in relation to @key in @folderid.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_set_key_value (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *key,
+ const gchar *value,
+ GError **error)
+{
+ gchar *stmt = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+ g_return_val_if_fail (key != NULL, FALSE);
+ g_return_val_if_fail (value != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ stmt = sqlite3_mprintf (
+ "INSERT or REPLACE INTO keys (key, value, folder_id) "
+ "values (%Q, %Q, %Q)", key, value, folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ if (success)
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ else
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_partially_cached_ids:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Obsolete, do not use, this always ends with an error.
+ *
+ * Returns: %NULL
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+GSList *
+e_book_backend_sqlitedb_get_partially_cached_ids (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error)
+{
+ gchar *stmt;
+ GSList *uids = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid != NULL, NULL);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf (
+ "SELECT uid FROM %Q WHERE partial_content = 1",
+ folderid);
+ book_backend_sql_exec (
+ ebsdb->priv->db, stmt, addto_slist_cb, &uids, error);
+ sqlite3_free (stmt);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return uids;
+}
+
+/**
+ * e_book_backend_sqlitedb_delete_addressbook:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Deletes the addressbook indicated by @folderid in @ebsdb.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_sqlitedb_delete_addressbook (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid != NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ /* delete the contacts table */
+ stmt = sqlite3_mprintf ("DROP TABLE %Q ", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ if (!success)
+ goto rollback;
+
+ /* delete the key/value pairs corresponding to this table */
+ stmt = sqlite3_mprintf (
+ "DELETE FROM keys WHERE folder_id = %Q", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ if (!success)
+ goto rollback;
+
+ /* delete the folder from the folders table */
+ stmt = sqlite3_mprintf (
+ "DELETE FROM folders WHERE folder_id = %Q", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ if (!success)
+ goto rollback;
+
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+
+rollback:
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return FALSE;
+}
+
+/**
+ * e_book_backend_sqlitedb_search_data_free:
+ * @s_data: An #EbSdbSearchData
+ *
+ * Frees an #EbSdbSearchData
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+void
+e_book_backend_sqlitedb_search_data_free (EbSdbSearchData *s_data)
+{
+ if (s_data) {
+ g_free (s_data->uid);
+ g_free (s_data->vcard);
+ g_free (s_data->bdata);
+ g_slice_free (EbSdbSearchData, s_data);
+ }
+}
+
+/**
+ * e_book_backend_sqlitedb_remove:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Removes the entire @ebsdb from storage on disk.
+ *
+ * FIXME: it is unclear when it is safe to call this function,
+ * it should probably be deprecated.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.2
+ **/
+gboolean
+e_book_backend_sqlitedb_remove (EBookBackendSqliteDB *ebsdb,
+ GError **error)
+{
+ gchar *filename;
+ gint ret;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ sqlite3_close (ebsdb->priv->db);
+
+ filename = g_build_filename (ebsdb->priv->path, DB_FILENAME, NULL);
+ ret = g_unlink (filename);
+ g_free (filename);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (ret == -1) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
+ _("Unable to remove the db file: errno %d"), errno);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+upgrade_contacts_table (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *region,
+ const gchar *lc_collate,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success = FALSE;
+ GSList *vcard_data = NULL;
+ GSList *l;
+
+ stmt = sqlite3_mprintf ("SELECT uid, vcard, NULL FROM %Q", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, addto_vcard_list_cb, &vcard_data, error);
+ sqlite3_free (stmt);
+
+ for (l = vcard_data; success && l; l = l->next) {
+ EbSdbSearchData *const s_data = l->data;
+ EContact *contact = NULL;
+
+ /* It can be we're opening a light summary which was created without
+ * storing the vcards, such as was used in EDS versions 3.2 to 3.6.
+ *
+ * In this case we just want to skip the contacts we can't load
+ * and leave them as is in the SQLite, they will be added from
+ * the old BDB in the case of a migration anyway.
+ */
+ if (s_data->vcard)
+ contact = e_contact_new_from_vcard_with_uid (s_data->vcard, s_data->uid);
+
+ if (contact == NULL)
+ continue;
+
+ success = insert_contact (ebsdb, contact, folderid, TRUE, region, error);
+
+ g_object_unref (contact);
+ }
+
+ g_slist_free_full (vcard_data, destroy_search_data);
+
+ if (success) {
+
+ stmt = sqlite3_mprintf (
+ "UPDATE folders SET countrycode = %Q WHERE folder_id = %Q",
+ region, folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+
+ stmt = sqlite3_mprintf (
+ "UPDATE folders SET lc_collate = %Q WHERE folder_id = %Q",
+ lc_collate, folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, NULL, NULL, error);
+ sqlite3_free (stmt);
+ }
+
+ return success;
+}
+
+static gboolean
+sqlitedb_set_locale_internal (EBookBackendSqliteDB *ebsdb,
+ const gchar *locale,
+ GError **error)
+{
+ EBookBackendSqliteDBPrivate *priv = ebsdb->priv;
+ ECollator *collator;
+
+ if (g_strcmp0 (priv->locale, locale) != 0) {
+
+ collator = e_collator_new (locale, error);
+ if (!collator)
+ return FALSE;
+
+ g_free (priv->locale);
+ priv->locale = g_strdup (locale);
+
+ if (ebsdb->priv->collator)
+ e_collator_unref (ebsdb->priv->collator);
+
+ ebsdb->priv->collator = collator;
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_sqlitedb_set_locale:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @lc_collate: The new locale for the addressbook
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Relocalizes any locale specific data in the specified
+ * new @lc_collate locale.
+ *
+ * The @lc_collate locale setting is stored and remembered on
+ * subsequent accesses of the addressbook, changing the locale
+ * will store the new locale and will modify sort keys and any
+ * locale specific data in the addressbook.
+ *
+ * Returns: Whether the new locale was successfully set.
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+gboolean
+e_book_backend_sqlitedb_set_locale (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *lc_collate,
+ GError **error)
+{
+ gboolean success;
+ gchar *stmt;
+ gchar *stored_lc_collate;
+ gchar *current_region = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid && folderid[0], FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (e_phone_number_is_supported ()) {
+ current_region = e_phone_number_get_default_region (error);
+
+ if (current_region == NULL) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+ }
+
+ if (!sqlitedb_set_locale_internal (ebsdb, lc_collate, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ g_free (current_region);
+ return FALSE;
+ }
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ g_free (current_region);
+ return FALSE;
+ }
+
+ stmt = sqlite3_mprintf ("SELECT lc_collate FROM folders WHERE folder_id = %Q", folderid);
+ success = book_backend_sql_exec (ebsdb->priv->db, stmt, get_string_cb, &stored_lc_collate, error);
+ sqlite3_free (stmt);
+
+ if (success && g_strcmp0 (stored_lc_collate, lc_collate) != 0)
+ success = upgrade_contacts_table (ebsdb, folderid, current_region, lc_collate, error);
+
+ /* If for some reason we failed, then reset the collator to use the old locale */
+ if (!success)
+ sqlitedb_set_locale_internal (ebsdb, stored_lc_collate, NULL);
+
+ g_free (stored_lc_collate);
+ g_free (current_region);
+
+ if (!success)
+ goto rollback;
+
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+
+ rollback:
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return FALSE;
+}
+
+/**
+ * e_book_backend_sqlitedb_get_locale:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @locale_out: (out) (transfer full): The location to return the current locale
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetches the current locale setting for the address-book indicated by @folderid.
+ *
+ * Upon success, @lc_collate_out will hold the returned locale setting,
+ * otherwise %FALSE will be returned and @error will be updated accordingly.
+ *
+ * Returns: Whether the locale was successfully fetched.
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+gboolean
+e_book_backend_sqlitedb_get_locale (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gchar **locale_out,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (folderid && folderid[0], FALSE);
+ g_return_val_if_fail (locale_out != NULL && *locale_out == NULL, FALSE);
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ stmt = sqlite3_mprintf (
+ "SELECT lc_collate FROM folders WHERE folder_id = %Q", folderid);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, stmt, get_string_cb, locale_out, error);
+ sqlite3_free (stmt);
+
+ if (!sqlitedb_set_locale_internal (ebsdb, *locale_out, &local_error)) {
+ g_warning ("Error loading new locale: %s", local_error->message);
+ g_clear_error (&local_error);
+ }
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ return success;
+}
+
+/******************************************************************
+ * EbSdbCursor apis *
+ ******************************************************************/
+typedef struct _CursorState CursorState;
+
+struct _CursorState {
+ gchar **values; /* The current cursor position, results will be returned after this position */
+ gchar *last_uid; /* The current cursor contact UID position, used as a tie breaker */
+ EbSdbCursorOrigin position; /* The position is updated with the cursor state and is used to distinguish
+ * between the beginning and the ending of the cursor's contact list.
+ * While the cursor is in a non-null state, the position will be
+ * EBSDB_CURSOR_ORIGIN_CURRENT.
+ */
+};
+
+struct _EbSdbCursor {
+ gchar *folderid; /* The folderid for this cursor */
+
+ EBookBackendSExp *sexp; /* An EBookBackendSExp based on the query, used by e_book_backend_sqlitedb_cursor_compare () */
+ gchar *select_vcards; /* The first fragment when querying results */
+ gchar *select_count; /* The first fragment when querying contact counts */
+ gchar *query; /* The SQL query expression derived from the passed search expression */
+ gchar *order; /* The normal order SQL query fragment to append at the end, containing ORDER BY etc */
+ gchar *reverse_order; /* The reverse order SQL query fragment to append at the end, containing ORDER BY etc */
+
+ EContactField *sort_fields; /* The fields to sort in a query in the order or sort priority */
+ EBookCursorSortType *sort_types; /* The sort method to use for each field */
+ gint n_sort_fields; /* The amound of sort fields */
+
+ CursorState state;
+};
+
+static CursorState *cursor_state_copy (EbSdbCursor *cursor,
+ CursorState *state);
+static void cursor_state_free (EbSdbCursor *cursor,
+ CursorState *state);
+static void cursor_state_clear (EbSdbCursor *cursor,
+ CursorState *state,
+ EbSdbCursorOrigin position);
+static void cursor_state_set_from_contact (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ CursorState *state,
+ EContact *contact);
+static void cursor_state_set_from_vcard (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ CursorState *state,
+ const gchar *vcard);
+
+static CursorState *
+cursor_state_copy (EbSdbCursor *cursor,
+ CursorState *state)
+{
+ CursorState *copy;
+ gint i;
+
+ copy = g_slice_new0 (CursorState);
+ copy->values = g_new0 (gchar *, cursor->n_sort_fields);
+
+ for (i = 0; i < cursor->n_sort_fields; i++)
+ copy->values[i] = g_strdup (state->values[i]);
+
+ copy->last_uid = g_strdup (state->last_uid);
+ copy->position = state->position;
+
+ return copy;
+}
+
+static void
+cursor_state_free (EbSdbCursor *cursor,
+ CursorState *state)
+{
+ if (state) {
+ cursor_state_clear (cursor, state, EBSDB_CURSOR_ORIGIN_BEGIN);
+ g_free (state->values);
+ g_slice_free (CursorState, state);
+ }
+}
+
+static void
+cursor_state_clear (EbSdbCursor *cursor,
+ CursorState *state,
+ EbSdbCursorOrigin position)
+{
+ gint i;
+
+ for (i = 0; i < cursor->n_sort_fields; i++) {
+ g_free (state->values[i]);
+ state->values[i] = NULL;
+ }
+
+ g_free (state->last_uid);
+ state->last_uid = NULL;
+ state->position = position;
+}
+
+static void
+cursor_state_set_from_contact (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ CursorState *state,
+ EContact *contact)
+{
+ gint i;
+
+ cursor_state_clear (cursor, state, EBSDB_CURSOR_ORIGIN_BEGIN);
+
+ for (i = 0; i < cursor->n_sort_fields; i++) {
+ const gchar *string;
+
+ string = e_contact_get_const (
+ contact, cursor->sort_fields[i]);
+
+ if (string != NULL) {
+ state->values[i] = e_collator_generate_key (
+ ebsdb->priv->collator,
+ string, NULL);
+ } else {
+ state->values[i] = g_strdup ("");
+ }
+ }
+
+ state->last_uid = e_contact_get (contact, E_CONTACT_UID);
+ state->position = EBSDB_CURSOR_ORIGIN_CURRENT;
+}
+
+static void
+cursor_state_set_from_vcard (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ CursorState *state,
+ const gchar *vcard)
+{
+ EContact *contact;
+
+ contact = e_contact_new_from_vcard (vcard);
+ cursor_state_set_from_contact (ebsdb, cursor, state, contact);
+ g_object_unref (contact);
+}
+
+static void
+ebsdb_cursor_setup_query (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ const gchar *sexp,
+ gboolean query_with_list_attrs)
+{
+ gchar *stmt;
+ gchar *count_stmt;
+
+ g_free (cursor->select_vcards);
+ g_free (cursor->select_count);
+ g_free (cursor->query);
+ g_clear_object (&(cursor->sexp));
+
+ if (query_with_list_attrs) {
+ gchar *list_table = g_strconcat (cursor->folderid, "_lists", NULL);
+
+ stmt = sqlite3_mprintf ("SELECT DISTINCT summary.uid, vcard, bdata FROM %Q AS summary "
+ "LEFT OUTER JOIN %Q AS multi ON summary.uid = multi.uid",
+ cursor->folderid, list_table);
+
+ count_stmt = sqlite3_mprintf ("SELECT count(DISTINCT summary.uid), vcard, bdata FROM %Q AS summary "
+ "LEFT OUTER JOIN %Q AS multi ON summary.uid = multi.uid",
+ cursor->folderid, list_table);
+ g_free (list_table);
+ } else {
+ stmt = sqlite3_mprintf ("SELECT uid, vcard, bdata FROM %Q AS summary", cursor->folderid);
+ count_stmt = sqlite3_mprintf ("SELECT count(*) FROM %Q AS summary", cursor->folderid);
+ }
+
+ cursor->select_vcards = g_strdup (stmt);
+ cursor->select_count = g_strdup (count_stmt);
+ sqlite3_free (stmt);
+ sqlite3_free (count_stmt);
+
+ if (sexp) {
+ cursor->query = sexp_to_sql_query (ebsdb, cursor->folderid, sexp);
+ cursor->sexp = e_book_backend_sexp_new (sexp);
+ } else {
+ cursor->query = NULL;
+ cursor->sexp = NULL;
+ }
+}
+
+static gchar *
+ebsdb_cursor_order_by_fragment (EBookBackendSqliteDB *ebsdb,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ gboolean reverse)
+{
+ GString *string;
+ const gchar *field_name;
+ gint i;
+
+ string = g_string_new ("ORDER BY ");
+
+ for (i = 0; i < n_sort_fields; i++) {
+
+ field_name = summary_dbname_from_field (ebsdb, sort_fields[i]);
+
+ if (i > 0)
+ g_string_append (string, ", ");
+
+ g_string_append_printf (
+ string, "summary.%s_localized %s", field_name,
+ reverse ?
+ (sort_types[i] == E_BOOK_CURSOR_SORT_ASCENDING ? "DESC" : "ASC") :
+ (sort_types[i] == E_BOOK_CURSOR_SORT_ASCENDING ? "ASC" : "DESC"));
+ }
+
+ /* Also order the UID, since it's our tie
+ * breaker, we must also order the UID field. */
+ if (n_sort_fields > 0)
+ g_string_append (string, ", ");
+ g_string_append_printf (
+ string, "summary.uid %s", reverse ? "DESC" : "ASC");
+
+ return g_string_free (string, FALSE);
+}
+
+static EbSdbCursor *
+ebsdb_cursor_new (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *sexp,
+ gboolean query_with_list_attrs,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_sort_fields)
+{
+ EbSdbCursor *cursor = g_slice_new0 (EbSdbCursor);
+
+ cursor->folderid = g_strdup (folderid);
+
+ /* Setup the initial query fragments */
+ ebsdb_cursor_setup_query (ebsdb, cursor, sexp, query_with_list_attrs);
+
+ cursor->order = ebsdb_cursor_order_by_fragment (
+ ebsdb,
+ sort_fields,
+ sort_types,
+ n_sort_fields,
+ FALSE);
+ cursor->reverse_order = ebsdb_cursor_order_by_fragment (
+ ebsdb,
+ sort_fields,
+ sort_types,
+ n_sort_fields,
+ TRUE);
+
+ /* Sort parameters */
+ cursor->n_sort_fields = n_sort_fields;
+ cursor->sort_fields = g_memdup (
+ sort_fields, sizeof (EContactField) * n_sort_fields);
+ cursor->sort_types = g_memdup (
+ sort_types, sizeof (EBookCursorSortType) * n_sort_fields);
+
+ /* Cursor state */
+ cursor->state.values = g_new0 (gchar *, n_sort_fields);
+ cursor->state.last_uid = NULL;
+ cursor->state.position = EBSDB_CURSOR_ORIGIN_BEGIN;
+
+ return cursor;
+}
+
+static void
+ebsdb_cursor_free (EbSdbCursor *cursor)
+{
+ if (cursor != NULL) {
+ cursor_state_clear (
+ cursor, &(cursor->state),
+ EBSDB_CURSOR_ORIGIN_BEGIN);
+ g_free (cursor->state.values);
+
+ g_clear_object (&(cursor->sexp));
+ g_free (cursor->folderid);
+ g_free (cursor->select_vcards);
+ g_free (cursor->select_count);
+ g_free (cursor->query);
+ g_free (cursor->order);
+ g_free (cursor->reverse_order);
+ g_free (cursor->sort_fields);
+ g_free (cursor->sort_types);
+
+ g_slice_free (EbSdbCursor, cursor);
+ }
+}
+
+#define GREATER_OR_LESS(cursor, index, reverse) \
+ (reverse ? \
+ (((EbSdbCursor *) cursor)->sort_types[index] == E_BOOK_CURSOR_SORT_ASCENDING ? '<' : '>') : \
+ (((EbSdbCursor *) cursor)->sort_types[index] == E_BOOK_CURSOR_SORT_ASCENDING ? '>' : '<'))
+
+static gchar *
+ebsdb_cursor_constraints (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ CursorState *state,
+ gboolean reverse,
+ gboolean include_current_uid)
+{
+ GString *string;
+ const gchar *field_name;
+ gint i, j;
+
+ /* Example for:
+ * ORDER BY family_name ASC, given_name DESC
+ *
+ * Where current cursor values are:
+ * family_name = Jackson
+ * given_name = Micheal
+ *
+ * With reverse = FALSE
+ *
+ * (summary.family_name > 'Jackson') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name < 'Micheal') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid > 'last-uid')
+ *
+ * With reverse = TRUE (needed for moving the cursor backwards through results)
+ *
+ * (summary.family_name < 'Jackson') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name > 'Micheal') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid < 'last-uid')
+ *
+ */
+
+ string = g_string_new (NULL);
+
+ for (i = 0; i <= cursor->n_sort_fields; i++) {
+ gchar *stmt;
+
+ /* Break once we hit a NULL value */
+ if ((i < cursor->n_sort_fields && state->values[i] == NULL) ||
+ (i == cursor->n_sort_fields && state->last_uid == NULL))
+ break;
+
+ /* Between each qualifier, add an 'OR' */
+ if (i > 0)
+ g_string_append (string, " OR ");
+
+ /* Begin qualifier */
+ g_string_append_c (string, '(');
+
+ /* Create the '=' statements leading up to the current tie breaker */
+ for (j = 0; j < i; j++) {
+ field_name = summary_dbname_from_field (ebsdb, cursor->sort_fields[j]);
+
+ stmt = sqlite3_mprintf (
+ "summary.%s_localized = %Q",
+ field_name, state->values[j]);
+
+ g_string_append (string, stmt);
+ g_string_append (string, " AND ");
+
+ sqlite3_free (stmt);
+
+ }
+
+ if (i == cursor->n_sort_fields) {
+
+ /* The 'include_current_uid' clause is used for calculating
+ * the current position of the cursor, inclusive of the
+ * current position.
+ */
+ if (include_current_uid)
+ g_string_append_c (string, '(');
+
+ /* Append the UID tie breaker */
+ stmt = sqlite3_mprintf (
+ "summary.uid %c %Q",
+ reverse ? '<' : '>',
+ state->last_uid);
+ g_string_append (string, stmt);
+ sqlite3_free (stmt);
+
+ if (include_current_uid) {
+ stmt = sqlite3_mprintf (
+ " OR summary.uid = %Q",
+ state->last_uid);
+ g_string_append (string, stmt);
+ g_string_append_c (string, ')');
+ sqlite3_free (stmt);
+ }
+
+ } else {
+
+ /* SPECIAL CASE: If we have a parially set cursor state, then we must
+ * report next results that are inclusive of the final qualifier.
+ *
+ * This allows one to set the cursor with the family name set to 'J'
+ * and include the results for contact's Mr & Miss 'J'.
+ */
+ gboolean include_exact_match =
+ (reverse == FALSE &&
+ ((i + 1 < cursor->n_sort_fields && state->values[i + 1] == NULL) ||
+ (i + 1 == cursor->n_sort_fields && state->last_uid == NULL)));
+
+ if (include_exact_match)
+ g_string_append_c (string, '(');
+
+ /* Append the final qualifier for this field */
+ field_name = summary_dbname_from_field (ebsdb, cursor->sort_fields[i]);
+
+ stmt = sqlite3_mprintf (
+ "summary.%s_localized %c %Q",
+ field_name,
+ GREATER_OR_LESS (cursor, i, reverse),
+ state->values[i]);
+
+ g_string_append (string, stmt);
+ sqlite3_free (stmt);
+
+ if (include_exact_match) {
+
+ stmt = sqlite3_mprintf (
+ " OR summary.%s_localized = %Q",
+ field_name, state->values[i]);
+
+ g_string_append (string, stmt);
+ g_string_append_c (string, ')');
+ sqlite3_free (stmt);
+ }
+ }
+
+ /* End qualifier */
+ g_string_append_c (string, ')');
+ }
+
+ return g_string_free (string, FALSE);
+}
+
+static gboolean
+cursor_count_total_locked (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ gint *total,
+ GError **error)
+{
+ GString *query;
+ gboolean success;
+
+ query = g_string_new (cursor->select_count);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Execute the query */
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, query->str,
+ get_count_cb, total, error);
+
+ g_string_free (query, TRUE);
+
+ return success;
+}
+
+static gboolean
+cursor_count_position_locked (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ gint *position,
+ GError **error)
+{
+ GString *query;
+ gboolean success;
+
+ query = g_string_new (cursor->select_count);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Add the cursor constraints (if any) */
+ if (cursor->state.values[0] != NULL) {
+ gchar *constraints = NULL;
+
+ if (!cursor->query)
+ g_string_append (query, " WHERE ");
+ else
+ g_string_append (query, " AND ");
+
+ /* Here we do a reverse query, we're looking for all the
+ * results leading up to the current cursor value, including
+ * the cursor value. */
+ constraints = ebsdb_cursor_constraints (
+ ebsdb, cursor,
+ &(cursor->state),
+ TRUE, TRUE);
+
+ g_string_append_c (query, '(');
+ g_string_append (query, constraints);
+ g_string_append_c (query, ')');
+
+ g_free (constraints);
+ }
+
+ /* Execute the query */
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, query->str,
+ get_count_cb, position, error);
+
+ g_string_free (query, TRUE);
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_new:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @sexp: search expression; use NULL or an empty string to get all stored contacts.
+ * @sort_fields: (array length=n_sort_fields): An array of #EContactFields as sort keys in order of priority
+ * @sort_types: (array length=n_sort_fields): An array of #EBookCursorSortTypes, one for each field in @sort_fields
+ * @n_sort_fields: The number of fields to sort results by.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Creates a new #EbSdbCursor.
+ *
+ * The cursor should be freed with e_book_backend_sqlitedb_cursor_free().
+ *
+ * Returns: (transfer full): A newly created #EbSdbCursor
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+EbSdbCursor *
+e_book_backend_sqlitedb_cursor_new (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *sexp,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ GError **error)
+{
+ gboolean query_with_list_attrs = FALSE;
+ gint i;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folderid && folderid[0], NULL);
+
+ /* We don't like '\0' sexps, prefer NULL */
+ if (sexp && !sexp[0])
+ sexp = NULL;
+
+ /* We only support cursors for summary fields in the query */
+ if (sexp && !e_book_backend_sqlitedb_check_summary_query (ebsdb, sexp, &query_with_list_attrs)) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+ _("Only summary queries are supported by EbSdbCursor"));
+ return NULL;
+ }
+
+ if (n_sort_fields == 0) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+ _("At least one sort field must be specified to use an EbSdbCursor"));
+ return NULL;
+ }
+
+ /* We only support summarized sort keys which are not multi value fields */
+ for (i = 0; i < n_sort_fields; i++) {
+
+ gint support;
+
+ support = func_check_field_test (ebsdb, e_contact_field_name (sort_fields[i]), NULL);
+
+ if ((support & CHECK_IS_SUMMARY) == 0) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+ _("Cannot sort by a field that is not in the summary"));
+ return NULL;
+ }
+
+ if ((support & CHECK_IS_LIST_ATTR) != 0) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+ _("Cannot sort by a field which may have multiple values"));
+ return NULL;
+ }
+ }
+
+ return ebsdb_cursor_new (
+ ebsdb, folderid, sexp, query_with_list_attrs,
+ sort_fields, sort_types, n_sort_fields);
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_free:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor to free
+ *
+ * Frees @cursor.
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+void
+e_book_backend_sqlitedb_cursor_free (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb));
+
+ ebsdb_cursor_free (cursor);
+}
+
+typedef struct {
+ GSList *results;
+ gchar *alloc_vcard;
+ const gchar *last_vcard;
+
+ gboolean collect_results;
+ gint n_results;
+} CursorCollectData;
+
+static gint
+collect_results_for_cursor_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ CursorCollectData *data = ref;
+
+ if (data->collect_results) {
+ EbSdbSearchData *search_data;
+
+ search_data = search_data_from_results (cols);
+
+ data->results = g_slist_prepend (data->results, search_data);
+
+ data->last_vcard = search_data->vcard;
+ } else {
+ g_free (data->alloc_vcard);
+ data->alloc_vcard = g_strdup (cols[1]);
+
+ data->last_vcard = data->alloc_vcard;
+ }
+
+ data->n_results++;
+
+ return 0;
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_step:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor to use
+ * @flags: The #EbSdbCursorStepFlags for this step
+ * @origin: The #EbSdbCursorOrigin from whence to step
+ * @count: A positive or negative amount of contacts to try and fetch
+ * @results: (out) (allow-none) (element-type EbSdbSearchData) (transfer full):
+ * A return location to store the results, or %NULL if %EBSDB_CURSOR_STEP_FETCH is not specified in %flags.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Steps @cursor through it's sorted query by a maximum of @count contacts
+ * starting from @origin.
+ *
+ * If @count is negative, then the cursor will move through the list in reverse.
+ *
+ * If @cursor reaches the beginning or end of the query results, then the
+ * returned list might not contain the amount of desired contacts, or might
+ * return no results if the cursor currently points to the last contact.
+ * Reaching the end of the list is not considered an error condition. Attempts
+ * to step beyond the end of the list after having reached the end of the list
+ * will however trigger an %E_BOOK_SDB_ERROR_END_OF_LIST error.
+ *
+ * If %EBSDB_CURSOR_STEP_FETCH is specified in %flags, a pointer to
+ * a %NULL #GSList pointer should be provided for the @results parameter.
+ *
+ * The result list will be stored to @results and should be freed with g_slist_free()
+ * and all elements freed with e_book_backend_sqlitedb_search_data_free().
+ *
+ * Returns: The number of contacts traversed if successful, otherwise -1 is
+ * returned and @error is set.
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+gint
+e_book_backend_sqlitedb_cursor_step (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ EbSdbCursorStepFlags flags,
+ EbSdbCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GError **error)
+{
+ CursorCollectData data = { NULL, NULL, NULL, FALSE, 0 };
+ CursorState *state;
+ GString *query;
+ gboolean success;
+ EbSdbCursorOrigin try_position;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), -1);
+ g_return_val_if_fail (cursor != NULL, -1);
+ g_return_val_if_fail ((flags & EBSDB_CURSOR_STEP_FETCH) == 0 ||
+ (results != NULL && *results == NULL), -1);
+
+ /* Check if this step should result in an end of list error first */
+ try_position = cursor->state.position;
+ if (origin != EBSDB_CURSOR_ORIGIN_CURRENT)
+ try_position = origin;
+
+ /* Report errors for requests to run off the end of the list */
+ if (try_position == EBSDB_CURSOR_ORIGIN_BEGIN && count < 0) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR,
+ E_BOOK_SDB_ERROR_END_OF_LIST,
+ _("Tried to step a cursor in reverse, "
+ "but cursor is already at the beginning of the contact list"));
+
+ return -1;
+ } else if (try_position == EBSDB_CURSOR_ORIGIN_END && count > 0) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR,
+ E_BOOK_SDB_ERROR_END_OF_LIST,
+ _("Tried to step a cursor forwards, "
+ "but cursor is already at the end of the contact list"));
+
+ return -1;
+ }
+
+ /* Nothing to do, silently return */
+ if (count == 0 && try_position == EBSDB_CURSOR_ORIGIN_CURRENT)
+ return 0;
+
+ /* If we're not going to modify the position, just use
+ * a copy of the current cursor state.
+ */
+ if ((flags & EBSDB_CURSOR_STEP_MOVE) != 0)
+ state = &(cursor->state);
+ else
+ state = cursor_state_copy (cursor, &(cursor->state));
+
+ /* Every query starts with the STATE_CURRENT position, first
+ * fix up the cursor state according to 'origin'
+ */
+ switch (origin) {
+ case EBSDB_CURSOR_ORIGIN_CURRENT:
+ /* Do nothing, normal operation */
+ break;
+
+ case EBSDB_CURSOR_ORIGIN_BEGIN:
+ case EBSDB_CURSOR_ORIGIN_END:
+
+ /* Prepare the state before executing the query */
+ cursor_state_clear (cursor, state, origin);
+ break;
+ }
+
+ /* If count is 0 then there is no need to run any
+ * query, however it can be useful if you just want
+ * to move the cursor to the beginning or ending of
+ * the list.
+ */
+ if (count == 0) {
+
+ /* Free the state copy if need be */
+ if ((flags & EBSDB_CURSOR_STEP_MOVE) == 0)
+ cursor_state_free (cursor, state);
+
+ return 0;
+ }
+
+ query = g_string_new (cursor->select_vcards);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Add the cursor constraints (if any) */
+ if (state->values[0] != NULL) {
+ gchar *constraints = NULL;
+
+ if (!cursor->query)
+ g_string_append (query, " WHERE ");
+ else
+ g_string_append (query, " AND ");
+
+ constraints = ebsdb_cursor_constraints (
+ ebsdb, cursor, state, count < 0, FALSE);
+
+ g_string_append_c (query, '(');
+ g_string_append (query, constraints);
+ g_string_append_c (query, ')');
+
+ g_free (constraints);
+ }
+
+ /* Add the sort order */
+ g_string_append_c (query, ' ');
+ if (count > 0)
+ g_string_append (query, cursor->order);
+ else
+ g_string_append (query, cursor->reverse_order);
+
+ /* Add the limit */
+ g_string_append_printf (query, " LIMIT %d", ABS (count));
+
+ /* Specify whether we really want results or not */
+ data.collect_results = (flags & EBSDB_CURSOR_STEP_FETCH) != 0;
+
+ /* Execute the query */
+ LOCK_MUTEX (&ebsdb->priv->lock);
+ success = book_backend_sql_exec (
+ ebsdb->priv->db, query->str,
+ collect_results_for_cursor_cb, &data,
+ error);
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ g_string_free (query, TRUE);
+
+ /* If there was no error, update the internal cursor state */
+ if (success) {
+
+ if (data.n_results < ABS (count)) {
+
+ /* We've reached the end, clear the current state */
+ if (count < 0)
+ cursor_state_clear (cursor, state, EBSDB_CURSOR_ORIGIN_BEGIN);
+ else
+ cursor_state_clear (cursor, state, EBSDB_CURSOR_ORIGIN_END);
+
+ } else if (data.last_vcard) {
+
+ /* Set the cursor state to the last result */
+ cursor_state_set_from_vcard (ebsdb, cursor, state, data.last_vcard);
+ } else
+ /* Should never get here */
+ g_warn_if_reached ();
+
+ /* Assign the results to return (if any) */
+ if (results) {
+ /* Correct the order of results at the last minute */
+ *results = g_slist_reverse (data.results);
+ data.results = NULL;
+ }
+ }
+
+ /* Cleanup what was allocated by collect_results_for_cursor_cb() */
+ if (data.results)
+ g_slist_free_full (
+ data.results,
+ (GDestroyNotify) e_book_backend_sqlitedb_search_data_free);
+ g_free (data.alloc_vcard);
+
+ /* Free the copy state if we were working with a copy */
+ if ((flags & EBSDB_CURSOR_STEP_MOVE) == 0)
+ cursor_state_free (cursor, state);
+
+ if (success)
+ return data.n_results;
+
+ return -1;
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_set_target_alphabetic_index:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor to modify
+ * @index: The alphabetic index
+ *
+ * Sets the @cursor position to an
+ * <link linkend="cursor-alphabet">Alphabetic Index</link>
+ * into the alphabet active in @ebsdb's locale.
+ *
+ * After setting the target to an alphabetic index, for example the
+ * index for letter 'E', then further calls to e_book_backend_sqlitedb_cursor_step()
+ * will return results starting with the letter 'E' (or results starting
+ * with the last result in 'D', if moving in a negative direction).
+ *
+ * The passed index must be a valid index in the active locale, knowledge
+ * on the currently active alphabet index must be obtained using #ECollator
+ * APIs.
+ *
+ * Use e_book_backend_sqlitedb_ref_collator() to obtain the active collator for @ebsdb.
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+void
+e_book_backend_sqlitedb_cursor_set_target_alphabetic_index (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ gint index)
+{
+ gint n_labels = 0;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb));
+ g_return_if_fail (cursor != NULL);
+ g_return_if_fail (index >= 0);
+
+ e_collator_get_index_labels (
+ ebsdb->priv->collator, &n_labels,
+ NULL, NULL, NULL);
+ g_return_if_fail (index < n_labels);
+
+ cursor_state_clear (cursor, &(cursor->state), EBSDB_CURSOR_ORIGIN_CURRENT);
+ if (cursor->n_sort_fields > 0) {
+ cursor->state.values[0] =
+ e_collator_generate_key_for_index (ebsdb->priv->collator,
+ index);
+ }
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_set_sexp:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor
+ * @sexp: The new query expression for @cursor
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Modifies the current query expression for @cursor. This will not
+ * modify @cursor's state, but will change the outcome of any further
+ * calls to e_book_backend_sqlitedb_cursor_calculate() or
+ * e_book_backend_sqlitedb_cursor_step().
+ *
+ * Returns: %TRUE if the expression was valid and accepted by @ebsdb
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+gboolean
+e_book_backend_sqlitedb_cursor_set_sexp (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ const gchar *sexp,
+ GError **error)
+{
+ gboolean query_with_list_attrs = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (cursor != NULL, FALSE);
+
+ /* We don't like '\0' sexps, prefer NULL */
+ if (sexp && !sexp[0])
+ sexp = NULL;
+
+ /* We only support cursors for summary fields in the query */
+ if (sexp && !e_book_backend_sqlitedb_check_summary_query (ebsdb, sexp, &query_with_list_attrs)) {
+ g_set_error (
+ error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+ _("Only summary queries are supported by EbSdbCursor"));
+ return FALSE;
+ }
+
+ ebsdb_cursor_setup_query (ebsdb, cursor, sexp, query_with_list_attrs);
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_calculate:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor
+ * @total: (out) (allow-none): A return location to store the total result set for this cursor
+ * @position: (out) (allow-none): A return location to store the total results before the cursor value
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Calculates the @total amount of results for the @cursor's query expression,
+ * as well as the current @position of @cursor in the results. @position is
+ * represented as the amount of results which lead up to the current value
+ * of @cursor, if @cursor currently points to an exact contact, the position
+ * also includes the cursor contact.
+ *
+ * Returns: Whether @total and @position were successfully calculated.
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+gboolean
+e_book_backend_sqlitedb_cursor_calculate (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ gint *total,
+ gint *position,
+ GError **error)
+{
+ gboolean success = TRUE;
+ gint local_total = 0;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+ g_return_val_if_fail (cursor != NULL, FALSE);
+
+ /* If we're in a clear cursor state, then the position is 0 */
+ if (position && cursor->state.values[0] == NULL) {
+
+ if (cursor->state.position == EBSDB_CURSOR_ORIGIN_BEGIN) {
+ /* Mark the local pointer NULL, no need to calculate this anymore */
+ *position = 0;
+ position = NULL;
+ } else if (cursor->state.position == EBSDB_CURSOR_ORIGIN_END) {
+
+ /* Make sure that we look up the total so we can
+ * set the position to 'total + 1'
+ */
+ if (!total)
+ total = &local_total;
+ }
+ }
+
+ /* Early return if there is nothing to do */
+ if (!total && !position)
+ return TRUE;
+
+ LOCK_MUTEX (&ebsdb->priv->lock);
+
+ if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+ return FALSE;
+ }
+
+ if (total)
+ success = cursor_count_total_locked (ebsdb, cursor, total, error);
+
+ if (success && position)
+ success = cursor_count_position_locked (ebsdb, cursor, position, error);
+
+ if (success)
+ success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+ else
+ /* The GError is already set. */
+ book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+ UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+ /* In the case we're at the end, we just set the position
+ * to be the total + 1
+ */
+ if (success && position && total &&
+ cursor->state.position == EBSDB_CURSOR_ORIGIN_END)
+ *position = *total + 1;
+
+ return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_compare_contact:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor
+ * @contact: The #EContact to compare
+ * @matches_sexp: (out) (allow-none): Whether the contact matches the cursor's search expression
+ *
+ * Compares @contact with @cursor and returns whether @contact is less than, equal to, or greater
+ * than @cursor.
+ *
+ * Returns: A value that is less than, equal to, or greater than zero if @contact is found,
+ * respectively, to be less than, to match, or be greater than the current value of @cursor.
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+gint
+e_book_backend_sqlitedb_cursor_compare_contact (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp)
+{
+ EBookBackendSqliteDBPrivate *priv;
+ gint i;
+ gint comparison = 0;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), -1);
+ g_return_val_if_fail (E_IS_CONTACT (contact), -1);
+ g_return_val_if_fail (cursor != NULL, -1);
+
+ priv = ebsdb->priv;
+
+ if (matches_sexp) {
+ if (cursor->sexp == NULL)
+ *matches_sexp = TRUE;
+ else
+ *matches_sexp =
+ e_book_backend_sexp_match_contact (cursor->sexp, contact);
+ }
+
+ for (i = 0; i < cursor->n_sort_fields && comparison == 0; i++) {
+
+ /* Empty state sorts below any contact value, which means the contact sorts above cursor */
+ if (cursor->state.values[i] == NULL) {
+ comparison = 1;
+ } else {
+ const gchar *field_value;
+
+ field_value = (const gchar *)
+ e_contact_get_const (contact, cursor->sort_fields[i]);
+
+ /* Empty contact state sorts below any cursor value */
+ if (field_value == NULL)
+ comparison = -1;
+ else {
+ gchar *collation_key;
+
+ /* Check of contact sorts below, equal to, or above the cursor */
+ collation_key = e_collator_generate_key (priv->collator, field_value, NULL);
+ comparison = strcmp (collation_key, cursor->state.values[i]);
+ g_free (collation_key);
+ }
+ }
+ }
+
+ /* UID tie-breaker */
+ if (comparison == 0) {
+ const gchar *uid;
+
+ uid = (const gchar *) e_contact_get_const (contact, E_CONTACT_UID);
+
+ if (cursor->state.last_uid == NULL)
+ comparison = 1;
+ else if (uid == NULL)
+ comparison = -1;
+ else
+ comparison = strcmp (uid, cursor->state.last_uid);
+ }
+
+ return comparison;
+}
diff --git a/src/addressbook/libedata-book/e-book-backend-sqlitedb.h b/src/addressbook/libedata-book/e-book-backend-sqlitedb.h
new file mode 100644
index 000000000..4a3e388d0
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-sqlitedb.h
@@ -0,0 +1,435 @@
+/*-*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* e-book-backend-sqlitedb.h
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2012 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Chenthill Palanisamy <pchenthill@novell.com>
+ * Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_BACKEND_SQLITEDB_H
+#define E_BOOK_BACKEND_SQLITEDB_H
+
+#ifndef EDS_DISABLE_DEPRECATED
+
+#include <libebook-contacts/libebook-contacts.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_BACKEND_SQLITEDB \
+ (e_book_backend_sqlitedb_get_type ())
+#define E_BOOK_BACKEND_SQLITEDB(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_BACKEND_SQLITEDB, EBookBackendSqliteDB))
+#define E_BOOK_BACKEND_SQLITEDB_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_BACKEND_SQLITEDB, EBookBackendSqliteDBClass))
+#define E_IS_BOOK_BACKEND_SQLITEDB(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_BACKEND_SQLITEDB))
+#define E_IS_BOOK_BACKEND_SQLITEDB_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_BACKEND_SQLITEDB))
+#define E_BOOK_BACKEND_SQLITEDB_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_BACKEND_SQLITEDB, EBookBackendSqliteDBClass))
+
+/**
+ * E_BOOK_SDB_ERROR:
+ *
+ * Error domain for #EBookBackendSqliteDB operations.
+ *
+ * Since: 3.8
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+#define E_BOOK_SDB_ERROR (e_book_backend_sqlitedb_error_quark ())
+
+G_BEGIN_DECLS
+
+typedef struct _EBookBackendSqliteDB EBookBackendSqliteDB;
+typedef struct _EBookBackendSqliteDBClass EBookBackendSqliteDBClass;
+typedef struct _EBookBackendSqliteDBPrivate EBookBackendSqliteDBPrivate;
+
+/**
+ * EBookSDBError:
+ * @E_BOOK_SDB_ERROR_CONSTRAINT: The error occurred due to an explicit constraint
+ * @E_BOOK_SDB_ERROR_CONTACT_NOT_FOUND: A contact was not found by UID (this is different
+ * from a query that returns no results, which is not an error).
+ * @E_BOOK_SDB_ERROR_OTHER: Another error occurred
+ * @E_BOOK_SDB_ERROR_NOT_SUPPORTED: A query was not supported
+ * @E_BOOK_SDB_ERROR_INVALID_QUERY: A query was invalid. This can happen if the sexp could not be parsed
+ * or if a phone number query contained non-phonenumber input.
+ * @E_BOOK_SDB_ERROR_END_OF_LIST: An attempt was made to fetch results past the end of a contact list
+ *
+ * Defines the types of possible errors reported by the #EBookBackendSqliteDB
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+typedef enum {
+ E_BOOK_SDB_ERROR_CONSTRAINT,
+ E_BOOK_SDB_ERROR_CONTACT_NOT_FOUND,
+ E_BOOK_SDB_ERROR_OTHER,
+ E_BOOK_SDB_ERROR_NOT_SUPPORTED,
+ E_BOOK_SDB_ERROR_INVALID_QUERY,
+ E_BOOK_SDB_ERROR_END_OF_LIST
+} EBookSDBError;
+
+/**
+ * EBookBackendSqliteDB:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+struct _EBookBackendSqliteDB {
+ /*< private >*/
+ GObject parent;
+ EBookBackendSqliteDBPrivate *priv;
+};
+
+/**
+ * EBookBackendSqliteDBClass:
+ *
+ * Class structure for the #EBookBackendSqlite class.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+struct _EBookBackendSqliteDBClass {
+ /*< private >*/
+ GObjectClass parent_class;
+};
+
+/**
+ * EbSdbSearchData:
+ * @vcard: The the vcard string
+ * @uid: The %E_CONTACT_UID field of this contact
+ * @bdata: Extra data set for this contact.
+ *
+ * This structure is used to represent contacts returned
+ * by the EBookBackendSqliteDB from various functions
+ * such as e_book_backend_sqlitedb_search().
+ *
+ * The @bdata parameter will contain any data previously
+ * set for the given contact with e_book_backend_sqlitedb_set_contact_bdata().
+ *
+ * These should be freed with e_book_backend_sqlitedb_search_data_free().
+ *
+ * Since: 3.2
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+typedef struct {
+ gchar *vcard;
+ gchar *uid;
+ gchar *bdata;
+} EbSdbSearchData;
+
+/**
+ * EbSdbCuror:
+ *
+ * An opaque cursor pointer
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+typedef struct _EbSdbCursor EbSdbCursor;
+
+/**
+ * EbSdbCursorOrigin:
+ * @EBSDB_CURSOR_ORIGIN_CURRENT: The current cursor position
+ * @EBSDB_CURSOR_ORIGIN_BEGIN: The beginning of the cursor results.
+ * @EBSDB_CURSOR_ORIGIN_END: The ending of the cursor results.
+ *
+ * Specifies the start position to in the list of traversed contacts
+ * in calls to e_book_backend_sqlitedb_cursor_step().
+ *
+ * When an #EbSdbCuror is created, the current position implied by %EBSDB_CURSOR_ORIGIN_CURRENT
+ * is the same as %EBSDB_CURSOR_ORIGIN_BEGIN.
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+typedef enum {
+ EBSDB_CURSOR_ORIGIN_CURRENT = 0,
+ EBSDB_CURSOR_ORIGIN_BEGIN,
+ EBSDB_CURSOR_ORIGIN_END
+} EbSdbCursorOrigin;
+
+/**
+ * EbSdbCursorStepFlags:
+ * @EBSDB_CURSOR_STEP_MOVE: The cursor position should be modified while stepping
+ * @EBSDB_CURSOR_STEP_FETCH: Traversed contacts should be listed and returned while stepping.
+ *
+ * Defines the behaviour of e_book_backend_sqlitedb_cursor_step().
+ *
+ * Since: 3.12
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+typedef enum {
+ EBSDB_CURSOR_STEP_MOVE = (1 << 0),
+ EBSDB_CURSOR_STEP_FETCH = (1 << 1)
+} EbSdbCursorStepFlags;
+
+GType e_book_backend_sqlitedb_get_type
+ (void) G_GNUC_CONST;
+GQuark e_book_backend_sqlitedb_error_quark
+ (void);
+EBookBackendSqliteDB *
+ e_book_backend_sqlitedb_new (const gchar *path,
+ const gchar *emailid,
+ const gchar *folderid,
+ const gchar *folder_name,
+ gboolean store_vcard,
+ GError **error);
+EBookBackendSqliteDB *
+ e_book_backend_sqlitedb_new_full
+ (const gchar *path,
+ const gchar *emailid,
+ const gchar *folderid,
+ const gchar *folder_name,
+ gboolean store_vcard,
+ ESourceBackendSummarySetup *setup,
+ GError **error);
+gboolean e_book_backend_sqlitedb_lock_updates
+ (EBookBackendSqliteDB *ebsdb,
+ GError **error);
+gboolean e_book_backend_sqlitedb_unlock_updates
+ (EBookBackendSqliteDB *ebsdb,
+ gboolean do_commit,
+ GError **error);
+ECollator *e_book_backend_sqlitedb_ref_collator
+ (EBookBackendSqliteDB *ebsdb);
+gboolean e_book_backend_sqlitedb_new_contact
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ EContact *contact,
+ gboolean replace_existing,
+ GError **error);
+gboolean e_book_backend_sqlitedb_new_contacts
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GSList *contacts,
+ gboolean replace_existing,
+ GError **error);
+gboolean e_book_backend_sqlitedb_remove_contact
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ GError **error);
+gboolean e_book_backend_sqlitedb_remove_contacts
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GSList *uids,
+ GError **error);
+gboolean e_book_backend_sqlitedb_has_contact
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ gboolean *partial_content,
+ GError **error);
+EContact * e_book_backend_sqlitedb_get_contact
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ GHashTable *fields_of_interest,
+ gboolean *with_all_required_fields,
+ GError **error);
+gchar * e_book_backend_sqlitedb_get_vcard_string
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ GHashTable *fields_of_interest,
+ gboolean *with_all_required_fields,
+ GError **error);
+GSList * e_book_backend_sqlitedb_search (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *sexp,
+ GHashTable *fields_of_interest,
+ gboolean *searched,
+ gboolean *with_all_required_fields,
+ GError **error);
+GSList * e_book_backend_sqlitedb_search_uids
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *sexp,
+ gboolean *searched,
+ GError **error);
+GHashTable * e_book_backend_sqlitedb_get_uids_and_rev
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error);
+gboolean e_book_backend_sqlitedb_get_is_populated
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error);
+gboolean e_book_backend_sqlitedb_set_is_populated
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gboolean populated,
+ GError **error);
+gboolean e_book_backend_sqlitedb_get_revision
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gchar **revision_out,
+ GError **error);
+gboolean e_book_backend_sqlitedb_set_revision
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *revision,
+ GError **error);
+gchar * e_book_backend_sqlitedb_get_sync_data
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error);
+gboolean e_book_backend_sqlitedb_set_sync_data
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *sync_data,
+ GError **error);
+gchar * e_book_backend_sqlitedb_get_key_value
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *key,
+ GError **error);
+gboolean e_book_backend_sqlitedb_set_key_value
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *key,
+ const gchar *value,
+ GError **error);
+gchar * e_book_backend_sqlitedb_get_contact_bdata
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ GError **error);
+gboolean e_book_backend_sqlitedb_set_contact_bdata
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *uid,
+ const gchar *value,
+ GError **error);
+gboolean e_book_backend_sqlitedb_get_has_partial_content
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error);
+gboolean e_book_backend_sqlitedb_set_has_partial_content
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gboolean partial_content,
+ GError **error);
+GSList * e_book_backend_sqlitedb_get_partially_cached_ids
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error);
+gboolean e_book_backend_sqlitedb_delete_addressbook
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GError **error);
+gboolean e_book_backend_sqlitedb_remove (EBookBackendSqliteDB *ebsdb,
+ GError **error);
+void e_book_backend_sqlitedb_search_data_free
+ (EbSdbSearchData *s_data);
+gboolean e_book_backend_sqlitedb_check_summary_query
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *query,
+ gboolean *with_list_attrs);
+gboolean e_book_backend_sqlitedb_check_summary_fields
+ (EBookBackendSqliteDB *ebsdb,
+ GHashTable *fields_of_interest);
+gboolean e_book_backend_sqlitedb_set_locale
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *lc_collate,
+ GError **error);
+gboolean e_book_backend_sqlitedb_get_locale
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ gchar **locale_out,
+ GError **error);
+
+/* Cursor API */
+EbSdbCursor *e_book_backend_sqlitedb_cursor_new
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ const gchar *sexp,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ GError **error);
+void e_book_backend_sqlitedb_cursor_free
+ (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor);
+gint e_book_backend_sqlitedb_cursor_step
+ (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ EbSdbCursorStepFlags flags,
+ EbSdbCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GError **error);
+void e_book_backend_sqlitedb_cursor_set_target_alphabetic_index
+ (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ gint index);
+gboolean e_book_backend_sqlitedb_cursor_set_sexp
+ (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ const gchar *sexp,
+ GError **error);
+gboolean e_book_backend_sqlitedb_cursor_calculate
+ (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ gint *total,
+ gint *position,
+ GError **error);
+gint e_book_backend_sqlitedb_cursor_compare_contact
+ (EBookBackendSqliteDB *ebsdb,
+ EbSdbCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp);
+
+gboolean e_book_backend_sqlitedb_is_summary_query
+ (const gchar *query);
+gboolean e_book_backend_sqlitedb_is_summary_fields
+ (GHashTable *fields_of_interest);
+gboolean e_book_backend_sqlitedb_add_contact
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ EContact *contact,
+ gboolean partial_content,
+ GError **error);
+gboolean e_book_backend_sqlitedb_add_contacts
+ (EBookBackendSqliteDB *ebsdb,
+ const gchar *folderid,
+ GSList *contacts,
+ gboolean partial_content,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* EDS_DISABLE_DEPRECATED */
+
+#endif /* E_BOOK_BACKEND_SQLITEDB_H */
diff --git a/src/addressbook/libedata-book/e-book-backend-summary.c b/src/addressbook/libedata-book/e-book-backend-summary.c
new file mode 100644
index 000000000..df69281fa
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-summary.c
@@ -0,0 +1,1347 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Chris Toshok <toshok@ximian.com>
+ */
+
+/**
+ * SECTION: e-book-backend-summary
+ * @include: libedata-book/libedata-book.h
+ * @short_description: A utility for storing contact data and searching for contacts
+ *
+ * The #EBookBackendSummary is deprecated, use #EBookSqlite instead.
+ */
+#include "evolution-data-server-config.h"
+
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <utime.h>
+#include <errno.h>
+
+#include <glib/gstdio.h>
+
+#include "e-book-backend-summary.h"
+
+#define E_BOOK_BACKEND_SUMMARY_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_BOOK_BACKEND_SUMMARY, EBookBackendSummaryPrivate))
+
+G_DEFINE_TYPE (EBookBackendSummary, e_book_backend_summary, G_TYPE_OBJECT)
+
+struct _EBookBackendSummaryPrivate {
+ gchar *summary_path;
+ FILE *fp;
+ guint32 file_version;
+ time_t mtime;
+ gboolean upgraded;
+ gboolean dirty;
+ gint flush_timeout_millis;
+ gint flush_timeout;
+ GPtrArray *items;
+ GHashTable *id_to_item;
+ guint32 num_items; /* used only for loading */
+#ifdef SUMMARY_STATS
+ gint size;
+#endif
+};
+
+typedef struct {
+ gchar *id;
+ gchar *nickname;
+ gchar *full_name;
+ gchar *given_name;
+ gchar *surname;
+ gchar *file_as;
+ gchar *email_1;
+ gchar *email_2;
+ gchar *email_3;
+ gchar *email_4;
+ gboolean wants_html;
+ gboolean wants_html_set;
+ gboolean list;
+ gboolean list_show_addresses;
+} EBookBackendSummaryItem;
+
+typedef struct {
+ /* these lengths do *not* including the terminating \0, as
+ * it's not stored on disk. */
+ guint16 id_len;
+ guint16 nickname_len;
+ guint16 full_name_len; /* version 3.0 field */
+ guint16 given_name_len;
+ guint16 surname_len;
+ guint16 file_as_len;
+ guint16 email_1_len;
+ guint16 email_2_len;
+ guint16 email_3_len;
+ guint16 email_4_len;
+ guint8 wants_html;
+ guint8 wants_html_set;
+ guint8 list;
+ guint8 list_show_addresses;
+} EBookBackendSummaryDiskItem;
+
+typedef struct {
+ guint32 file_version;
+ guint32 num_items;
+ guint32 summary_mtime; /* version 2.0 field */
+} EBookBackendSummaryHeader;
+
+#define PAS_SUMMARY_MAGIC "PAS-SUMMARY"
+#define PAS_SUMMARY_MAGIC_LEN 11
+
+#define PAS_SUMMARY_FILE_VERSION_1_0 1000
+#define PAS_SUMMARY_FILE_VERSION_2_0 2000
+#define PAS_SUMMARY_FILE_VERSION_3_0 3000
+#define PAS_SUMMARY_FILE_VERSION_4_0 4000
+#define PAS_SUMMARY_FILE_VERSION_5_0 5000
+
+#define PAS_SUMMARY_FILE_VERSION PAS_SUMMARY_FILE_VERSION_5_0
+
+static void
+free_summary_item (EBookBackendSummaryItem *item)
+{
+ g_free (item->id);
+ g_free (item->nickname);
+ g_free (item->full_name);
+ g_free (item->given_name);
+ g_free (item->surname);
+ g_free (item->file_as);
+ g_free (item->email_1);
+ g_free (item->email_2);
+ g_free (item->email_3);
+ g_free (item->email_4);
+ g_free (item);
+}
+
+static void
+clear_items (EBookBackendSummary *summary)
+{
+ gint i;
+ gint num = summary->priv->items->len;
+ for (i = 0; i < num; i++) {
+ EBookBackendSummaryItem *item = g_ptr_array_remove_index_fast (summary->priv->items, 0);
+ if (item) {
+ g_hash_table_remove (summary->priv->id_to_item, item->id);
+ free_summary_item (item);
+ }
+ }
+}
+
+/**
+ * e_book_backend_summary_new:
+ * @summary_path: a local file system path
+ * @flush_timeout_millis: a flush interval, in milliseconds
+ *
+ * Creates an #EBookBackendSummary object without loading it
+ * or otherwise affecting the file. @flush_timeout_millis
+ * specifies how much time should elapse, at a minimum, from
+ * the summary is changed until it is flushed to disk.
+ *
+ * Returns: A new #EBookBackendSummary.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+EBookBackendSummary *
+e_book_backend_summary_new (const gchar *summary_path,
+ gint flush_timeout_millis)
+{
+ EBookBackendSummary *summary = g_object_new (E_TYPE_BOOK_BACKEND_SUMMARY, NULL);
+
+ summary->priv->summary_path = g_strdup (summary_path);
+ summary->priv->flush_timeout_millis = flush_timeout_millis;
+ summary->priv->file_version = PAS_SUMMARY_FILE_VERSION_4_0;
+
+ return summary;
+}
+
+static void
+e_book_backend_summary_finalize (GObject *object)
+{
+ EBookBackendSummaryPrivate *priv;
+
+ priv = E_BOOK_BACKEND_SUMMARY_GET_PRIVATE (object);
+
+ if (priv->fp)
+ fclose (priv->fp);
+ if (priv->dirty)
+ e_book_backend_summary_save (E_BOOK_BACKEND_SUMMARY (object));
+ else
+ utime (priv->summary_path, NULL);
+
+ if (priv->flush_timeout)
+ g_source_remove (priv->flush_timeout);
+
+ g_free (priv->summary_path);
+ clear_items (E_BOOK_BACKEND_SUMMARY (object));
+ g_ptr_array_free (priv->items, TRUE);
+
+ g_hash_table_destroy (priv->id_to_item);
+
+ /* Chain up to parent's finalize() method. */
+ G_OBJECT_CLASS (e_book_backend_summary_parent_class)->finalize (object);
+}
+
+static void
+e_book_backend_summary_class_init (EBookBackendSummaryClass *class)
+{
+ GObjectClass *object_class;
+
+ g_type_class_add_private (class, sizeof (EBookBackendSummaryPrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->finalize = e_book_backend_summary_finalize;
+}
+
+static void
+e_book_backend_summary_init (EBookBackendSummary *summary)
+{
+ summary->priv = E_BOOK_BACKEND_SUMMARY_GET_PRIVATE (summary);
+
+ summary->priv->items = g_ptr_array_new ();
+ summary->priv->id_to_item = g_hash_table_new (g_str_hash, g_str_equal);
+}
+
+
+static gboolean
+e_book_backend_summary_check_magic (EBookBackendSummary *summary,
+ FILE *fp)
+{
+ gchar buf[PAS_SUMMARY_MAGIC_LEN + 1];
+ gint rv;
+
+ memset (buf, 0, sizeof (buf));
+
+ rv = fread (buf, PAS_SUMMARY_MAGIC_LEN, 1, fp);
+ if (rv != 1)
+ return FALSE;
+ if (strcmp (buf, PAS_SUMMARY_MAGIC))
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+e_book_backend_summary_load_header (EBookBackendSummary *summary,
+ FILE *fp,
+ EBookBackendSummaryHeader *header)
+{
+ gint rv;
+
+ rv = fread (&header->file_version, sizeof (header->file_version), 1, fp);
+ if (rv != 1)
+ return FALSE;
+
+ header->file_version = g_ntohl (header->file_version);
+
+ if (header->file_version < PAS_SUMMARY_FILE_VERSION) {
+ return FALSE; /* this will cause the entire summary to be rebuilt */
+ }
+
+ rv = fread (&header->num_items, sizeof (header->num_items), 1, fp);
+ if (rv != 1)
+ return FALSE;
+
+ header->num_items = g_ntohl (header->num_items);
+
+ rv = fread (&header->summary_mtime, sizeof (header->summary_mtime), 1, fp);
+ if (rv != 1)
+ return FALSE;
+ header->summary_mtime = g_ntohl (header->summary_mtime);
+
+ return TRUE;
+}
+
+static gchar *
+read_string (FILE *fp,
+ gsize len)
+{
+ gchar *buf;
+ size_t rv;
+
+ /* Avoid overflow for the nul byte. */
+ if (len == G_MAXSIZE)
+ return NULL;
+
+ buf = g_new0 (char, len + 1);
+
+ rv = fread (buf, sizeof (gchar), len, fp);
+ if (rv != len) {
+ g_free (buf);
+ return NULL;
+ }
+
+ /* Validate the string as UTF-8. */
+ if (!g_utf8_validate (buf, rv, NULL)) {
+ g_free (buf);
+ return NULL;
+ }
+
+ return buf;
+}
+
+static gboolean
+e_book_backend_summary_load_item (EBookBackendSummary *summary,
+ EBookBackendSummaryItem **new_item)
+{
+ EBookBackendSummaryItem *item;
+ gchar *buf;
+ FILE *fp = summary->priv->fp;
+
+ if (summary->priv->file_version >= PAS_SUMMARY_FILE_VERSION_4_0) {
+ EBookBackendSummaryDiskItem disk_item;
+ gint rv = fread (&disk_item, sizeof (disk_item), 1, fp);
+ if (rv != 1)
+ return FALSE;
+
+ disk_item.id_len = g_ntohs (disk_item.id_len);
+ disk_item.nickname_len = g_ntohs (disk_item.nickname_len);
+ disk_item.full_name_len = g_ntohs (disk_item.full_name_len);
+ disk_item.given_name_len = g_ntohs (disk_item.given_name_len);
+ disk_item.surname_len = g_ntohs (disk_item.surname_len);
+ disk_item.file_as_len = g_ntohs (disk_item.file_as_len);
+ disk_item.email_1_len = g_ntohs (disk_item.email_1_len);
+ disk_item.email_2_len = g_ntohs (disk_item.email_2_len);
+ disk_item.email_3_len = g_ntohs (disk_item.email_3_len);
+ disk_item.email_4_len = g_ntohs (disk_item.email_4_len);
+
+ item = g_new0 (EBookBackendSummaryItem, 1);
+
+ item->wants_html = disk_item.wants_html;
+ item->wants_html_set = disk_item.wants_html_set;
+ item->list = disk_item.list;
+ item->list_show_addresses = disk_item.list_show_addresses;
+
+ if (disk_item.id_len) {
+ buf = read_string (fp, disk_item.id_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->id = buf;
+ }
+
+ if (disk_item.nickname_len) {
+ buf = read_string (fp, disk_item.nickname_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->nickname = buf;
+ }
+
+ if (disk_item.full_name_len) {
+ buf = read_string (fp, disk_item.full_name_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->full_name = buf;
+ }
+
+ if (disk_item.given_name_len) {
+ buf = read_string (fp, disk_item.given_name_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->given_name = buf;
+ }
+
+ if (disk_item.surname_len) {
+ buf = read_string (fp, disk_item.surname_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->surname = buf;
+ }
+
+ if (disk_item.file_as_len) {
+ buf = read_string (fp, disk_item.file_as_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->file_as = buf;
+ }
+
+ if (disk_item.email_1_len) {
+ buf = read_string (fp, disk_item.email_1_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->email_1 = buf;
+ }
+
+ if (disk_item.email_2_len) {
+ buf = read_string (fp, disk_item.email_2_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->email_2 = buf;
+ }
+
+ if (disk_item.email_3_len) {
+ buf = read_string (fp, disk_item.email_3_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->email_3 = buf;
+ }
+
+ if (disk_item.email_4_len) {
+ buf = read_string (fp, disk_item.email_4_len);
+ if (!buf) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ item->email_4 = buf;
+ }
+
+ /* the only field that has to be there is the id */
+ if (!item->id) {
+ free_summary_item (item);
+ return FALSE;
+ }
+ }
+ else {
+ /* unhandled file version */
+ return FALSE;
+ }
+
+ *new_item = item;
+ return TRUE;
+}
+
+/* opens the file and loads the header */
+static gboolean
+e_book_backend_summary_open (EBookBackendSummary *summary)
+{
+ FILE *fp;
+ EBookBackendSummaryHeader header;
+ struct stat sb;
+
+ if (summary->priv->fp)
+ return TRUE;
+
+ /* Try opening the summary file. */
+ fp = g_fopen (summary->priv->summary_path, "rb");
+ if (!fp) {
+ /* if there's no summary present, look for the .new
+ * file and rename it if it's there, and attempt to
+ * load that */
+ gchar *new_filename = g_strconcat (summary->priv->summary_path, ".new", NULL);
+
+ if (g_rename (new_filename, summary->priv->summary_path) == -1 &&
+ errno != ENOENT) {
+ g_warning (
+ "%s: Failed to rename '%s' to '%s': %s", G_STRFUNC,
+ new_filename, summary->priv->summary_path, g_strerror (errno));
+ } else {
+ fp = g_fopen (summary->priv->summary_path, "rb");
+ }
+
+ g_free (new_filename);
+ }
+
+ if (!fp) {
+ g_warning ("failed to open summary file");
+ return FALSE;
+ }
+
+ if (fstat (fileno (fp), &sb) == -1) {
+ g_warning ("failed to get summary file size");
+ fclose (fp);
+ return FALSE;
+ }
+
+ if (!e_book_backend_summary_check_magic (summary, fp)) {
+ g_warning ("file is not a valid summary file");
+ fclose (fp);
+ return FALSE;
+ }
+
+ if (!e_book_backend_summary_load_header (summary, fp, &header)) {
+ g_warning ("failed to read summary header");
+ fclose (fp);
+ return FALSE;
+ }
+
+ summary->priv->num_items = header.num_items;
+ summary->priv->file_version = header.file_version;
+ summary->priv->mtime = sb.st_mtime;
+ summary->priv->fp = fp;
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_summary_load:
+ * @summary: an #EBookBackendSummary
+ *
+ * Attempts to load @summary from disk. The load is successful if
+ * the file was located, it was in the correct format, and it was
+ * not out of date.
+ *
+ * Returns: %TRUE if the load succeeded, %FALSE if it failed.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_summary_load (EBookBackendSummary *summary)
+{
+ EBookBackendSummaryItem *new_item;
+ gint i;
+
+ g_return_val_if_fail (summary != NULL, FALSE);
+
+ clear_items (summary);
+
+ if (!e_book_backend_summary_open (summary))
+ return FALSE;
+
+ for (i = 0; i < summary->priv->num_items; i++) {
+ if (!e_book_backend_summary_load_item (summary, &new_item)) {
+ g_warning ("error while reading summary item");
+ clear_items (summary);
+ fclose (summary->priv->fp);
+ summary->priv->fp = NULL;
+ summary->priv->dirty = FALSE;
+ return FALSE;
+ }
+
+ g_ptr_array_add (summary->priv->items, new_item);
+ g_hash_table_insert (summary->priv->id_to_item, new_item->id, new_item);
+ }
+
+ if (summary->priv->upgraded) {
+ e_book_backend_summary_save (summary);
+ }
+ summary->priv->dirty = FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+e_book_backend_summary_save_magic (FILE *fp)
+{
+ gint rv;
+ rv = fwrite (PAS_SUMMARY_MAGIC, sizeof (gchar), PAS_SUMMARY_MAGIC_LEN, fp);
+ if (rv != PAS_SUMMARY_MAGIC_LEN)
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+e_book_backend_summary_save_header (EBookBackendSummary *summary,
+ FILE *fp)
+{
+ EBookBackendSummaryHeader header;
+ gint rv;
+
+ header.file_version = g_htonl (PAS_SUMMARY_FILE_VERSION);
+ header.num_items = g_htonl (summary->priv->items->len);
+ header.summary_mtime = g_htonl (time (NULL));
+
+ rv = fwrite (&header, sizeof (header), 1, fp);
+ if (rv != 1)
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+save_string (const gchar *str,
+ FILE *fp)
+{
+ size_t rv, len;
+
+ if (!str || !*str)
+ return TRUE;
+
+ len = strlen (str);
+ rv = fwrite (str, sizeof (gchar), len, fp);
+ return (rv == len);
+}
+
+static gboolean
+e_book_backend_summary_save_item (EBookBackendSummary *summary,
+ FILE *fp,
+ EBookBackendSummaryItem *item)
+{
+ EBookBackendSummaryDiskItem disk_item;
+ gint len;
+ gint rv;
+
+ len = item->id ? strlen (item->id) : 0;
+ disk_item.id_len = g_htons (len);
+
+ len = item->nickname ? strlen (item->nickname) : 0;
+ disk_item.nickname_len = g_htons (len);
+
+ len = item->given_name ? strlen (item->given_name) : 0;
+ disk_item.given_name_len = g_htons (len);
+
+ len = item->full_name ? strlen (item->full_name) : 0;
+ disk_item.full_name_len = g_htons (len);
+
+ len = item->surname ? strlen (item->surname) : 0;
+ disk_item.surname_len = g_htons (len);
+
+ len = item->file_as ? strlen (item->file_as) : 0;
+ disk_item.file_as_len = g_htons (len);
+
+ len = item->email_1 ? strlen (item->email_1) : 0;
+ disk_item.email_1_len = g_htons (len);
+
+ len = item->email_2 ? strlen (item->email_2) : 0;
+ disk_item.email_2_len = g_htons (len);
+
+ len = item->email_3 ? strlen (item->email_3) : 0;
+ disk_item.email_3_len = g_htons (len);
+
+ len = item->email_4 ? strlen (item->email_4) : 0;
+ disk_item.email_4_len = g_htons (len);
+
+ disk_item.wants_html = item->wants_html;
+ disk_item.wants_html_set = item->wants_html_set;
+ disk_item.list = item->list;
+ disk_item.list_show_addresses = item->list_show_addresses;
+
+ rv = fwrite (&disk_item, sizeof (disk_item), 1, fp);
+ if (rv != 1)
+ return FALSE;
+
+ if (!save_string (item->id, fp))
+ return FALSE;
+ if (!save_string (item->nickname, fp))
+ return FALSE;
+ if (!save_string (item->full_name, fp))
+ return FALSE;
+ if (!save_string (item->given_name, fp))
+ return FALSE;
+ if (!save_string (item->surname, fp))
+ return FALSE;
+ if (!save_string (item->file_as, fp))
+ return FALSE;
+ if (!save_string (item->email_1, fp))
+ return FALSE;
+ if (!save_string (item->email_2, fp))
+ return FALSE;
+ if (!save_string (item->email_3, fp))
+ return FALSE;
+ if (!save_string (item->email_4, fp))
+ return FALSE;
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_summary_save:
+ * @summary: an #EBookBackendSummary
+ *
+ * Attempts to save @summary to disk.
+ *
+ * Returns: %TRUE if the save succeeded, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_summary_save (EBookBackendSummary *summary)
+{
+ struct stat sb;
+ FILE *fp = NULL;
+ gchar *new_filename = NULL;
+ gint i;
+
+ g_return_val_if_fail (summary != NULL, FALSE);
+
+ if (!summary->priv->dirty)
+ return TRUE;
+
+ new_filename = g_strconcat (summary->priv->summary_path, ".new", NULL);
+
+ fp = g_fopen (new_filename, "wb");
+ if (!fp) {
+ g_warning ("could not create new summary file");
+ goto lose;
+ }
+
+ if (!e_book_backend_summary_save_magic (fp)) {
+ g_warning ("could not write magic to new summary file");
+ goto lose;
+ }
+
+ if (!e_book_backend_summary_save_header (summary, fp)) {
+ g_warning ("could not write header to new summary file");
+ goto lose;
+ }
+
+ for (i = 0; i < summary->priv->items->len; i++) {
+ EBookBackendSummaryItem *item = g_ptr_array_index (summary->priv->items, i);
+ if (!e_book_backend_summary_save_item (summary, fp, item)) {
+ g_warning ("failed to write an item to new summary file, errno = %d", errno);
+ goto lose;
+ }
+ }
+
+ fclose (fp);
+
+ /* if we have a queued flush, clear it (since we just flushed) */
+ if (summary->priv->flush_timeout) {
+ g_source_remove (summary->priv->flush_timeout);
+ summary->priv->flush_timeout = 0;
+ }
+
+ /* unlink the old summary and rename the new one */
+ g_unlink (summary->priv->summary_path);
+ if (g_rename (new_filename, summary->priv->summary_path) == -1) {
+ g_warning (
+ "%s: Failed to rename '%s' to '%s': %s", G_STRFUNC,
+ new_filename, summary->priv->summary_path, g_strerror (errno));
+ }
+
+ g_free (new_filename);
+
+ /* lastly, update the in memory mtime to that of the file */
+ if (g_stat (summary->priv->summary_path, &sb) == -1) {
+ g_warning ("error stat'ing saved summary");
+ }
+ else {
+ summary->priv->mtime = sb.st_mtime;
+ }
+
+ summary->priv->dirty = FALSE;
+ return TRUE;
+
+ lose:
+ if (fp)
+ fclose (fp);
+ g_unlink (new_filename);
+ g_free (new_filename);
+ return FALSE;
+}
+
+/**
+ * e_book_backend_summary_add_contact:
+ * @summary: an #EBookBackendSummary
+ * @contact: an #EContact to add
+ *
+ * Adds a summary of @contact to @summary. Does not check if
+ * the contact already has a summary.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+void
+e_book_backend_summary_add_contact (EBookBackendSummary *summary,
+ EContact *contact)
+{
+ EBookBackendSummaryItem *new_item;
+ gchar *id = NULL;
+
+ g_return_if_fail (summary != NULL);
+
+ /* ID normally should not be NULL for a contact. */
+ /* Added this check as groupwise server sometimes returns
+ * contacts with NULL id
+ */
+ id = e_contact_get (contact, E_CONTACT_UID);
+ if (!id) {
+ g_warning ("found a contact with NULL uid");
+ return;
+ }
+
+ /* Ensure the duplicate contacts are not added */
+ if (e_book_backend_summary_check_contact (summary, id))
+ e_book_backend_summary_remove_contact (summary, id);
+
+ new_item = g_new0 (EBookBackendSummaryItem, 1);
+
+ new_item->id = id;
+ new_item->nickname = e_contact_get (contact, E_CONTACT_NICKNAME);
+ new_item->full_name = e_contact_get (contact, E_CONTACT_FULL_NAME);
+ new_item->given_name = e_contact_get (contact, E_CONTACT_GIVEN_NAME);
+ new_item->surname = e_contact_get (contact, E_CONTACT_FAMILY_NAME);
+ new_item->file_as = e_contact_get (contact, E_CONTACT_FILE_AS);
+ new_item->email_1 = e_contact_get (contact, E_CONTACT_EMAIL_1);
+ new_item->email_2 = e_contact_get (contact, E_CONTACT_EMAIL_2);
+ new_item->email_3 = e_contact_get (contact, E_CONTACT_EMAIL_3);
+ new_item->email_4 = e_contact_get (contact, E_CONTACT_EMAIL_4);
+ new_item->list = GPOINTER_TO_INT (e_contact_get (contact, E_CONTACT_IS_LIST));
+ new_item->list_show_addresses = GPOINTER_TO_INT (e_contact_get (contact, E_CONTACT_LIST_SHOW_ADDRESSES));
+ new_item->wants_html = GPOINTER_TO_INT (e_contact_get (contact, E_CONTACT_WANTS_HTML));
+
+ g_ptr_array_add (summary->priv->items, new_item);
+ g_hash_table_insert (summary->priv->id_to_item, new_item->id, new_item);
+
+#ifdef SUMMARY_STATS
+ summary->priv->size += sizeof (EBookBackendSummaryItem);
+ summary->priv->size += new_item->id ? strlen (new_item->id) : 0;
+ summary->priv->size += new_item->nickname ? strlen (new_item->nickname) : 0;
+ summary->priv->size += new_item->full_name ? strlen (new_item->full_name) : 0;
+ summary->priv->size += new_item->given_name ? strlen (new_item->given_name) : 0;
+ summary->priv->size += new_item->surname ? strlen (new_item->surname) : 0;
+ summary->priv->size += new_item->file_as ? strlen (new_item->file_as) : 0;
+ summary->priv->size += new_item->email_1 ? strlen (new_item->email_1) : 0;
+ summary->priv->size += new_item->email_2 ? strlen (new_item->email_2) : 0;
+ summary->priv->size += new_item->email_3 ? strlen (new_item->email_3) : 0;
+ summary->priv->size += new_item->email_4 ? strlen (new_item->email_4) : 0;
+#endif
+ e_book_backend_summary_touch (summary);
+}
+
+/**
+ * e_book_backend_summary_remove_contact:
+ * @summary: an #EBookBackendSummary
+ * @id: a unique contact ID string
+ *
+ * Removes the summary of the contact identified by @id from @summary.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+void
+e_book_backend_summary_remove_contact (EBookBackendSummary *summary,
+ const gchar *id)
+{
+ EBookBackendSummaryItem *item;
+
+ g_return_if_fail (summary != NULL);
+
+ item = g_hash_table_lookup (summary->priv->id_to_item, id);
+
+ if (item) {
+ g_ptr_array_remove (summary->priv->items, item);
+ g_hash_table_remove (summary->priv->id_to_item, id);
+ free_summary_item (item);
+ e_book_backend_summary_touch (summary);
+ return;
+ }
+
+ g_warning ("e_book_backend_summary_remove_contact: unable to locate id `%s'", id);
+}
+
+/**
+ * e_book_backend_summary_check_contact:
+ * @summary: an #EBookBackendSummary
+ * @id: a unique contact ID string
+ *
+ * Checks if a summary of the contact identified by @id
+ * exists in @summary.
+ *
+ * Returns: %TRUE if the summary exists, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_summary_check_contact (EBookBackendSummary *summary,
+ const gchar *id)
+{
+ g_return_val_if_fail (summary != NULL, FALSE);
+
+ return g_hash_table_lookup (summary->priv->id_to_item, id) != NULL;
+}
+
+static gboolean
+summary_flush_func (gpointer data)
+{
+ EBookBackendSummary *summary = E_BOOK_BACKEND_SUMMARY (data);
+
+ if (!summary->priv->dirty) {
+ summary->priv->flush_timeout = 0;
+ return FALSE;
+ }
+
+ if (!e_book_backend_summary_save (summary)) {
+ /* this isn't fatal, as we can just either 1) flush
+ * out with the next change, or 2) regen the summary
+ * when we next load the uri */
+ g_warning ("failed to flush summary file to disk");
+ return TRUE; /* try again after the next timeout */
+ }
+
+ g_message ("Flushed summary to disk");
+
+ /* we only want this to execute once, so return FALSE and set
+ * summary->flush_timeout to 0 */
+ summary->priv->flush_timeout = 0;
+ return FALSE;
+}
+
+/**
+ * e_book_backend_summary_touch:
+ * @summary: an #EBookBackendSummary
+ *
+ * Indicates that @summary has changed and should be flushed to disk.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+void
+e_book_backend_summary_touch (EBookBackendSummary *summary)
+{
+ g_return_if_fail (summary != NULL);
+
+ summary->priv->dirty = TRUE;
+ if (!summary->priv->flush_timeout
+ && summary->priv->flush_timeout_millis) {
+ summary->priv->flush_timeout = e_named_timeout_add (
+ summary->priv->flush_timeout_millis,
+ summary_flush_func, summary);
+ }
+}
+
+/**
+ * e_book_backend_summary_is_up_to_date:
+ * @summary: an #EBookBackendSummary
+ * @t: the time to compare with
+ *
+ * Checks if @summary is more recent than @t.
+ *
+ * Returns: %TRUE if the summary is up to date, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_summary_is_up_to_date (EBookBackendSummary *summary,
+ time_t t)
+{
+ g_return_val_if_fail (summary != NULL, FALSE);
+
+ if (!e_book_backend_summary_open (summary))
+ return FALSE;
+ else
+ return summary->priv->mtime >= t;
+}
+
+
+/* we only want to do summary queries if the query is over the set fields in the summary */
+
+static ESExpResult *
+func_check (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ ESExpResult *r;
+ gint truth = FALSE;
+ gboolean *pretval = data;
+
+ if (argc == 2
+ && argv[0]->type == ESEXP_RES_STRING
+ && argv[1]->type == ESEXP_RES_STRING) {
+ gchar *query_name = argv[0]->value.string;
+
+ if (!strcmp (query_name, "nickname") ||
+ !strcmp (query_name, "full_name") ||
+ !strcmp (query_name, "file_as") ||
+ !strcmp (query_name, "email")) {
+ truth = TRUE;
+ }
+ }
+
+ r = e_sexp_result_new (f, ESEXP_RES_BOOL);
+ r->value.boolean = truth;
+
+ if (pretval)
+ *pretval = (*pretval) && truth;
+
+ return r;
+}
+
+/* 'builtin' functions */
+static const struct {
+ const gchar *name;
+ ESExpFunc *func;
+ gint type; /* set to 1 if a function can perform shortcut evaluation, or
+ doesn't execute everything, 0 otherwise */
+} check_symbols[] = {
+ { "contains", func_check, 0 },
+ { "is", func_check, 0 },
+ { "beginswith", func_check, 0 },
+ { "endswith", func_check, 0 },
+ { "exists", func_check, 0 },
+ { "exists_vcard", func_check, 0 }
+};
+
+/**
+ * e_book_backend_summary_is_summary_query:
+ * @summary: an #EBookBackendSummary
+ * @query: an s-expression to check
+ *
+ * Checks if @query can be satisfied by searching only the fields
+ * stored by @summary.
+ *
+ * Returns: %TRUE if the query can be satisfied, %FALSE otherwise.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gboolean
+e_book_backend_summary_is_summary_query (EBookBackendSummary *summary,
+ const gchar *query)
+{
+ ESExp *sexp;
+ ESExpResult *r;
+ gboolean retval = TRUE;
+ gint i;
+ gint esexp_error;
+
+ g_return_val_if_fail (summary != NULL, FALSE);
+
+ sexp = e_sexp_new ();
+
+ for (i = 0; i < G_N_ELEMENTS (check_symbols); i++) {
+ if (check_symbols[i].type == 1) {
+ e_sexp_add_ifunction (sexp, 0, check_symbols[i].name,
+ (ESExpIFunc *) check_symbols[i].func, &retval);
+ } else {
+ e_sexp_add_function (
+ sexp, 0, check_symbols[i].name,
+ check_symbols[i].func, &retval);
+ }
+ }
+
+ e_sexp_input_text (sexp, query, strlen (query));
+ esexp_error = e_sexp_parse (sexp);
+
+ if (esexp_error == -1) {
+ g_object_unref (sexp);
+ return FALSE;
+ }
+
+ r = e_sexp_eval (sexp);
+
+ retval = retval && (r && r->type == ESEXP_RES_BOOL && r->value.boolean);
+
+ e_sexp_result_free (sexp, r);
+
+ g_object_unref (sexp);
+
+ return retval;
+}
+
+
+
+/* the actual query mechanics */
+static ESExpResult *
+do_compare (EBookBackendSummary *summary,
+ struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gchar *(*compare)(const gchar *, const gchar *))
+{
+ GPtrArray *result = g_ptr_array_new ();
+ ESExpResult *r;
+ gint i;
+
+ if (argc == 2
+ && argv[0]->type == ESEXP_RES_STRING
+ && argv[1]->type == ESEXP_RES_STRING) {
+
+ for (i = 0; i < summary->priv->items->len; i++) {
+ EBookBackendSummaryItem *item = g_ptr_array_index (summary->priv->items, i);
+ if (!strcmp (argv[0]->value.string, "full_name")) {
+ gchar *given = item->given_name;
+ gchar *surname = item->surname;
+ gchar *full_name = item->full_name;
+
+ if ((given && compare (given, argv[1]->value.string))
+ || (surname && compare (surname, argv[1]->value.string))
+ || (full_name && compare (full_name, argv[1]->value.string)))
+ g_ptr_array_add (result, item->id);
+ }
+ else if (!strcmp (argv[0]->value.string, "email")) {
+ gchar *email_1 = item->email_1;
+ gchar *email_2 = item->email_2;
+ gchar *email_3 = item->email_3;
+ gchar *email_4 = item->email_4;
+ if ((email_1 && compare (email_1, argv[1]->value.string))
+ || (email_2 && compare (email_2, argv[1]->value.string))
+ || (email_3 && compare (email_3, argv[1]->value.string))
+ || (email_4 && compare (email_4, argv[1]->value.string)))
+ g_ptr_array_add (result, item->id);
+ }
+ else if (!strcmp (argv[0]->value.string, "file_as")) {
+ gchar *file_as = item->file_as;
+ if (file_as && compare (file_as, argv[1]->value.string))
+ g_ptr_array_add (result, item->id);
+ }
+ else if (!strcmp (argv[0]->value.string, "nickname")) {
+ gchar *nickname = item->nickname;
+ if (nickname && compare (nickname, argv[1]->value.string))
+ g_ptr_array_add (result, item->id);
+ }
+ }
+ }
+
+ r = e_sexp_result_new (f, ESEXP_RES_ARRAY_PTR);
+ r->value.ptrarray = result;
+
+ return r;
+}
+
+static gchar *
+contains_helper (const gchar *ps1,
+ const gchar *ps2)
+{
+ gchar *s1 = e_util_utf8_remove_accents (ps1);
+ gchar *s2 = e_util_utf8_remove_accents (ps2);
+ gchar *res;
+
+ res = (gchar *) e_util_utf8_strstrcase (s1, s2);
+
+ g_free (s1);
+ g_free (s2);
+
+ return res;
+}
+
+static ESExpResult *
+func_contains (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ EBookBackendSummary *summary = data;
+
+ return do_compare (summary, f, argc, argv, contains_helper);
+}
+
+static gchar *
+is_helper (const gchar *ps1,
+ const gchar *ps2)
+{
+ gchar *s1 = e_util_utf8_remove_accents (ps1);
+ gchar *s2 = e_util_utf8_remove_accents (ps2);
+ gchar *res;
+
+ if (!e_util_utf8_strcasecmp (s1, s2))
+ res = (gchar *) ps1;
+ else
+ res = NULL;
+
+ g_free (s1);
+ g_free (s2);
+
+ return res;
+}
+
+static ESExpResult *
+func_is (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ EBookBackendSummary *summary = data;
+
+ return do_compare (summary, f, argc, argv, is_helper);
+}
+
+static gchar *
+endswith_helper (const gchar *ps1,
+ const gchar *ps2)
+{
+ gchar *s1 = e_util_utf8_remove_accents (ps1);
+ gchar *s2 = e_util_utf8_remove_accents (ps2);
+ gchar *res;
+ glong s1len = g_utf8_strlen (s1, -1);
+ glong s2len = g_utf8_strlen (s2, -1);
+
+ if (s1len < s2len)
+ res = NULL;
+ else
+ res = (gchar *) e_util_utf8_strstrcase (g_utf8_offset_to_pointer (s1, s1len - s2len), s2);
+
+ g_free (s1);
+ g_free (s2);
+
+ return res;
+}
+
+static ESExpResult *
+func_endswith (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ EBookBackendSummary *summary = data;
+
+ return do_compare (summary, f, argc, argv, endswith_helper);
+}
+
+static gchar *
+beginswith_helper (const gchar *ps1,
+ const gchar *ps2)
+{
+ gchar *p, *res;
+ gchar *s1 = e_util_utf8_remove_accents (ps1);
+ gchar *s2 = e_util_utf8_remove_accents (ps2);
+
+ if ((p = (gchar *) e_util_utf8_strstrcase (s1, s2))
+ && (p == s1))
+ res = (gchar *) ps1;
+ else
+ res = NULL;
+
+ g_free (s1);
+ g_free (s2);
+
+ return res;
+}
+
+static ESExpResult *
+func_beginswith (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ EBookBackendSummary *summary = data;
+
+ return do_compare (summary, f, argc, argv, beginswith_helper);
+}
+
+/* 'builtin' functions */
+static const struct {
+ const gchar *name;
+ ESExpFunc *func;
+ gint type; /* set to 1 if a function can perform shortcut evaluation, or
+ doesn't execute everything, 0 otherwise */
+} symbols[] = {
+ { "contains", func_contains, 0 },
+ { "is", func_is, 0 },
+ { "beginswith", func_beginswith, 0 },
+ { "endswith", func_endswith, 0 },
+};
+
+/**
+ * e_book_backend_summary_search:
+ * @summary: an #EBookBackendSummary
+ * @query: an s-expression
+ *
+ * Searches @summary for contacts matching @query.
+ *
+ * Returns: A #GPtrArray of pointers to contact ID strings.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+GPtrArray *
+e_book_backend_summary_search (EBookBackendSummary *summary,
+ const gchar *query)
+{
+ ESExp *sexp;
+ ESExpResult *r;
+ GPtrArray *retval;
+ gint i;
+ gint esexp_error;
+
+ g_return_val_if_fail (summary != NULL, NULL);
+
+ sexp = e_sexp_new ();
+
+ for (i = 0; i < G_N_ELEMENTS (symbols); i++) {
+ if (symbols[i].type == 1) {
+ e_sexp_add_ifunction (sexp, 0, symbols[i].name,
+ (ESExpIFunc *) symbols[i].func, summary);
+ } else {
+ e_sexp_add_function (
+ sexp, 0, symbols[i].name,
+ symbols[i].func, summary);
+ }
+ }
+
+ e_sexp_input_text (sexp, query, strlen (query));
+ esexp_error = e_sexp_parse (sexp);
+
+ if (esexp_error == -1) {
+ return NULL;
+ }
+
+ retval = g_ptr_array_new ();
+ r = e_sexp_eval (sexp);
+
+ if (r && r->type == ESEXP_RES_ARRAY_PTR && r->value.ptrarray) {
+ GPtrArray *ptrarray = r->value.ptrarray;
+ gint i;
+
+ for (i = 0; i < ptrarray->len; i++)
+ g_ptr_array_add (retval, g_ptr_array_index (ptrarray, i));
+ }
+
+ e_sexp_result_free (sexp, r);
+
+ g_object_unref (sexp);
+
+ return retval;
+}
+
+/**
+ * e_book_backend_summary_get_summary_vcard:
+ * @summary: an #EBookBackendSummary
+ * @id: a unique contact ID
+ *
+ * Constructs and returns a VCard from the contact summary specified
+ * by @id.
+ *
+ * Returns: A new VCard, or %NULL if the contact summary didn't exist.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ **/
+gchar *
+e_book_backend_summary_get_summary_vcard (EBookBackendSummary *summary,
+ const gchar *id)
+{
+ EBookBackendSummaryItem *item;
+
+ g_return_val_if_fail (summary != NULL, NULL);
+
+ item = g_hash_table_lookup (summary->priv->id_to_item, id);
+
+ if (item) {
+ EContact *contact = e_contact_new ();
+ gchar *vcard;
+
+ e_contact_set (contact, E_CONTACT_UID, item->id);
+ e_contact_set (contact, E_CONTACT_FILE_AS, item->file_as);
+ e_contact_set (contact, E_CONTACT_GIVEN_NAME, item->given_name);
+ e_contact_set (contact, E_CONTACT_FAMILY_NAME, item->surname);
+ e_contact_set (contact, E_CONTACT_NICKNAME, item->nickname);
+ e_contact_set (contact, E_CONTACT_FULL_NAME, item->full_name);
+ e_contact_set (contact, E_CONTACT_EMAIL_1, item->email_1);
+ e_contact_set (contact, E_CONTACT_EMAIL_2, item->email_2);
+ e_contact_set (contact, E_CONTACT_EMAIL_3, item->email_3);
+ e_contact_set (contact, E_CONTACT_EMAIL_4, item->email_4);
+
+ e_contact_set (contact, E_CONTACT_IS_LIST, GINT_TO_POINTER (item->list));
+ e_contact_set (contact, E_CONTACT_LIST_SHOW_ADDRESSES, GINT_TO_POINTER (item->list_show_addresses));
+ e_contact_set (contact, E_CONTACT_WANTS_HTML, GINT_TO_POINTER (item->wants_html));
+
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+ g_object_unref (contact);
+
+ return vcard;
+ }
+ else {
+ g_warning ("in unable to locate card `%s' in summary", id);
+ return NULL;
+ }
+}
+
diff --git a/src/addressbook/libedata-book/e-book-backend-summary.h b/src/addressbook/libedata-book/e-book-backend-summary.h
new file mode 100644
index 000000000..6b2ae0210
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend-summary.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Chris Toshok <toshok@ximian.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_BACKEND_SUMMARY_H
+#define E_BOOK_BACKEND_SUMMARY_H
+
+#ifndef EDS_DISABLE_DEPRECATED
+
+#include <libebook-contacts/libebook-contacts.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_BACKEND_SUMMARY \
+ (e_book_backend_summary_get_type ())
+#define E_BOOK_BACKEND_SUMMARY(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_BACKEND_SUMMARY, EBookBackendSummary))
+#define E_BOOK_BACKEND_SUMMARY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_BACKEND_SUMMARY, EBookBackendSummaryClass))
+#define E_IS_BOOK_BACKEND_SUMMARY(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_BACKEND_SUMMARY))
+#define E_IS_BOOK_BACKEND_SUMMARY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_BACKEND_SUMMARY))
+#define E_BOOK_BACKEND_SUMMARY_GET_CLASS(cls) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_BACKEND_SUMMARY, EBookBackendSummaryClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookBackendSummary EBookBackendSummary;
+typedef struct _EBookBackendSummaryClass EBookBackendSummaryClass;
+typedef struct _EBookBackendSummaryPrivate EBookBackendSummaryPrivate;
+
+/**
+ * EBookBackendSummary:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+struct _EBookBackendSummary {
+ /*< private >*/
+ GObject parent_object;
+ EBookBackendSummaryPrivate *priv;
+};
+
+/**
+ * EBookBackendSummaryClass:
+ *
+ * Class structure for the deprecated API for accessing the addressbook
+ *
+ * Deprecated: 3.12: Use #EBookSqlite instead
+ */
+struct _EBookBackendSummaryClass{
+ /*< private >*/
+ GObjectClass parent_class;
+};
+
+GType e_book_backend_summary_get_type (void) G_GNUC_CONST;
+EBookBackendSummary *
+ e_book_backend_summary_new (const gchar *summary_path,
+ gint flush_timeout_millis);
+
+/* returns FALSE if the load fails for any reason (including that the
+ * summary is out of date), TRUE if it succeeds */
+gboolean e_book_backend_summary_load (EBookBackendSummary *summary);
+/* returns FALSE if the save fails, TRUE if it succeeds (or isn't required due to no changes) */
+gboolean e_book_backend_summary_save (EBookBackendSummary *summary);
+
+void e_book_backend_summary_add_contact
+ (EBookBackendSummary *summary,
+ EContact *contact);
+void e_book_backend_summary_remove_contact
+ (EBookBackendSummary *summary,
+ const gchar *id);
+gboolean e_book_backend_summary_check_contact
+ (EBookBackendSummary *summary,
+ const gchar *id);
+
+void e_book_backend_summary_touch (EBookBackendSummary *summary);
+
+/* returns TRUE if the summary's mtime is >= @t. */
+gboolean e_book_backend_summary_is_up_to_date
+ (EBookBackendSummary *summary,
+ time_t t);
+
+gboolean e_book_backend_summary_is_summary_query
+ (EBookBackendSummary *summary,
+ const gchar *query);
+GPtrArray * e_book_backend_summary_search (EBookBackendSummary *summary,
+ const gchar *query);
+gchar * e_book_backend_summary_get_summary_vcard
+ (EBookBackendSummary *summary,
+ const gchar *id);
+
+G_END_DECLS
+
+#endif /* EDS_DISABLE_DEPRECATED */
+
+#endif /* E_BOOK_BACKEND_SUMMARY_H */
diff --git a/src/addressbook/libedata-book/e-book-backend.c b/src/addressbook/libedata-book/e-book-backend.c
new file mode 100644
index 000000000..9efac670a
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend.c
@@ -0,0 +1,3642 @@
+/*
+ * e-book-backend.c
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2012 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Nat Friedman (nat@ximian.com)
+ * Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+/**
+ * SECTION: e-book-backend
+ * @include: libedata-book/libedata-book.h
+ * @short_description: An abstract class for implementing addressbook backends
+ *
+ * This is the main server facing API for interfacing with addressbook backends,
+ * addressbook backends must implement methods on this class.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "e-data-book-view.h"
+#include "e-data-book.h"
+#include "e-book-backend.h"
+
+#define E_BOOK_BACKEND_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_BOOK_BACKEND, EBookBackendPrivate))
+
+typedef struct _AsyncContext AsyncContext;
+typedef struct _DispatchNode DispatchNode;
+
+struct _EBookBackendPrivate {
+ ESourceRegistry *registry;
+ EDataBook *data_book;
+
+ gboolean opened;
+
+ GMutex views_mutex;
+ GList *views;
+
+ GMutex property_lock;
+ GProxyResolver *proxy_resolver;
+ gchar *cache_dir;
+ gboolean writable;
+
+ ESource *authentication_source;
+ gulong auth_source_changed_handler_id;
+
+ GMutex operation_lock;
+ GThreadPool *thread_pool;
+ GHashTable *operation_ids;
+ GQueue pending_operations;
+ guint32 next_operation_id;
+ GSimpleAsyncResult *blocked;
+};
+
+struct _AsyncContext {
+
+ /* Indicates if we're using the old or new style API,
+ * as method results are stashed differently for each. */
+ gboolean old_style;
+
+ /* Inputs */
+ gchar *uid;
+ gchar *query;
+ gchar **strv;
+
+ /* Outputs */
+ EContact *contact;
+ GQueue result_queue;
+
+ /* One of these should point to result_queue
+ * so any leftover resources can be released. */
+ GQueue *object_queue;
+ GQueue *string_queue;
+};
+
+struct _DispatchNode {
+ /* This is the dispatch function
+ * that invokes the class method. */
+ GSimpleAsyncThreadFunc dispatch_func;
+ gboolean blocking_operation;
+
+ GSimpleAsyncResult *simple;
+ GCancellable *cancellable;
+};
+
+enum {
+ PROP_0,
+ PROP_CACHE_DIR,
+ PROP_PROXY_RESOLVER,
+ PROP_REGISTRY,
+ PROP_WRITABLE
+};
+
+enum {
+ CLOSED,
+ SHUTDOWN,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE (EBookBackend, e_book_backend, E_TYPE_BACKEND)
+
+static void
+async_context_free (AsyncContext *async_context)
+{
+ GQueue *queue;
+
+ g_free (async_context->uid);
+ g_free (async_context->query);
+ g_strfreev (async_context->strv);
+
+ g_clear_object (&async_context->contact);
+
+ queue = async_context->object_queue;
+ while (queue != NULL && !g_queue_is_empty (queue))
+ g_object_unref (g_queue_pop_head (queue));
+
+ queue = async_context->string_queue;
+ while (queue != NULL && !g_queue_is_empty (queue))
+ g_free (g_queue_pop_head (queue));
+
+ g_slice_free (AsyncContext, async_context);
+}
+
+static void
+dispatch_node_free (DispatchNode *dispatch_node)
+{
+ g_clear_object (&dispatch_node->simple);
+ g_clear_object (&dispatch_node->cancellable);
+
+ g_slice_free (DispatchNode, dispatch_node);
+}
+
+static void
+book_backend_push_operation (EBookBackend *backend,
+ GSimpleAsyncResult *simple,
+ GCancellable *cancellable,
+ gboolean blocking_operation,
+ GSimpleAsyncThreadFunc dispatch_func)
+{
+ DispatchNode *node;
+
+ g_return_if_fail (G_IS_SIMPLE_ASYNC_RESULT (simple));
+ g_return_if_fail (dispatch_func != NULL);
+
+ g_mutex_lock (&backend->priv->operation_lock);
+
+ node = g_slice_new0 (DispatchNode);
+ node->dispatch_func = dispatch_func;
+ node->blocking_operation = blocking_operation;
+ node->simple = g_object_ref (simple);
+
+ if (G_IS_CANCELLABLE (cancellable))
+ node->cancellable = g_object_ref (cancellable);
+
+ g_queue_push_tail (&backend->priv->pending_operations, node);
+
+ g_mutex_unlock (&backend->priv->operation_lock);
+}
+
+static void
+book_backend_dispatch_thread (DispatchNode *node)
+{
+ GCancellable *cancellable = node->cancellable;
+ GError *local_error = NULL;
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) {
+ g_simple_async_result_take_error (node->simple, local_error);
+ g_simple_async_result_complete_in_idle (node->simple);
+ } else {
+ GAsyncResult *result;
+ GObject *source_object;
+
+ result = G_ASYNC_RESULT (node->simple);
+ source_object = g_async_result_get_source_object (result);
+ node->dispatch_func (node->simple, source_object, cancellable);
+ g_object_unref (source_object);
+ }
+
+ dispatch_node_free (node);
+}
+
+static gboolean
+book_backend_dispatch_next_operation (EBookBackend *backend)
+{
+ DispatchNode *node;
+
+ g_mutex_lock (&backend->priv->operation_lock);
+
+ /* We can't dispatch additional operations
+ * while a blocking operation is in progress. */
+ if (backend->priv->blocked != NULL) {
+ g_mutex_unlock (&backend->priv->operation_lock);
+ return FALSE;
+ }
+
+ /* Pop the next DispatchNode off the queue. */
+ node = g_queue_pop_head (&backend->priv->pending_operations);
+ if (node == NULL) {
+ g_mutex_unlock (&backend->priv->operation_lock);
+ return FALSE;
+ }
+
+ /* If this a blocking operation, block any
+ * further dispatching until this finishes. */
+ if (node->blocking_operation)
+ backend->priv->blocked = g_object_ref (node->simple);
+
+ g_mutex_unlock (&backend->priv->operation_lock);
+
+ /* An error here merely indicates a thread could not be
+ * created, and so the node was queued. We don't care. */
+ g_thread_pool_push (backend->priv->thread_pool, node, NULL);
+
+ return TRUE;
+}
+
+static void
+book_backend_unblock_operations (EBookBackend *backend,
+ GSimpleAsyncResult *simple)
+{
+ /* If the GSimpleAsyncResult was blocking the dispatch queue,
+ * unblock the dispatch queue. Then dispatch as many waiting
+ * operations as we can. */
+
+ g_mutex_lock (&backend->priv->operation_lock);
+ if (backend->priv->blocked == simple)
+ g_clear_object (&backend->priv->blocked);
+ g_mutex_unlock (&backend->priv->operation_lock);
+
+ while (book_backend_dispatch_next_operation (backend))
+ ;
+}
+
+static guint32
+book_backend_stash_operation (EBookBackend *backend,
+ GSimpleAsyncResult *simple)
+{
+ guint32 opid;
+
+ g_mutex_lock (&backend->priv->operation_lock);
+
+ if (backend->priv->next_operation_id == 0)
+ backend->priv->next_operation_id = 1;
+
+ opid = backend->priv->next_operation_id++;
+
+ g_hash_table_insert (
+ backend->priv->operation_ids,
+ GUINT_TO_POINTER (opid),
+ g_object_ref (simple));
+
+ g_mutex_unlock (&backend->priv->operation_lock);
+
+ return opid;
+}
+
+static GSimpleAsyncResult *
+book_backend_claim_operation (EBookBackend *backend,
+ guint32 opid)
+{
+ GSimpleAsyncResult *simple;
+
+ g_return_val_if_fail (opid > 0, NULL);
+
+ g_mutex_lock (&backend->priv->operation_lock);
+
+ simple = g_hash_table_lookup (
+ backend->priv->operation_ids,
+ GUINT_TO_POINTER (opid));
+
+ if (simple != NULL) {
+ /* Steal the hash table's reference. */
+ g_hash_table_steal (
+ backend->priv->operation_ids,
+ GUINT_TO_POINTER (opid));
+ }
+
+ g_mutex_unlock (&backend->priv->operation_lock);
+
+ return simple;
+}
+
+static void
+book_backend_set_default_cache_dir (EBookBackend *backend)
+{
+ ESource *source;
+ const gchar *user_cache_dir;
+ const gchar *uid;
+ gchar *filename;
+
+ user_cache_dir = e_get_user_cache_dir ();
+ source = e_backend_get_source (E_BACKEND (backend));
+
+ uid = e_source_get_uid (source);
+ g_return_if_fail (uid != NULL);
+
+ filename = g_build_filename (
+ user_cache_dir, "addressbook", uid, NULL);
+ e_book_backend_set_cache_dir (backend, filename);
+ g_free (filename);
+}
+
+static void
+book_backend_update_proxy_resolver (EBookBackend *backend)
+{
+ GProxyResolver *proxy_resolver = NULL;
+ ESourceAuthentication *extension;
+ ESource *source = NULL;
+ gboolean notify = FALSE;
+ gchar *uid;
+
+ extension = e_source_get_extension (
+ backend->priv->authentication_source,
+ E_SOURCE_EXTENSION_AUTHENTICATION);
+
+ uid = e_source_authentication_dup_proxy_uid (extension);
+ if (uid != NULL) {
+ ESourceRegistry *registry;
+
+ registry = e_book_backend_get_registry (backend);
+ source = e_source_registry_ref_source (registry, uid);
+ g_free (uid);
+ }
+
+ if (source != NULL) {
+ proxy_resolver = G_PROXY_RESOLVER (source);
+ if (!g_proxy_resolver_is_supported (proxy_resolver))
+ proxy_resolver = NULL;
+ }
+
+ g_mutex_lock (&backend->priv->property_lock);
+
+ /* Emitting a "notify" signal unnecessarily might have
+ * unwanted side effects like cancelling a SoupMessage.
+ * Only emit if we now have a different GProxyResolver. */
+
+ if (proxy_resolver != backend->priv->proxy_resolver) {
+ g_clear_object (&backend->priv->proxy_resolver);
+ backend->priv->proxy_resolver = proxy_resolver;
+
+ if (proxy_resolver != NULL)
+ g_object_ref (proxy_resolver);
+
+ notify = TRUE;
+ }
+
+ g_mutex_unlock (&backend->priv->property_lock);
+
+ if (notify)
+ g_object_notify (G_OBJECT (backend), "proxy-resolver");
+
+ g_clear_object (&source);
+}
+
+static void
+book_backend_auth_source_changed_cb (ESource *authentication_source,
+ GWeakRef *backend_weak_ref)
+{
+ EBookBackend *backend;
+
+ backend = g_weak_ref_get (backend_weak_ref);
+
+ if (backend != NULL) {
+ book_backend_update_proxy_resolver (backend);
+ g_object_unref (backend);
+ }
+}
+
+static void
+book_backend_set_registry (EBookBackend *backend,
+ ESourceRegistry *registry)
+{
+ g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
+ g_return_if_fail (backend->priv->registry == NULL);
+
+ backend->priv->registry = g_object_ref (registry);
+}
+
+static void
+book_backend_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_CACHE_DIR:
+ e_book_backend_set_cache_dir (
+ E_BOOK_BACKEND (object),
+ g_value_get_string (value));
+ return;
+
+ case PROP_REGISTRY:
+ book_backend_set_registry (
+ E_BOOK_BACKEND (object),
+ g_value_get_object (value));
+ return;
+
+ case PROP_WRITABLE:
+ e_book_backend_set_writable (
+ E_BOOK_BACKEND (object),
+ g_value_get_boolean (value));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+book_backend_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_CACHE_DIR:
+ g_value_take_string (
+ value, e_book_backend_dup_cache_dir (
+ E_BOOK_BACKEND (object)));
+ return;
+
+ case PROP_PROXY_RESOLVER:
+ g_value_take_object (
+ value, e_book_backend_ref_proxy_resolver (
+ E_BOOK_BACKEND (object)));
+ return;
+
+ case PROP_REGISTRY:
+ g_value_set_object (
+ value, e_book_backend_get_registry (
+ E_BOOK_BACKEND (object)));
+ return;
+
+ case PROP_WRITABLE:
+ g_value_set_boolean (
+ value, e_book_backend_get_writable (
+ E_BOOK_BACKEND (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+book_backend_dispose (GObject *object)
+{
+ EBookBackendPrivate *priv;
+
+ priv = E_BOOK_BACKEND_GET_PRIVATE (object);
+
+ if (priv->auth_source_changed_handler_id > 0) {
+ g_signal_handler_disconnect (
+ priv->authentication_source,
+ priv->auth_source_changed_handler_id);
+ priv->auth_source_changed_handler_id = 0;
+ }
+
+ g_clear_object (&priv->registry);
+ g_clear_object (&priv->data_book);
+ g_clear_object (&priv->proxy_resolver);
+ g_clear_object (&priv->authentication_source);
+
+ if (priv->views != NULL) {
+ g_list_free (priv->views);
+ priv->views = NULL;
+ }
+
+ g_hash_table_remove_all (priv->operation_ids);
+
+ while (!g_queue_is_empty (&priv->pending_operations))
+ g_object_unref (g_queue_pop_head (&priv->pending_operations));
+
+ g_clear_object (&priv->blocked);
+
+ /* Chain up to parent's dispose() method. */
+ G_OBJECT_CLASS (e_book_backend_parent_class)->dispose (object);
+}
+
+static void
+book_backend_finalize (GObject *object)
+{
+ EBookBackendPrivate *priv;
+
+ priv = E_BOOK_BACKEND_GET_PRIVATE (object);
+
+ g_mutex_clear (&priv->views_mutex);
+ g_mutex_clear (&priv->property_lock);
+
+ g_free (priv->cache_dir);
+
+ g_mutex_clear (&priv->operation_lock);
+ g_hash_table_destroy (priv->operation_ids);
+
+ /* Return immediately, do not wait. */
+ g_thread_pool_free (priv->thread_pool, TRUE, FALSE);
+
+ /* Chain up to parent's finalize() method. */
+ G_OBJECT_CLASS (e_book_backend_parent_class)->finalize (object);
+}
+
+static void
+book_backend_constructed (GObject *object)
+{
+ EBookBackend *backend;
+ ESourceRegistry *registry;
+ ESource *source;
+ gint max_threads = -1;
+ gboolean exclusive = FALSE;
+
+ /* Chain up to parent's constructed() method. */
+ G_OBJECT_CLASS (e_book_backend_parent_class)->constructed (object);
+
+ backend = E_BOOK_BACKEND (object);
+ registry = e_book_backend_get_registry (backend);
+ source = e_backend_get_source (E_BACKEND (backend));
+
+ /* If the backend specifies a serial dispatch queue, create
+ * a thread pool with one exclusive thread. The thread pool
+ * will serialize operations for us. */
+ if (E_BOOK_BACKEND_GET_CLASS (backend)->use_serial_dispatch_queue) {
+ max_threads = 1;
+ exclusive = TRUE;
+ }
+
+ /* XXX If creating an exclusive thread pool, technically there's
+ * a small chance of error here but we'll risk it since it's
+ * only for one exclusive thread. */
+ backend->priv->thread_pool = g_thread_pool_new (
+ (GFunc) book_backend_dispatch_thread,
+ NULL, max_threads, exclusive, NULL);
+
+ /* Initialize the "cache-dir" property. */
+ book_backend_set_default_cache_dir (backend);
+
+ /* Track the proxy resolver for this backend. */
+ backend->priv->authentication_source =
+ e_source_registry_find_extension (
+ registry, source, E_SOURCE_EXTENSION_AUTHENTICATION);
+ if (backend->priv->authentication_source != NULL) {
+ gulong handler_id;
+
+ handler_id = g_signal_connect_data (
+ backend->priv->authentication_source, "changed",
+ G_CALLBACK (book_backend_auth_source_changed_cb),
+ e_weak_ref_new (backend),
+ (GClosureNotify) e_weak_ref_free, 0);
+ backend->priv->auth_source_changed_handler_id = handler_id;
+
+ book_backend_update_proxy_resolver (backend);
+ }
+}
+
+static void
+book_backend_prepare_shutdown (EBackend *backend)
+{
+ GList *list, *l;
+
+ list = e_book_backend_list_views (E_BOOK_BACKEND (backend));
+
+ for (l = list; l != NULL; l = g_list_next (l)) {
+ EDataBookView *view = l->data;
+
+ e_book_backend_remove_view (E_BOOK_BACKEND (backend), view);
+ }
+
+ g_list_free_full (list, g_object_unref);
+
+ /* Chain up to parent's prepare_shutdown() method. */
+ E_BACKEND_CLASS (e_book_backend_parent_class)->prepare_shutdown (backend);
+}
+
+static gchar *
+book_backend_get_backend_property (EBookBackend *backend,
+ const gchar *prop_name)
+{
+ gchar *prop_value = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+ g_return_val_if_fail (prop_name != NULL, NULL);
+
+ if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_OPENED)) {
+ prop_value = g_strdup ("TRUE");
+
+ } else if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_OPENING)) {
+ prop_value = g_strdup ("FALSE");
+
+ } else if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_REVISION)) {
+ prop_value = g_strdup ("0");
+
+ } else if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_ONLINE)) {
+ gboolean online;
+
+ online = e_backend_get_online (E_BACKEND (backend));
+ prop_value = g_strdup (online ? "TRUE" : "FALSE");
+
+ } else if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_READONLY)) {
+ gboolean readonly;
+
+ readonly = e_book_backend_is_readonly (backend);
+ prop_value = g_strdup (readonly ? "TRUE" : "FALSE");
+
+ } else if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CACHE_DIR)) {
+ prop_value = e_book_backend_dup_cache_dir (backend);
+ }
+
+ return prop_value;
+}
+
+static gboolean
+book_backend_get_contact_list_uids_sync (EBookBackend *backend,
+ const gchar *query,
+ GQueue *out_uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookBackendClass *class;
+ GQueue queue = G_QUEUE_INIT;
+ gboolean success;
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_val_if_fail (class->get_contact_list_sync != NULL, FALSE);
+
+ success = class->get_contact_list_sync (
+ backend, query, &queue, cancellable, error);
+
+ if (success) {
+ while (!g_queue_is_empty (&queue)) {
+ EContact *contact;
+ gchar *uid;
+
+ contact = g_queue_pop_head (&queue);
+ uid = e_contact_get (contact, E_CONTACT_UID);
+ g_queue_push_tail (out_uids, uid);
+ g_object_unref (contact);
+ }
+ }
+
+ g_warn_if_fail (g_queue_is_empty (&queue));
+
+ return success;
+}
+
+static void
+book_backend_notify_update (EBookBackend *backend,
+ const EContact *contact)
+{
+ GList *list, *link;
+
+ list = e_book_backend_list_views (backend);
+
+ for (link = list; link != NULL; link = g_list_next (link)) {
+ EDataBookView *view = E_DATA_BOOK_VIEW (link->data);
+ e_data_book_view_notify_update (view, contact);
+ }
+
+ g_list_free_full (list, (GDestroyNotify) g_object_unref);
+}
+
+static void
+book_backend_shutdown (EBookBackend *backend)
+{
+ ESource *source;
+
+ source = e_backend_get_source (E_BACKEND (backend));
+
+ e_source_registry_debug_print (
+ "The %s instance for \"%s\" is shutting down.\n",
+ G_OBJECT_TYPE_NAME (backend),
+ e_source_get_display_name (source));
+}
+
+static void
+e_book_backend_class_init (EBookBackendClass *class)
+{
+ GObjectClass *object_class;
+ EBackendClass *backend_class;
+
+ g_type_class_add_private (class, sizeof (EBookBackendPrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->set_property = book_backend_set_property;
+ object_class->get_property = book_backend_get_property;
+ object_class->dispose = book_backend_dispose;
+ object_class->finalize = book_backend_finalize;
+ object_class->constructed = book_backend_constructed;
+
+ backend_class = E_BACKEND_CLASS (class);
+ backend_class->prepare_shutdown = book_backend_prepare_shutdown;
+
+ class->get_backend_property = book_backend_get_backend_property;
+ class->get_contact_list_uids_sync = book_backend_get_contact_list_uids_sync;
+ class->notify_update = book_backend_notify_update;
+ class->shutdown = book_backend_shutdown;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_CACHE_DIR,
+ g_param_spec_string (
+ "cache-dir",
+ "Cache Dir",
+ "The backend's cache directory",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_PROXY_RESOLVER,
+ g_param_spec_object (
+ "proxy-resolver",
+ "Proxy Resolver",
+ "The proxy resolver for this backend",
+ G_TYPE_PROXY_RESOLVER,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_REGISTRY,
+ g_param_spec_object (
+ "registry",
+ "Registry",
+ "Data source registry",
+ E_TYPE_SOURCE_REGISTRY,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_WRITABLE,
+ g_param_spec_boolean (
+ "writable",
+ "Writable",
+ "Whether the backend will accept changes",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * EBookBackend::closed:
+ * @backend: the #EBookBackend which emitted the signal
+ * @sender: the bus name that invoked the "close" method
+ *
+ * Emitted when a client destroys its #EBookClient for @backend.
+ *
+ * Since: 3.10
+ **/
+ signals[CLOSED] = g_signal_new (
+ "closed",
+ G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (EBookBackendClass, closed),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ G_TYPE_STRING);
+
+ /**
+ * EBookBackend::shutdown:
+ * @backend: the #EBookBackend which emitted the signal
+ *
+ * Emitted when the last client destroys its #EBookClient for
+ * @backend. This signals the @backend to begin final cleanup
+ * tasks such as synchronizing data to permanent storage.
+ *
+ * Since: 3.10
+ **/
+ signals[SHUTDOWN] = g_signal_new (
+ "shutdown",
+ G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (EBookBackendClass, shutdown),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+static void
+e_book_backend_init (EBookBackend *backend)
+{
+ backend->priv = E_BOOK_BACKEND_GET_PRIVATE (backend);
+
+ backend->priv->views = NULL;
+ g_mutex_init (&backend->priv->views_mutex);
+ g_mutex_init (&backend->priv->property_lock);
+ g_mutex_init (&backend->priv->operation_lock);
+
+ backend->priv->operation_ids = g_hash_table_new_full (
+ (GHashFunc) g_direct_hash,
+ (GEqualFunc) g_direct_equal,
+ (GDestroyNotify) NULL,
+ (GDestroyNotify) g_object_unref);
+}
+
+/**
+ * e_book_backend_get_cache_dir:
+ * @backend: an #EBookBackend
+ *
+ * Returns the cache directory path used by @backend.
+ *
+ * Returns: the cache directory path
+ *
+ * Since: 2.32
+ **/
+const gchar *
+e_book_backend_get_cache_dir (EBookBackend *backend)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+
+ return backend->priv->cache_dir;
+}
+
+/**
+ * e_book_backend_dup_cache_dir:
+ * @backend: an #EBookBackend
+ *
+ * Thread-safe variation of e_book_backend_get_cache_dir().
+ * Use this function when accessing @backend from multiple threads.
+ *
+ * The returned string should be freed with g_free() when no longer needed.
+ *
+ * Returns: a newly-allocated copy of #EBookBackend:cache-dir
+ *
+ * Since: 3.10
+ **/
+gchar *
+e_book_backend_dup_cache_dir (EBookBackend *backend)
+{
+ const gchar *protected;
+ gchar *duplicate;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+
+ g_mutex_lock (&backend->priv->property_lock);
+
+ protected = e_book_backend_get_cache_dir (backend);
+ duplicate = g_strdup (protected);
+
+ g_mutex_unlock (&backend->priv->property_lock);
+
+ return duplicate;
+}
+
+/**
+ * e_book_backend_set_cache_dir:
+ * @backend: an #EBookBackend
+ * @cache_dir: a local cache directory path
+ *
+ * Sets the cache directory path for use by @backend.
+ *
+ * Note that #EBookBackend is initialized with a default cache directory
+ * path which should suffice for most cases. Backends should not override
+ * the default path without good reason.
+ *
+ * Since: 2.32
+ **/
+void
+e_book_backend_set_cache_dir (EBookBackend *backend,
+ const gchar *cache_dir)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (cache_dir != NULL);
+
+ g_mutex_lock (&backend->priv->property_lock);
+
+ if (g_strcmp0 (backend->priv->cache_dir, cache_dir) == 0) {
+ g_mutex_unlock (&backend->priv->property_lock);
+ return;
+ }
+
+ g_free (backend->priv->cache_dir);
+ backend->priv->cache_dir = g_strdup (cache_dir);
+
+ g_mutex_unlock (&backend->priv->property_lock);
+
+ g_object_notify (G_OBJECT (backend), "cache-dir");
+}
+
+/**
+ * e_book_backend_ref_data_book:
+ * @backend: an #EBookBackend
+ *
+ * Returns the #EDataBook for @backend. The #EDataBook is essentially
+ * the glue between incoming D-Bus requests and @backend's native API.
+ *
+ * An #EDataBook should be set only once after @backend is first created.
+ * If an #EDataBook has not yet been set, the function returns %NULL.
+ *
+ * The returned #EDataBook is referenced for thread-safety and must be
+ * unreferenced with g_object_unref() when finished with it.
+ *
+ * Returns: an #EDataBook, or %NULL
+ *
+ * Since: 3.10
+ **/
+EDataBook *
+e_book_backend_ref_data_book (EBookBackend *backend)
+{
+ EDataBook *data_book = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+
+ if (backend->priv->data_book != NULL)
+ data_book = g_object_ref (backend->priv->data_book);
+
+ return data_book;
+}
+
+/**
+ * e_book_backend_set_data_book:
+ * @backend: an #EBookBackend
+ * @data_book: an #EDataBook
+ *
+ * Sets the #EDataBook for @backend. The #EDataBook is essentially the
+ * glue between incoming D-Bus requests and @backend's native API.
+ *
+ * An #EDataBook should be set only once after @backend is first created.
+ *
+ * Since: 3.10
+ **/
+void
+e_book_backend_set_data_book (EBookBackend *backend,
+ EDataBook *data_book)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (E_IS_DATA_BOOK (data_book));
+
+ /* This should be set only once. Warn if not. */
+ g_warn_if_fail (backend->priv->data_book == NULL);
+
+ backend->priv->data_book = g_object_ref (data_book);
+}
+
+/**
+ * e_book_backend_ref_proxy_resolver:
+ * @backend: an #EBookBackend
+ *
+ * Returns the #GProxyResolver for @backend (if applicable), as indicated
+ * by the #ESourceAuthentication:proxy-uid of @backend's #EBackend:source
+ * or one of its ancestors.
+ *
+ * The returned #GProxyResolver is referenced for thread-safety and must
+ * be unreferenced with g_object_unref() when finished with it.
+ *
+ * Returns: a #GProxyResolver, or %NULL
+ *
+ * Since: 3.12
+ **/
+GProxyResolver *
+e_book_backend_ref_proxy_resolver (EBookBackend *backend)
+{
+ GProxyResolver *proxy_resolver = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+
+ g_mutex_lock (&backend->priv->property_lock);
+
+ if (backend->priv->proxy_resolver != NULL)
+ proxy_resolver = g_object_ref (backend->priv->proxy_resolver);
+
+ g_mutex_unlock (&backend->priv->property_lock);
+
+ return proxy_resolver;
+}
+
+/**
+ * e_book_backend_get_registry:
+ * @backend: an #EBookBackend
+ *
+ * Returns the data source registry to which #EBackend:source belongs.
+ *
+ * Returns: an #ESourceRegistry
+ *
+ * Since: 3.6
+ **/
+ESourceRegistry *
+e_book_backend_get_registry (EBookBackend *backend)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+
+ return backend->priv->registry;
+}
+
+/**
+ * e_book_backend_get_writable:
+ * @backend: an #EBookBackend
+ *
+ * Returns whether @backend will accept changes to its data content.
+ *
+ * Returns: whether @backend is writable
+ *
+ * Since: 3.8
+ **/
+gboolean
+e_book_backend_get_writable (EBookBackend *backend)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+
+ return backend->priv->writable;
+}
+
+/**
+ * e_book_backend_set_writable:
+ * @backend: an #EBookBackend
+ * @writable: whether @backend is writable
+ *
+ * Sets whether @backend will accept changes to its data content.
+ *
+ * Since: 3.8
+ **/
+void
+e_book_backend_set_writable (EBookBackend *backend,
+ gboolean writable)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+
+ if (writable == backend->priv->writable)
+ return;
+
+ backend->priv->writable = writable;
+
+ g_object_notify (G_OBJECT (backend), "writable");
+}
+
+/**
+ * e_book_backend_open_sync:
+ * @backend: an #EBookBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * "Opens" the @backend. Opening a backend is something of an outdated
+ * concept, but the operation is hanging around for a little while longer.
+ * This usually involves some custom initialization logic, and testing of
+ * remote authentication if applicable.
+ *
+ * If an error occurs, the function will set @error and return %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_open_sync (EBookBackend *backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EAsyncClosure *closure;
+ GAsyncResult *result;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+
+ closure = e_async_closure_new ();
+
+ e_book_backend_open (
+ backend, cancellable,
+ e_async_closure_callback, closure);
+
+ result = e_async_closure_wait (closure);
+
+ success = e_book_backend_open_finish (backend, result, error);
+
+ e_async_closure_free (closure);
+
+ return success;
+}
+
+/* Helper for e_book_backend_open() */
+static void
+book_backend_open_thread (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->open_sync != NULL);
+
+ if (!e_book_backend_is_opened (backend)) {
+ GError *error = NULL;
+
+ e_backend_ensure_online_state_updated (E_BACKEND (backend), cancellable);
+
+ class->open_sync (backend, cancellable, &error);
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ /* XXX Once we get rid of the old-style API we can dispatch
+ * methods using g_simple_async_result_run_in_thread(),
+ * which completes the GSimpleAsyncResult for us. */
+ g_simple_async_result_complete_in_idle (simple);
+}
+
+/* Helper for e_book_backend_open() */
+static void
+book_backend_open_thread_old_style (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ EDataBook *data_book;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->open != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+ g_return_if_fail (data_book != NULL);
+
+ if (e_book_backend_is_opened (backend)) {
+ g_simple_async_result_complete_in_idle (simple);
+
+ } else {
+ guint32 opid;
+
+ opid = book_backend_stash_operation (backend, simple);
+
+ e_backend_ensure_online_state_updated (E_BACKEND (backend), cancellable);
+
+ class->open (backend, data_book, opid, cancellable, FALSE);
+ }
+
+ g_object_unref (data_book);
+}
+
+/**
+ * e_book_backend_open:
+ * @backend: an #EBookBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: data to pass to the callback function
+ *
+ * Asynchronously "opens" the @backend. Opening a backend is something of
+ * an outdated concept, but the operation is hanging around for a little
+ * while longer. This usually involves some custom initialization logic,
+ * and testing of remote authentication if applicable.
+ *
+ * When the operation is finished, @callback will be called. You can then
+ * call e_book_backend_open_finish() to get the result of the operation.
+ *
+ * Since: 3.10
+ **/
+void
+e_book_backend_open (EBookBackend *backend,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ EBookBackendClass *class;
+ GSimpleAsyncResult *simple;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+
+ simple = g_simple_async_result_new (
+ G_OBJECT (backend), callback,
+ user_data, e_book_backend_open);
+
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ if (class->open_sync != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, TRUE,
+ book_backend_open_thread);
+ book_backend_dispatch_next_operation (backend);
+
+ } else if (class->open != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, TRUE,
+ book_backend_open_thread_old_style);
+ book_backend_dispatch_next_operation (backend);
+
+ } else {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_SUPPORTED));
+ g_simple_async_result_complete_in_idle (simple);
+ }
+
+ g_object_unref (simple);
+}
+
+/**
+ * e_book_backend_open_finish:
+ * @backend: an #EBookBackend
+ * @result: a #GAsyncResult
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finishes the operation started with e_book_backend_open().
+ *
+ * If an error occurred, the function will set @error and return %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_open_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+
+ g_return_val_if_fail (
+ g_simple_async_result_is_valid (
+ result, G_OBJECT (backend),
+ e_book_backend_open), FALSE);
+
+ simple = G_SIMPLE_ASYNC_RESULT (result);
+
+ book_backend_unblock_operations (backend, simple);
+
+ if (g_simple_async_result_propagate_error (simple, error))
+ return FALSE;
+
+ backend->priv->opened = TRUE;
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_refresh_sync:
+ * @backend: an #EBookBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Initiates a refresh for @backend, if the @backend supports refreshing.
+ * The actual refresh operation completes on its own time. This function
+ * merely initiates the operation.
+ *
+ * If an error occurs while initiating the refresh, the function will set
+ * @error and return %FALSE. If the @backend does not support refreshing,
+ * the function will set an %E_CLIENT_ERROR_NOT_SUPPORTED error and return
+ * %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_refresh_sync (EBookBackend *backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EAsyncClosure *closure;
+ GAsyncResult *result;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+
+ closure = e_async_closure_new ();
+
+ e_book_backend_refresh (
+ backend, cancellable,
+ e_async_closure_callback, closure);
+
+ result = e_async_closure_wait (closure);
+
+ success = e_book_backend_refresh_finish (backend, result, error);
+
+ e_async_closure_free (closure);
+
+ return success;
+}
+
+/* Helper for e_book_backend_refresh() */
+static void
+book_backend_refresh_thread (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->refresh_sync != NULL);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+
+ } else {
+ GError *error = NULL;
+
+ class->refresh_sync (backend, cancellable, &error);
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ /* XXX Once we get rid of the old-style API we can dispatch
+ * methods using g_simple_async_result_run_in_thread(),
+ * which completes the GSimpleAsyncResult for us. */
+ g_simple_async_result_complete_in_idle (simple);
+}
+
+/* Helper for e_book_backend_refresh() */
+static void
+book_backend_refresh_thread_old_style (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ EDataBook *data_book;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->refresh != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+ g_return_if_fail (data_book != NULL);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+ g_simple_async_result_complete_in_idle (simple);
+
+ } else {
+ guint32 opid;
+
+ opid = book_backend_stash_operation (backend, simple);
+
+ class->refresh (backend, data_book, opid, cancellable);
+ }
+
+ g_object_unref (data_book);
+}
+
+/**
+ * e_book_backend_refresh:
+ * @backend: an #EBookBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: data to pass to the callback function
+ *
+ * Asynchronously initiates a refresh for @backend, if the @backend supports
+ * refreshing. The actual refresh operation completes on its own time. This
+ * function, along with e_book_backend_refresh_finish(), merely initiates the
+ * operation.
+ *
+ * Once the refresh is initiated, @callback will be called. You can then
+ * call e_book_backend_refresh_finish() to get the result of the initiation.
+ *
+ * Since: 3.10
+ **/
+void
+e_book_backend_refresh (EBookBackend *backend,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ EBookBackendClass *class;
+ GSimpleAsyncResult *simple;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+
+ simple = g_simple_async_result_new (
+ G_OBJECT (backend), callback,
+ user_data, e_book_backend_refresh);
+
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ if (class->refresh_sync != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_refresh_thread);
+ book_backend_dispatch_next_operation (backend);
+
+ } else if (class->refresh != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_refresh_thread_old_style);
+ book_backend_dispatch_next_operation (backend);
+
+ } else {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_SUPPORTED));
+ g_simple_async_result_complete_in_idle (simple);
+ }
+
+ g_object_unref (simple);
+}
+
+/**
+ * e_book_backend_refresh_finish:
+ * @backend: an #EBookBackend
+ * @result: a #GAsyncResult
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finishes the refresh initiation started with e_book_backend_refresh().
+ *
+ * If an error occurred while initiating the refresh, the function will set
+ * @error and return %FALSE. If the @backend does not support refreshing,
+ * the function will set an %E_CLIENT_ERROR_NOT_SUPPORTED error and return
+ * %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_refresh_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+
+ g_return_val_if_fail (
+ g_simple_async_result_is_valid (
+ result, G_OBJECT (backend),
+ e_book_backend_refresh), FALSE);
+
+ simple = G_SIMPLE_ASYNC_RESULT (result);
+
+ book_backend_unblock_operations (backend, simple);
+
+ /* Assume success unless a GError is set. */
+ return !g_simple_async_result_propagate_error (simple, error);
+}
+
+/**
+ * e_book_backend_create_contacts_sync:
+ * @backend: an #EBookBackend
+ * @vcards: a %NULL-terminated array of vCard strings
+ * @out_contacts: a #GQueue in which to deposit results
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates one or more new contacts from @vcards, and deposits an #EContact
+ * instance for each newly-created contact in @out_contacts.
+ *
+ * The returned #EContact instances are referenced for thread-safety and
+ * must be unreferenced with g_object_unref() when finished with them.
+ *
+ * If an error occurs, the function will set @error and return %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_create_contacts_sync (EBookBackend *backend,
+ const gchar * const *vcards,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EAsyncClosure *closure;
+ GAsyncResult *result;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+ g_return_val_if_fail (vcards != NULL, FALSE);
+ g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+ closure = e_async_closure_new ();
+
+ e_book_backend_create_contacts (
+ backend, vcards, cancellable,
+ e_async_closure_callback, closure);
+
+ result = e_async_closure_wait (closure);
+
+ success = e_book_backend_create_contacts_finish (
+ backend, result, out_contacts, error);
+
+ e_async_closure_free (closure);
+
+ return success;
+}
+
+/* Helper for e_book_backend_create_contacts() */
+static void
+book_backend_create_contacts_thread (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->create_contacts_sync != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+
+ } else {
+ GError *error = NULL;
+
+ class->create_contacts_sync (
+ backend,
+ (const gchar * const *) async_context->strv,
+ async_context->object_queue,
+ cancellable, &error);
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ /* XXX Once we get rid of the old-style API we can dispatch
+ * methods using g_simple_async_result_run_in_thread(),
+ * which completes the GSimpleAsyncResult for us. */
+ g_simple_async_result_complete_in_idle (simple);
+}
+
+/* Helper for e_book_backend_create_contacts() */
+static void
+book_backend_create_contacts_thread_old_style (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ EDataBook *data_book;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->create_contacts != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+ g_return_if_fail (data_book != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+ g_simple_async_result_complete_in_idle (simple);
+
+ } else {
+ GSList *list = NULL;
+ guint32 opid;
+ guint ii;
+
+ /* This is so the finish function knows which method
+ * was invoked and can gather results appropriately. */
+ async_context->old_style = TRUE;
+
+ opid = book_backend_stash_operation (backend, simple);
+
+ /* The AsyncContext retains ownership of the strings. */
+ for (ii = 0; async_context->strv[ii] != NULL; ii++)
+ list = g_slist_prepend (list, async_context->strv[ii]);
+ list = g_slist_reverse (list);
+
+ class->create_contacts (
+ backend, data_book, opid, cancellable, list);
+
+ g_slist_free (list);
+ }
+
+ g_object_unref (data_book);
+}
+
+/**
+ * e_book_backend_create_contacts
+ * @backend: an #EBookBackend
+ * @vcards: a %NULL-terminated array of vCard strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: data to pass to the callback function
+ *
+ * Asynchronously creates one or more new contacts from @vcards.
+ *
+ * When the operation is finished, @callback will be called. You can then
+ * call e_book_backend_create_contacts_finish() to get the result of the
+ * operation.
+ *
+ * Since: 3.10
+ **/
+void
+e_book_backend_create_contacts (EBookBackend *backend,
+ const gchar * const *vcards,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ EBookBackendClass *class;
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (vcards != NULL);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+
+ async_context = g_slice_new0 (AsyncContext);
+ async_context->strv = g_strdupv ((gchar **) vcards);
+ async_context->object_queue = &async_context->result_queue;
+
+ simple = g_simple_async_result_new (
+ G_OBJECT (backend), callback, user_data,
+ e_book_backend_create_contacts);
+
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ g_simple_async_result_set_op_res_gpointer (
+ simple, async_context, (GDestroyNotify) async_context_free);
+
+ if (class->create_contacts_sync != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_create_contacts_thread);
+ book_backend_dispatch_next_operation (backend);
+
+ } else if (class->create_contacts != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_create_contacts_thread_old_style);
+ book_backend_dispatch_next_operation (backend);
+
+ } else {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_SUPPORTED));
+ g_simple_async_result_complete_in_idle (simple);
+ }
+
+ g_object_unref (simple);
+}
+
+/**
+ * e_book_backend_create_contacts_finish:
+ * @backend: an #EBookBackend
+ * @result: a #GAsyncResult
+ * @out_contacts: a #GQueue in which to deposit results
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finishes the operation started with e_book_backend_create_contacts().
+ *
+ * An #EContact instance for each newly-created contact is deposited in
+ * @out_contacts. The returned #EContact instances are referenced for
+ * thread-safety and must be unreferenced with g_object_unref() when
+ * finished with them.
+ *
+ * If an error occurred, the function will set @error and return %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_create_contacts_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GQueue *out_contacts,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_val_if_fail (
+ g_simple_async_result_is_valid (
+ result, G_OBJECT (backend),
+ e_book_backend_create_contacts), FALSE);
+ g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+ simple = G_SIMPLE_ASYNC_RESULT (result);
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ book_backend_unblock_operations (backend, simple);
+
+ if (g_simple_async_result_propagate_error (simple, error))
+ return FALSE;
+
+ while (!g_queue_is_empty (async_context->object_queue)) {
+ EContact *contact;
+
+ contact = g_queue_pop_head (async_context->object_queue);
+ g_queue_push_tail (out_contacts, g_object_ref (contact));
+ e_book_backend_notify_update (backend, contact);
+ g_object_unref (contact);
+ }
+
+ e_book_backend_notify_complete (backend);
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_modify_contacts_sync:
+ * @backend: an #EBookBackend
+ * @vcards: a %NULL-terminated array of vCard strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Modifies one or more contacts according to @vcards.
+ *
+ * If an error occurs, the function will set @error and return %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_modify_contacts_sync (EBookBackend *backend,
+ const gchar * const *vcards,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EAsyncClosure *closure;
+ GAsyncResult *result;
+ gboolean success;
+
+ closure = e_async_closure_new ();
+
+ e_book_backend_modify_contacts (
+ backend, vcards, cancellable,
+ e_async_closure_callback, closure);
+
+ result = e_async_closure_wait (closure);
+
+ success = e_book_backend_modify_contacts_finish (
+ backend, result, error);
+
+ e_async_closure_free (closure);
+
+ return success;
+}
+
+/* Helper for e_book_backend_modify_contacts() */
+static void
+book_backend_modify_contacts_thread (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->modify_contacts_sync != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+
+ } else {
+ GError *error = NULL;
+
+ class->modify_contacts_sync (
+ backend,
+ (const gchar * const *) async_context->strv,
+ async_context->object_queue,
+ cancellable, &error);
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ /* XXX Once we get rid of the old-style API we can dispatch
+ * methods using g_simple_async_result_run_in_thread(),
+ * which completes the GSimpleAsyncResult for us. */
+ g_simple_async_result_complete_in_idle (simple);
+}
+
+/* Helper for e_book_backend_modify_contacts() */
+static void
+book_backend_modify_contacts_thread_old_style (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ EDataBook *data_book;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->modify_contacts != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+ g_return_if_fail (data_book != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+ g_simple_async_result_complete_in_idle (simple);
+
+ } else {
+ GSList *list = NULL;
+ guint32 opid;
+ guint ii;
+
+ /* This is so the finish function knows which method
+ * was invoked and can gather results appropriately. */
+ async_context->old_style = TRUE;
+
+ opid = book_backend_stash_operation (backend, simple);
+
+ /* The AsyncContext retains ownership of the strings. */
+ for (ii = 0; async_context->strv[ii] != NULL; ii++)
+ list = g_slist_prepend (list, async_context->strv[ii]);
+ list = g_slist_reverse (list);
+
+ class->modify_contacts (
+ backend, data_book, opid, cancellable, list);
+
+ g_slist_free (list);
+ }
+
+ g_object_unref (data_book);
+}
+
+/**
+ * e_book_backend_modify_contacts:
+ * @backend: an #EBookBackend
+ * @vcards: a %NULL-terminated array of vCard strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: data to pass to the callback function
+ *
+ * Asynchronously modifies one or more contacts according to @vcards.
+ *
+ * When the operation is finished, @callback will be called. You can then
+ * call e_book_backend_modify_contacts_finish() to get the result of the
+ * operation.
+ *
+ * Since: 3.10
+ **/
+void
+e_book_backend_modify_contacts (EBookBackend *backend,
+ const gchar * const *vcards,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ EBookBackendClass *class;
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (vcards != NULL);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+
+ async_context = g_slice_new0 (AsyncContext);
+ async_context->strv = g_strdupv ((gchar **) vcards);
+ async_context->object_queue = &async_context->result_queue;
+
+ simple = g_simple_async_result_new (
+ G_OBJECT (backend), callback, user_data,
+ e_book_backend_modify_contacts);
+
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ g_simple_async_result_set_op_res_gpointer (
+ simple, async_context, (GDestroyNotify) async_context_free);
+
+ if (class->modify_contacts_sync != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_modify_contacts_thread);
+ book_backend_dispatch_next_operation (backend);
+
+ } else if (class->modify_contacts != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_modify_contacts_thread_old_style);
+ book_backend_dispatch_next_operation (backend);
+
+ } else {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_SUPPORTED));
+ g_simple_async_result_complete_in_idle (simple);
+ }
+
+ g_object_unref (simple);
+}
+
+/**
+ * e_book_backend_modify_contacts_finish:
+ * @backend: an #EBookBackend
+ * @result: a #GAsyncResult
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finishes the operation started with e_book_backend_modify_contacts().
+ *
+ * If an error occurred, the function will set @error and return %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_modify_contacts_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_val_if_fail (
+ g_simple_async_result_is_valid (
+ result, G_OBJECT (backend),
+ e_book_backend_modify_contacts), FALSE);
+
+ simple = G_SIMPLE_ASYNC_RESULT (result);
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ book_backend_unblock_operations (backend, simple);
+
+ if (g_simple_async_result_propagate_error (simple, error))
+ return FALSE;
+
+ while (!g_queue_is_empty (async_context->object_queue)) {
+ EContact *contact;
+
+ contact = g_queue_pop_head (async_context->object_queue);
+ e_book_backend_notify_update (backend, contact);
+ g_object_unref (contact);
+ }
+
+ e_book_backend_notify_complete (backend);
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_remove_contacts_sync:
+ * @backend: an #EBookBackend
+ * @uids: a %NULL-terminated array of contact ID strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes one or more contacts according to @uids.
+ *
+ * If an error occurs, the function will set @error and return %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_remove_contacts_sync (EBookBackend *backend,
+ const gchar * const *uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EAsyncClosure *closure;
+ GAsyncResult *result;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+ g_return_val_if_fail (uids != NULL, FALSE);
+
+ closure = e_async_closure_new ();
+
+ e_book_backend_remove_contacts (
+ backend, uids, cancellable,
+ e_async_closure_callback, closure);
+
+ result = e_async_closure_wait (closure);
+
+ success = e_book_backend_remove_contacts_finish (
+ backend, result, error);
+
+ e_async_closure_free (closure);
+
+ return success;
+}
+
+/* Helper for e_book_backend_remove_contacts() */
+static void
+book_backend_remove_contacts_thread (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->remove_contacts_sync != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+
+ } else {
+ GError *error = NULL;
+
+ class->remove_contacts_sync (
+ backend,
+ (const gchar * const *) async_context->strv,
+ cancellable, &error);
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ /* XXX Once we get rid of the old-style API we can dispatch
+ * methods using g_simple_async_result_run_in_thread(),
+ * which completes the GSimpleAsyncResult for us. */
+ g_simple_async_result_complete_in_idle (simple);
+}
+
+/* Helper for e_book_backend_remove_contacts() */
+static void
+book_backend_remove_contacts_thread_old_style (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ EDataBook *data_book;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->remove_contacts != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+ g_return_if_fail (data_book != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+ g_simple_async_result_complete_in_idle (simple);
+
+ } else {
+ GSList *list = NULL;
+ guint32 opid;
+ guint ii;
+
+ /* This is so the finish function knows which method
+ * was invoked and can gather results appropriately. */
+ async_context->old_style = TRUE;
+
+ opid = book_backend_stash_operation (backend, simple);
+
+ /* The AsyncContext retains ownership of the strings. */
+ for (ii = 0; async_context->strv[ii] != NULL; ii++)
+ list = g_slist_prepend (list, async_context->strv[ii]);
+ list = g_slist_reverse (list);
+
+ class->remove_contacts (
+ backend, data_book, opid, cancellable, list);
+
+ g_slist_free (list);
+ }
+
+ g_object_unref (data_book);
+}
+
+/**
+ * e_book_backend_remove_contacts:
+ * @backend: an #EBookBackend
+ * @uids: a %NULL-terminated array of contact ID strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: data to pass to the callback function
+ *
+ * Asynchronously removes one or more contacts according to @uids.
+ *
+ * When the operation is finished, @callback will be called. You can then
+ * call e_book_backend_remove_contacts_finish() to get the result of the
+ * operation.
+ *
+ * Since: 3.10
+ **/
+void
+e_book_backend_remove_contacts (EBookBackend *backend,
+ const gchar * const *uids,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ EBookBackendClass *class;
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (uids != NULL);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+
+ async_context = g_slice_new0 (AsyncContext);
+ async_context->strv = g_strdupv ((gchar **) uids);
+ async_context->string_queue = &async_context->result_queue;
+
+ simple = g_simple_async_result_new (
+ G_OBJECT (backend), callback, user_data,
+ e_book_backend_remove_contacts);
+
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ g_simple_async_result_set_op_res_gpointer (
+ simple, async_context, (GDestroyNotify) async_context_free);
+
+ if (class->remove_contacts_sync != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_remove_contacts_thread);
+ book_backend_dispatch_next_operation (backend);
+
+ } else if (class->remove_contacts != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_remove_contacts_thread_old_style);
+ book_backend_dispatch_next_operation (backend);
+
+ } else {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_SUPPORTED));
+ g_simple_async_result_complete_in_idle (simple);
+ }
+
+ g_object_unref (simple);
+}
+
+/**
+ * e_book_backend_remove_contacts_finish:
+ * @backend: an #EBookBackend
+ * @result: a #GAsyncResult
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finishes the operation started with e_book_backend_remove_contacts().
+ *
+ * If an error occurred, the function will set @error and return %FALSE.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_remove_contacts_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+ guint ii;
+
+ g_return_val_if_fail (
+ g_simple_async_result_is_valid (
+ result, G_OBJECT (backend),
+ e_book_backend_remove_contacts), FALSE);
+
+ simple = G_SIMPLE_ASYNC_RESULT (result);
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ book_backend_unblock_operations (backend, simple);
+
+ if (g_simple_async_result_propagate_error (simple, error))
+ return FALSE;
+
+ for (ii = 0; async_context->strv[ii] != NULL; ii++) {
+ const gchar *uid = async_context->strv[ii];
+ e_book_backend_notify_remove (backend, uid);
+ }
+
+ e_book_backend_notify_complete (backend);
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_get_contact_sync:
+ * @backend: an #EBookBackend
+ * @uid: a contact ID
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Obtains an #EContact for @uid.
+ *
+ * The returned #EContact is referenced for thread-safety and must be
+ * unreferenced with g_object_unref() when finished with it.
+ *
+ * If an error occurs, the function will set @error and return %NULL.
+ *
+ * Returns: an #EContact, or %NULL
+ *
+ * Since: 3.10
+ **/
+EContact *
+e_book_backend_get_contact_sync (EBookBackend *backend,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EAsyncClosure *closure;
+ GAsyncResult *result;
+ EContact *contact;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ closure = e_async_closure_new ();
+
+ e_book_backend_get_contact (
+ backend, uid, cancellable,
+ e_async_closure_callback, closure);
+
+ result = e_async_closure_wait (closure);
+
+ contact = e_book_backend_get_contact_finish (
+ backend, result, error);
+
+ e_async_closure_free (closure);
+
+ return contact;
+}
+
+/* Helper for e_book_backend_get_contact() */
+static void
+book_backend_get_contact_thread (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->get_contact_sync != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+
+ } else {
+ GError *error = NULL;
+
+ async_context->contact = class->get_contact_sync (
+ backend,
+ async_context->uid,
+ cancellable, &error);
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ /* XXX Once we get rid of the old-style API we can dispatch
+ * methods using g_simple_async_result_run_in_thread(),
+ * which completes the GSimpleAsyncResult for us. */
+ g_simple_async_result_complete_in_idle (simple);
+}
+
+/* Helper for e_book_backend_get_contact() */
+static void
+book_backend_get_contact_thread_old_style (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ EDataBook *data_book;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->get_contact != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+ g_return_if_fail (data_book != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+ g_simple_async_result_complete_in_idle (simple);
+
+ } else {
+ guint32 opid;
+
+ /* This is so the finish function knows which method
+ * was invoked and can gather results appropriately. */
+ async_context->old_style = TRUE;
+
+ opid = book_backend_stash_operation (backend, simple);
+
+ class->get_contact (
+ backend, data_book, opid, cancellable,
+ async_context->uid);
+ }
+
+ g_object_unref (data_book);
+}
+
+/**
+ * e_book_backend_get_contact:
+ * @backend: an #EBookBackend
+ * @uid: a contact ID
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: data to pass to the callback function
+ *
+ * Asynchronously obtains an #EContact for @uid.
+ *
+ * When the operation is finished, @callback will be called. You can
+ * then call e_book_backend_get_contact_finish() to get the result of the
+ * operation.
+ *
+ * Since: 3.10
+ **/
+void
+e_book_backend_get_contact (EBookBackend *backend,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ EBookBackendClass *class;
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (uid != NULL);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+
+ async_context = g_slice_new0 (AsyncContext);
+ async_context->uid = g_strdup (uid);
+ async_context->object_queue = &async_context->result_queue;
+
+ simple = g_simple_async_result_new (
+ G_OBJECT (backend), callback, user_data,
+ e_book_backend_get_contact);
+
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ g_simple_async_result_set_op_res_gpointer (
+ simple, async_context, (GDestroyNotify) async_context_free);
+
+ if (class->get_contact_sync != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_get_contact_thread);
+ book_backend_dispatch_next_operation (backend);
+
+ } else if (class->get_contact != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_get_contact_thread_old_style);
+ book_backend_dispatch_next_operation (backend);
+
+ } else {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_SUPPORTED));
+ g_simple_async_result_complete_in_idle (simple);
+ }
+
+ g_object_unref (simple);
+}
+
+/**
+ * e_book_backend_get_contact_finish:
+ * @backend: an #EBookBackend
+ * @result: a #GAsyncResult
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finishes the operation started with e_book_backend_get_contact_finish().
+ *
+ * The returned #EContact is referenced for thread-safety and must be
+ * unreferenced with g_object_unref() when finished with it.
+ *
+ * If an error occurred, the function will set @error and return %NULL.
+ *
+ * Returns: an #EContact, or %NULL
+ *
+ * Since: 3.10
+ **/
+EContact *
+e_book_backend_get_contact_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_val_if_fail (
+ g_simple_async_result_is_valid (
+ result, G_OBJECT (backend),
+ e_book_backend_get_contact), NULL);
+
+ simple = G_SIMPLE_ASYNC_RESULT (result);
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ book_backend_unblock_operations (backend, simple);
+
+ if (g_simple_async_result_propagate_error (simple, error))
+ return NULL;
+
+ /* XXX e_data_book_respond_get_contact() stuffs the
+ * resulting EContact into the object queue. */
+ if (async_context->old_style) {
+ GQueue *queue = async_context->object_queue;
+ g_warn_if_fail (async_context->contact == NULL);
+ async_context->contact = g_queue_pop_head (queue);
+ g_warn_if_fail (g_queue_is_empty (queue));
+ }
+
+ g_return_val_if_fail (E_IS_CONTACT (async_context->contact), NULL);
+
+ return g_object_ref (async_context->contact);
+}
+
+/**
+ * e_book_backend_get_contact_list_sync:
+ * @backend: an #EBookBackend
+ * @query: a search query in S-expression format
+ * @out_contacts: a #GQueue in which to deposit results
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Obtains a set of #EContact instances which satisfy the criteria specified
+ * in @query, and deposits them in @out_contacts.
+ *
+ * The returned #EContact instances are referenced for thread-safety and
+ * must be unreferenced with g_object_unref() when finished with them.
+ *
+ * If an error occurs, the function will set @error and return %FALSE.
+ * Note that an empty result set does not necessarily imply an error.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_get_contact_list_sync (EBookBackend *backend,
+ const gchar *query,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EAsyncClosure *closure;
+ GAsyncResult *result;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+ g_return_val_if_fail (query != NULL, FALSE);
+ g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+ closure = e_async_closure_new ();
+
+ e_book_backend_get_contact_list (
+ backend, query, cancellable,
+ e_async_closure_callback, closure);
+
+ result = e_async_closure_wait (closure);
+
+ success = e_book_backend_get_contact_list_finish (
+ backend, result, out_contacts, error);
+
+ e_async_closure_free (closure);
+
+ return success;
+}
+
+/* Helper for e_book_backend_get_contact_list() */
+static void
+book_backend_get_contact_list_thread (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->get_contact_list_sync != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+
+ } else {
+ GError *error = NULL;
+
+ class->get_contact_list_sync (
+ backend,
+ async_context->query,
+ async_context->object_queue,
+ cancellable, &error);
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ /* XXX Once we get rid of the old-style API we can dispatch
+ * methods using g_simple_async_result_run_in_thread(),
+ * which completes the GSimpleAsyncResult for us. */
+ g_simple_async_result_complete_in_idle (simple);
+}
+
+/* Helper for e_book_backend_get_contact_list() */
+static void
+book_backend_get_contact_list_thread_old_style (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ EDataBook *data_book;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->get_contact_list != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+ g_return_if_fail (data_book != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+ g_simple_async_result_complete_in_idle (simple);
+
+ } else {
+ guint32 opid;
+
+ /* This is so the finish function knows which method
+ * was invoked and can gather results appropriately. */
+ async_context->old_style = TRUE;
+
+ opid = book_backend_stash_operation (backend, simple);
+
+ class->get_contact_list (
+ backend, data_book, opid, cancellable,
+ async_context->query);
+ }
+
+ g_object_unref (data_book);
+}
+
+/**
+ * e_book_backend_get_contact_list:
+ * @backend: an #EBookBackend
+ * @query: a search query in S-expression format
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: data to pass to the callback function
+ *
+ * Asynchronously obtains a set of #EContact instances which satisfy the
+ * criteria specified in @query.
+ *
+ * When the operation is finished, @callback will be called. You can then
+ * call e_book_backend_get_contact_list_finish() to get the result of the
+ * operation.
+ *
+ * Since: 3.10
+ **/
+void
+e_book_backend_get_contact_list (EBookBackend *backend,
+ const gchar *query,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ EBookBackendClass *class;
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (query != NULL);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+
+ async_context = g_slice_new0 (AsyncContext);
+ async_context->query = g_strdup (query);
+ async_context->object_queue = &async_context->result_queue;
+
+ simple = g_simple_async_result_new (
+ G_OBJECT (backend), callback, user_data,
+ e_book_backend_get_contact_list);
+
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ g_simple_async_result_set_op_res_gpointer (
+ simple, async_context, (GDestroyNotify) async_context_free);
+
+ if (class->get_contact_list_sync != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_get_contact_list_thread);
+ book_backend_dispatch_next_operation (backend);
+
+ } else if (class->get_contact_list != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_get_contact_list_thread_old_style);
+ book_backend_dispatch_next_operation (backend);
+
+ } else {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_SUPPORTED));
+ g_simple_async_result_complete_in_idle (simple);
+ }
+
+ g_object_unref (simple);
+}
+
+/**
+ * e_book_backend_get_contact_list_finish:
+ * @backend: an #EBookBackend
+ * @result: a #GAsyncResult
+ * @out_contacts: a #GQueue in which to deposit results
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finishes the operation started with e_book_backend_get_contact_list().
+ *
+ * The matching #EContact instances are deposited in @out_contacts. The
+ * returned #EContact instances are referenced for thread-safety and must
+ * be unreferenced with g_object_unref() when finished with them.
+ *
+ * If an error occurred, the function will set @error and return %FALSE.
+ * Note that an empty result set does not necessarily imply an error.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_get_contact_list_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GQueue *out_contacts,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_val_if_fail (
+ g_simple_async_result_is_valid (
+ result, G_OBJECT (backend),
+ e_book_backend_get_contact_list), FALSE);
+ g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+ simple = G_SIMPLE_ASYNC_RESULT (result);
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ book_backend_unblock_operations (backend, simple);
+
+ if (g_simple_async_result_propagate_error (simple, error))
+ return FALSE;
+
+ e_queue_transfer (async_context->object_queue, out_contacts);
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_get_contact_list_uids_sync:
+ * @backend: an #EBookBackend
+ * @query: a search query in S-expression format
+ * @out_uids: a #GQueue in which to deposit results
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Obtains a set of ID strings for contacts which satisfy the criteria
+ * specified in @query, and deposits them in @out_uids.
+ *
+ * The returned ID strings must be freed with g_free() with finished
+ * with them.
+ *
+ * If an error occurs, the function will set @error and return %FALSE.
+ * Note that an empty result set does not necessarily imply an error.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_get_contact_list_uids_sync (EBookBackend *backend,
+ const gchar *query,
+ GQueue *out_uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EAsyncClosure *closure;
+ GAsyncResult *result;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+ g_return_val_if_fail (query != NULL, FALSE);
+ g_return_val_if_fail (out_uids != NULL, FALSE);
+
+ closure = e_async_closure_new ();
+
+ e_book_backend_get_contact_list_uids (
+ backend, query, cancellable,
+ e_async_closure_callback, closure);
+
+ result = e_async_closure_wait (closure);
+
+ success = e_book_backend_get_contact_list_uids_finish (
+ backend, result, out_uids, error);
+
+ e_async_closure_free (closure);
+
+ return success;
+}
+
+/* Helper for e_book_backend_get_contact_list_uids() */
+static void
+book_backend_get_contact_list_uids_thread (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->get_contact_list_uids_sync != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+
+ } else {
+ GError *error = NULL;
+
+ class->get_contact_list_uids_sync (
+ backend,
+ async_context->query,
+ async_context->string_queue,
+ cancellable, &error);
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ /* XXX Once we get rid of the old-style API we can dispatch
+ * methods using g_simple_async_result_run_in_thread(),
+ * which completes the GSimpleAsyncResult for us. */
+ g_simple_async_result_complete_in_idle (simple);
+}
+
+/* Helper for e_book_backend_get_contact_list_uids() */
+static void
+book_backend_get_contact_list_uids_thread_old_style (GSimpleAsyncResult *simple,
+ GObject *source_object,
+ GCancellable *cancellable)
+{
+ EBookBackend *backend;
+ EBookBackendClass *class;
+ EDataBook *data_book;
+ AsyncContext *async_context;
+
+ backend = E_BOOK_BACKEND (source_object);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->get_contact_list_uids != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+ g_return_if_fail (data_book != NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (!e_book_backend_is_opened (backend)) {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_OPENED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_OPENED));
+ g_simple_async_result_complete_in_idle (simple);
+
+ } else {
+ guint32 opid;
+
+ /* This is so the finish function knows which method
+ * was invoked and can gather results appropriately. */
+ async_context->old_style = TRUE;
+
+ opid = book_backend_stash_operation (backend, simple);
+
+ class->get_contact_list_uids (
+ backend, data_book, opid, cancellable,
+ async_context->query);
+ }
+
+ g_object_unref (data_book);
+}
+
+/**
+ * e_book_backend_get_contact_list_uids:
+ * @backend: an #EBookBackend
+ * @query: a search query in S-expression format
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: data to pass to the callback function
+ *
+ * Asynchronously obtains a set of ID strings for contacts which satisfy
+ * the criteria specified in @query.
+ *
+ * When the operation is finished, @callback will be called. You can then
+ * call e_book_backend_get_contact_list_uids_finish() to get the result of
+ * the operation.
+ *
+ * Since: 3.10
+ **/
+void
+e_book_backend_get_contact_list_uids (EBookBackend *backend,
+ const gchar *query,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ EBookBackendClass *class;
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (query != NULL);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+
+ async_context = g_slice_new0 (AsyncContext);
+ async_context->query = g_strdup (query);
+ async_context->string_queue = &async_context->result_queue;
+
+ simple = g_simple_async_result_new (
+ G_OBJECT (backend), callback, user_data,
+ e_book_backend_get_contact_list_uids);
+
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ g_simple_async_result_set_op_res_gpointer (
+ simple, async_context, (GDestroyNotify) async_context_free);
+
+ if (class->get_contact_list_uids_sync != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_get_contact_list_uids_thread);
+ book_backend_dispatch_next_operation (backend);
+
+ } else if (class->get_contact_list_uids != NULL) {
+ book_backend_push_operation (
+ backend, simple, cancellable, FALSE,
+ book_backend_get_contact_list_uids_thread_old_style);
+ book_backend_dispatch_next_operation (backend);
+
+ } else {
+ g_simple_async_result_set_error (
+ simple, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ "%s", e_client_error_to_string (
+ E_CLIENT_ERROR_NOT_SUPPORTED));
+ g_simple_async_result_complete_in_idle (simple);
+ }
+
+ g_object_unref (simple);
+}
+
+/**
+ * e_book_backend_get_contact_list_uids_finish:
+ * @backend: an #EBookBackend
+ * @result: a #GAsyncResult
+ * @out_uids: a #GQueue in which to deposit results
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finishes the operation started with
+ * e_book_backend_get_contact_list_uids_finish().
+ *
+ * ID strings for the matching contacts are deposited in @out_uids, and
+ * must be freed with g_free() when finished with them.
+ *
+ * If an error occurs, the function will set @error and return %FALSE.
+ * Note that an empty result set does not necessarily imply an error.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.10
+ **/
+gboolean
+e_book_backend_get_contact_list_uids_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GQueue *out_uids,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_val_if_fail (
+ g_simple_async_result_is_valid (
+ result, G_OBJECT (backend),
+ e_book_backend_get_contact_list_uids), FALSE);
+ g_return_val_if_fail (out_uids != NULL, FALSE);
+
+ simple = G_SIMPLE_ASYNC_RESULT (result);
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ book_backend_unblock_operations (backend, simple);
+
+ if (g_simple_async_result_propagate_error (simple, error))
+ return FALSE;
+
+ e_queue_transfer (async_context->string_queue, out_uids);
+
+ return TRUE;
+}
+
+/**
+ * e_book_backend_start_view:
+ * @backend: an #EBookBackend
+ * @view: the #EDataBookView to start
+ *
+ * Starts running the query specified by @view, emitting signals for
+ * matching contacts.
+ **/
+void
+e_book_backend_start_view (EBookBackend *backend,
+ EDataBookView *view)
+{
+ EBookBackendClass *class;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->start_view);
+
+ class->start_view (backend, view);
+}
+
+/**
+ * e_book_backend_stop_view:
+ * @backend: an #EBookBackend
+ * @view: the #EDataBookView to stop
+ *
+ * Stops running the query specified by @view, emitting no more signals.
+ **/
+void
+e_book_backend_stop_view (EBookBackend *backend,
+ EDataBookView *view)
+{
+ EBookBackendClass *class;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->stop_view != NULL);
+
+ class->stop_view (backend, view);
+}
+
+/**
+ * e_book_backend_add_view:
+ * @backend: an #EBookBackend
+ * @view: an #EDataBookView
+ *
+ * Adds @view to @backend for querying.
+ **/
+void
+e_book_backend_add_view (EBookBackend *backend,
+ EDataBookView *view)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+
+ g_mutex_lock (&backend->priv->views_mutex);
+
+ g_object_ref (view);
+ backend->priv->views = g_list_append (backend->priv->views, view);
+
+ g_mutex_unlock (&backend->priv->views_mutex);
+}
+
+/**
+ * e_book_backend_remove_view:
+ * @backend: an #EBookBackend
+ * @view: an #EDataBookView
+ *
+ * Removes @view from @backend.
+ **/
+void
+e_book_backend_remove_view (EBookBackend *backend,
+ EDataBookView *view)
+{
+ GList *list, *link;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+
+ g_mutex_lock (&backend->priv->views_mutex);
+
+ list = backend->priv->views;
+
+ link = g_list_find (list, view);
+ if (link != NULL) {
+ g_object_unref (view);
+ list = g_list_delete_link (list, link);
+ }
+
+ backend->priv->views = list;
+
+ g_mutex_unlock (&backend->priv->views_mutex);
+}
+
+/**
+ * e_book_backend_list_views:
+ * @backend: an #EBookBackend
+ *
+ * Returns a list of #EDataBookView instances added with
+ * e_book_backend_add_view().
+ *
+ * The views returned in the list are referenced for thread-safety.
+ * They must each be unreferenced with g_object_unref() when finished
+ * with them. Free the returned list itself with g_list_free().
+ *
+ * An easy way to free the list properly in one step is as follows:
+ *
+ * |[
+ * g_list_free_full (list, g_object_unref);
+ * ]|
+ *
+ * Returns: a list of book views
+ *
+ * Since: 3.8
+ **/
+GList *
+e_book_backend_list_views (EBookBackend *backend)
+{
+ GList *list;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+
+ g_mutex_lock (&backend->priv->views_mutex);
+
+ /* XXX Use g_list_copy_deep() once we require GLib >= 2.34. */
+ list = g_list_copy (backend->priv->views);
+ g_list_foreach (list, (GFunc) g_object_ref, NULL);
+
+ g_mutex_unlock (&backend->priv->views_mutex);
+
+ return list;
+}
+
+/**
+ * e_book_backend_get_backend_property:
+ * @backend: an #EBookBackend
+ * @prop_name: a backend property name
+ *
+ * Obtains the value of the backend property named @prop_name.
+ * Freed the returned string with g_free() when finished with it.
+ *
+ * Returns: the value for @prop_name
+ *
+ * Since: 3.10
+ **/
+gchar *
+e_book_backend_get_backend_property (EBookBackend *backend,
+ const gchar *prop_name)
+{
+ EBookBackendClass *class;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+ g_return_val_if_fail (prop_name != NULL, NULL);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_val_if_fail (class->get_backend_property != NULL, NULL);
+
+ return class->get_backend_property (backend, prop_name);
+}
+
+/**
+ * e_book_backend_is_opened:
+ * @backend: an #EBookBackend
+ *
+ * Checks if @backend's storage has been opened (and
+ * authenticated, if necessary) and the backend itself
+ * is ready for accessing. This property is changed automatically
+ * within call of e_book_backend_notify_opened().
+ *
+ * Returns: %TRUE if fully opened, %FALSE otherwise.
+ *
+ * Since: 3.2
+ **/
+gboolean
+e_book_backend_is_opened (EBookBackend *backend)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+
+ return backend->priv->opened;
+}
+
+/**
+ * e_book_backend_is_readonly:
+ * @backend: an #EBookBackend
+ *
+ * Checks if we can write to @backend.
+ *
+ * Returns: %TRUE if read-only, %FALSE if not.
+ *
+ * Since: 3.2
+ **/
+gboolean
+e_book_backend_is_readonly (EBookBackend *backend)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+
+ return !e_book_backend_get_writable (backend);
+}
+
+/**
+ * e_book_backend_get_direct_book:
+ * @backend: an #EBookBackend
+ *
+ * Tries to create an #EDataBookDirect for @backend if
+ * backend supports direct read access.
+ *
+ * Returns: (transfer full): A new #EDataBookDirect object, or %NULL if
+ * @backend does not support direct access
+ *
+ * Since: 3.8
+ */
+EDataBookDirect *
+e_book_backend_get_direct_book (EBookBackend *backend)
+{
+ EBookBackendClass *class;
+ EDataBookDirect *direct_book = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+
+ if (class->get_direct_book != NULL)
+ direct_book = class->get_direct_book (backend);
+
+ return direct_book;
+}
+
+/**
+ * e_book_backend_configure_direct:
+ * @backend: an #EBookBackend
+ * @config: The configuration string for the given backend
+ *
+ * This method is called on @backend in direct read access mode.
+ * The @config argument is the same configuration string which
+ * the same backend reported in the #EDataBookDirect returned
+ * by e_book_backend_get_direct_book().
+ *
+ * The configuration string is optional and is used to ensure
+ * that direct access backends are properly configured to
+ * interface with the same data as the running server side backend.
+ *
+ * Since: 3.8
+ */
+void
+e_book_backend_configure_direct (EBookBackend *backend,
+ const gchar *config)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+
+ if (E_BOOK_BACKEND_GET_CLASS (backend)->configure_direct)
+ E_BOOK_BACKEND_GET_CLASS (backend)->configure_direct (backend, config);
+}
+
+/**
+ * e_book_backend_sync:
+ * @backend: an #EBookbackend
+ *
+ * Write all pending data to disk. This is only required under special
+ * circumstances (for example before a live backup) and should not be used in
+ * normal use.
+ *
+ * Since: 1.12
+ */
+void
+e_book_backend_sync (EBookBackend *backend)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+
+ g_object_ref (backend);
+
+ if (E_BOOK_BACKEND_GET_CLASS (backend)->sync)
+ (* E_BOOK_BACKEND_GET_CLASS (backend)->sync) (backend);
+
+ g_object_unref (backend);
+}
+
+/**
+ * e_book_backend_set_locale:
+ * @backend: an #EBookbackend
+ * @locale: the new locale for the addressbook
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Notify the addressbook backend that the current locale has
+ * changed, this is important for backends which support
+ * ordered result lists which are locale sensitive.
+ *
+ * Returns: %TRUE on success, %FALSE on failure
+ *
+ * Since: 3.12
+ */
+gboolean
+e_book_backend_set_locale (EBookBackend *backend,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* If the backend does not support locales, just happily return */
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+
+ g_object_ref (backend);
+
+ if (E_BOOK_BACKEND_GET_CLASS (backend)->set_locale) {
+ success = (* E_BOOK_BACKEND_GET_CLASS (backend)->set_locale) (backend, locale,
+ cancellable, error);
+ if (success)
+ e_book_backend_notify_complete (backend);
+
+ }
+ g_object_unref (backend);
+
+ return success;
+}
+
+/**
+ * e_book_backend_dup_locale:
+ * @backend: an #EBookbackend
+ *
+ * Fetches a copy of the currently configured locale for the addressbook
+ *
+ * Returns: A copy of the currently configured locale for the addressbook.
+ * Free with g_free() when done with it.
+ *
+ * Since: 3.12
+ */
+gchar *
+e_book_backend_dup_locale (EBookBackend *backend)
+{
+ gchar *locale = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+
+ g_object_ref (backend);
+
+ if (E_BOOK_BACKEND_GET_CLASS (backend)->dup_locale)
+ locale = (* E_BOOK_BACKEND_GET_CLASS (backend)->dup_locale) (backend);
+
+ g_object_unref (backend);
+
+ return locale;
+}
+
+/**
+ * e_book_backend_notify_update:
+ * @backend: an #EBookBackend
+ * @contact: a new or modified contact
+ *
+ * Notifies all of @backend's book views about the new or modified
+ * contacts @contact.
+ *
+ * e_data_book_respond_create_contacts() and e_data_book_respond_modify_contacts() call this
+ * function for you. You only need to call this from your backend if
+ * contacts are created or modified by another (non-PAS-using) client.
+ **/
+void
+e_book_backend_notify_update (EBookBackend *backend,
+ const EContact *contact)
+{
+ EBookBackendClass *class;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (E_IS_CONTACT (contact));
+
+ class = E_BOOK_BACKEND_GET_CLASS (backend);
+ g_return_if_fail (class->notify_update != NULL);
+
+ class->notify_update (backend, contact);
+}
+
+/**
+ * e_book_backend_notify_remove:
+ * @backend: an #EBookBackend
+ * @id: a contact id
+ *
+ * Notifies all of @backend's book views that the contact with UID
+ * @id has been removed.
+ *
+ * e_data_book_respond_remove_contacts() calls this function for you. You
+ * only need to call this from your backend if contacts are removed by
+ * another (non-PAS-using) client.
+ **/
+void
+e_book_backend_notify_remove (EBookBackend *backend,
+ const gchar *id)
+{
+ GList *list, *link;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (id != NULL);
+
+ list = e_book_backend_list_views (backend);
+
+ for (link = list; link != NULL; link = g_list_next (link)) {
+ EDataBookView *view = E_DATA_BOOK_VIEW (link->data);
+ e_data_book_view_notify_remove (view, id);
+ }
+
+ g_list_free_full (list, (GDestroyNotify) g_object_unref);
+}
+
+/**
+ * e_book_backend_notify_complete:
+ * @backend: an #EBookbackend
+ *
+ * Notifies all of @backend's book views that the current set of
+ * notifications is complete; use this after a series of
+ * e_book_backend_notify_update() and e_book_backend_notify_remove() calls.
+ **/
+void
+e_book_backend_notify_complete (EBookBackend *backend)
+{
+ GList *list, *link;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+
+ list = e_book_backend_list_views (backend);
+
+ for (link = list; link != NULL; link = g_list_next (link)) {
+ EDataBookView *view = E_DATA_BOOK_VIEW (link->data);
+ e_data_book_view_notify_complete (view, NULL /* SUCCESS */);
+ }
+
+ g_list_free_full (list, (GDestroyNotify) g_object_unref);
+}
+
+/**
+ * e_book_backend_notify_error:
+ * @backend: an #EBookBackend
+ * @message: an error message
+ *
+ * Notifies each backend listener about an error. This is meant to be used
+ * for cases where is no GError return possibility, to notify user about
+ * an issue.
+ *
+ * Since: 3.2
+ **/
+void
+e_book_backend_notify_error (EBookBackend *backend,
+ const gchar *message)
+{
+ EDataBook *data_book;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (message != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+
+ if (data_book != NULL) {
+ e_data_book_report_error (data_book, message);
+ g_object_unref (data_book);
+ }
+}
+
+/**
+ * e_book_backend_notify_property_changed:
+ * @backend: an #EBookBackend
+ * @prop_name: property name, which changed
+ * @prop_value: new property value
+ *
+ * Notifies clients about property value change.
+ *
+ * Since: 3.2
+ **/
+void
+e_book_backend_notify_property_changed (EBookBackend *backend,
+ const gchar *prop_name,
+ const gchar *prop_value)
+{
+ EDataBook *data_book;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (prop_name != NULL);
+ g_return_if_fail (prop_value != NULL);
+
+ data_book = e_book_backend_ref_data_book (backend);
+
+ if (data_book != NULL) {
+ e_data_book_report_backend_property_changed (
+ data_book, prop_name, prop_value);
+ g_object_unref (data_book);
+ }
+}
+
+/**
+ * e_book_backend_prepare_for_completion:
+ * @backend: an #EBookBackend
+ * @opid: an operation ID given to #EDataBook
+ * @result_queue: return location for a #GQueue, or %NULL
+ *
+ * Obtains the #GSimpleAsyncResult for @opid and sets @result_queue as a
+ * place to deposit results prior to completing the #GSimpleAsyncResult.
+ *
+ * <note>
+ * <para>
+ * This is a temporary function to serve #EDataBook's "respond"
+ * functions until they can be removed. Nothing else should be
+ * calling this function.
+ * </para>
+ * </note>
+ *
+ * Returns: (transfer full): a #GSimpleAsyncResult
+ *
+ * Since: 3.10
+ **/
+GSimpleAsyncResult *
+e_book_backend_prepare_for_completion (EBookBackend *backend,
+ guint32 opid,
+ GQueue **result_queue)
+{
+ GSimpleAsyncResult *simple;
+ AsyncContext *async_context;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+ g_return_val_if_fail (opid > 0, NULL);
+
+ simple = book_backend_claim_operation (backend, opid);
+ g_return_val_if_fail (simple != NULL, NULL);
+
+ async_context = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (result_queue != NULL) {
+ if (async_context != NULL)
+ *result_queue = &async_context->result_queue;
+ else
+ *result_queue = NULL;
+ }
+
+ return simple;
+}
+
+/**
+ * e_book_backend_create_cursor:
+ * @backend: an #EBookBackend
+ * @sort_fields: the #EContactFields to sort by
+ * @sort_types: the #EBookCursorSortTypes for the sorted fields
+ * @n_fields: the number of fields in the @sort_fields and @sort_types
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EDataBookCursor for the given backend if the backend
+ * has cursor support. If the backend does not support cursors then
+ * an %E_CLIENT_ERROR_NOT_SUPPORTED error will be set in @error.
+ *
+ * Backends can also refuse to create cursors for some values of @sort_fields
+ * and report more specific errors.
+ *
+ * The returned cursor belongs to @backend and should be destroyed
+ * with e_book_backend_delete_cursor() when no longer needed.
+ *
+ * Returns: (transfer none): A newly created cursor, the cursor belongs
+ * to the backend and should not be unreffed, or %NULL
+ *
+ * Since: 3.12
+ */
+EDataBookCursor *
+e_book_backend_create_cursor (EBookBackend *backend,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error)
+{
+ EDataBookCursor *cursor = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+
+ g_object_ref (backend);
+
+ if (E_BOOK_BACKEND_GET_CLASS (backend)->create_cursor)
+ cursor = (* E_BOOK_BACKEND_GET_CLASS (backend)->create_cursor) (backend,
+ sort_fields,
+ sort_types,
+ n_fields,
+ error);
+ else
+ g_set_error (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ "Addressbook backend does not support cursors");
+
+ g_object_unref (backend);
+
+ return cursor;
+}
+
+/**
+ * e_book_backend_delete_cursor:
+ * @backend: an #EBookBackend
+ * @cursor: the #EDataBookCursor to destroy
+ * @error: return location for a #GError, or %NULL
+ *
+ * Requests @backend to release and destroy @cursor, this
+ * will trigger an %E_CLIENT_ERROR_INVALID_ARG error if @cursor
+ * is not owned by @backend.
+ *
+ * Returns: Whether @cursor was successfully deleted.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_book_backend_delete_cursor (EBookBackend *backend,
+ EDataBookCursor *cursor,
+ GError **error)
+{
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), FALSE);
+
+ g_object_ref (backend);
+
+ if (E_BOOK_BACKEND_GET_CLASS (backend)->delete_cursor)
+ success = (* E_BOOK_BACKEND_GET_CLASS (backend)->delete_cursor) (backend, cursor, error);
+ else
+ g_warning ("Backend asked to delete a cursor, but does not support cursors");
+
+ g_object_unref (backend);
+
+ return success;
+}
diff --git a/src/addressbook/libedata-book/e-book-backend.h b/src/addressbook/libedata-book/e-book-backend.h
new file mode 100644
index 000000000..a9b7bd5e3
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-backend.h
@@ -0,0 +1,481 @@
+/*
+ * e-book-backend.h
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2012 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Nat Friedman (nat@ximian.com)
+ * Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_BACKEND_H
+#define E_BOOK_BACKEND_H
+
+#include <libebook-contacts/libebook-contacts.h>
+#include <libebackend/libebackend.h>
+
+#include <libedata-book/e-data-book.h>
+#include <libedata-book/e-data-book-cursor.h>
+#include <libedata-book/e-data-book-direct.h>
+#include <libedata-book/e-data-book-view.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_BACKEND \
+ (e_book_backend_get_type ())
+#define E_BOOK_BACKEND(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_BACKEND, EBookBackend))
+#define E_BOOK_BACKEND_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_BACKEND, EBookBackendClass))
+#define E_IS_BOOK_BACKEND(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_BACKEND))
+#define E_IS_BOOK_BACKEND_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_BACKEND))
+#define E_BOOK_BACKEND_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_BACKEND, EBookBackendClass))
+
+/**
+ * CLIENT_BACKEND_PROPERTY_CAPABILITIES:
+ *
+ * FIXME: Document me.
+ *
+ * Since: 3.2
+ **/
+#define CLIENT_BACKEND_PROPERTY_CAPABILITIES "capabilities"
+
+/**
+ * BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS:
+ *
+ * FIXME: Document me.
+ *
+ * Since: 3.2
+ **/
+#define BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS "required-fields"
+
+/**
+ * BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS:
+ *
+ * FIXME: Document me.
+ *
+ * Since: 3.2
+ **/
+#define BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS "supported-fields"
+
+/**
+ * BOOK_BACKEND_PROPERTY_REVISION:
+ *
+ * The current overall revision string, this can be used as
+ * a quick check to see if data has changed at all since the
+ * last time the addressbook revision was observed.
+ *
+ * Since: 3.4
+ **/
+#define BOOK_BACKEND_PROPERTY_REVISION "revision"
+
+G_BEGIN_DECLS
+
+typedef struct _EBookBackend EBookBackend;
+typedef struct _EBookBackendClass EBookBackendClass;
+typedef struct _EBookBackendPrivate EBookBackendPrivate;
+
+/**
+ * EBookBackend:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ */
+struct _EBookBackend {
+ /*< private >*/
+ EBackend parent;
+ EBookBackendPrivate *priv;
+};
+
+/**
+ * EBookBackendClass:
+ * @use_serial_dispatch_queue: Whether a serial dispatch queue should
+ * be used for this backend or not.
+ * @get_backend_property: Fetch a property value by name from the backend
+ * @open_sync: Open the backend
+ * @refresh_sync: Refresh the backend
+ * @create_contacts_sync: Add and store the passed vcards
+ * @modify_contacts_sync: Modify the existing contacts using the passed vcards
+ * @remove_contacts_sync: Remove the contacts specified by the passed UIDs
+ * @get_contact_sync: Fetch a contact by UID
+ * @get_contact_list_sync: Fetch a list of contacts based on a search expression
+ * @get_contact_list_uids_sync: Fetch a list of contact UIDs based on a search expression (optional)
+ * @start_view: Start up the specified view
+ * @stop_view: Stop the specified view
+ * @notify_update: Notify changes which might have occured for a given contact
+ * @get_direct_book: For addressbook backends which support Direct Read Access,
+ * report some information on how to access the addressbook persistance directly
+ * @configure_direct: For addressbook backends which support Direct Read Access, configure a
+ * backend instantiated on the client side for Direct Read Access, using data
+ * reported from the server via the @get_direct_book method.
+ * @sync: Sync the backend's persistance
+ * @set_locale: Store & remember the passed locale setting
+ * @dup_locale: Return the currently set locale setting (must be a string duplicate, for thread safety).
+ * @create_cursor: Create an #EDataBookCursor
+ * @delete_cursor: Delete an #EDataBookCursor previously created by this backend
+ * @closed: A signal notifying that the backend was closed
+ * @shutdown: A signal notifying that the backend is being shut down
+ * @open: Deprecated method
+ * @refresh: Deprecated method
+ * @create_contacts: Deprecated method
+ * @remove_contacts: Deprecated method
+ * @modify_contacts: Deprecated method
+ * @get_contact: Deprecated method
+ * @get_contact_list: Deprecated method
+ * @get_contact_list_uids: Deprecated method
+ *
+ * Class structure for the #EBookBackend class.
+ *
+ * These virtual methods must be implemented when writing
+ * an addressbook backend.
+ */
+struct _EBookBackendClass {
+ /*< private >*/
+ EBackendClass parent_class;
+
+ /*< public >*/
+
+ /* Set this to TRUE to use a serial dispatch queue, instead
+ * of a concurrent dispatch queue. A serial dispatch queue
+ * executes one method at a time in the order in which they
+ * were called. This is generally slower than a concurrent
+ * dispatch queue, but helps avoid thread-safety issues. */
+ gboolean use_serial_dispatch_queue;
+
+ gchar * (*get_backend_property) (EBookBackend *backend,
+ const gchar *prop_name);
+
+ gboolean (*open_sync) (EBookBackend *backend,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (*refresh_sync) (EBookBackend *backend,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (*create_contacts_sync) (EBookBackend *backend,
+ const gchar * const *vcards,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (*modify_contacts_sync) (EBookBackend *backend,
+ const gchar * const *vcards,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (*remove_contacts_sync) (EBookBackend *backend,
+ const gchar * const *uids,
+ GCancellable *cancellable,
+ GError **error);
+ EContact * (*get_contact_sync) (EBookBackend *backend,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (*get_contact_list_sync)
+ (EBookBackend *backend,
+ const gchar *query,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error);
+
+ /* This method is optional. By default, it simply calls
+ * get_contact_list_sync() and extracts UID strings from
+ * the matched EContacts. Backends may override this if
+ * they can implement it more efficiently. */
+ gboolean (*get_contact_list_uids_sync)
+ (EBookBackend *backend,
+ const gchar *query,
+ GQueue *out_uids,
+ GCancellable *cancellable,
+ GError **error);
+
+ /* These methods are deprecated and will be removed once all
+ * known subclasses are converted to the new methods above. */
+ void (*open) (EBookBackend *backend,
+ EDataBook *book,
+ guint32 opid,
+ GCancellable *cancellable,
+ gboolean only_if_exists);
+ void (*refresh) (EBookBackend *backend,
+ EDataBook *book,
+ guint32 opid,
+ GCancellable *cancellable);
+ void (*create_contacts) (EBookBackend *backend,
+ EDataBook *book,
+ guint32 opid,
+ GCancellable *cancellable,
+ const GSList *vcards);
+ void (*remove_contacts) (EBookBackend *backend,
+ EDataBook *book,
+ guint32 opid,
+ GCancellable *cancellable,
+ const GSList *id_list);
+ void (*modify_contacts) (EBookBackend *backend,
+ EDataBook *book,
+ guint32 opid,
+ GCancellable *cancellable,
+ const GSList *vcards);
+ void (*get_contact) (EBookBackend *backend,
+ EDataBook *book,
+ guint32 opid,
+ GCancellable *cancellable,
+ const gchar *id);
+ void (*get_contact_list) (EBookBackend *backend,
+ EDataBook *book,
+ guint32 opid,
+ GCancellable *cancellable,
+ const gchar *query);
+ void (*get_contact_list_uids)
+ (EBookBackend *backend,
+ EDataBook *book,
+ guint32 opid,
+ GCancellable *cancellable,
+ const gchar *query);
+
+ void (*start_view) (EBookBackend *backend,
+ EDataBookView *book_view);
+ void (*stop_view) (EBookBackend *backend,
+ EDataBookView *book_view);
+
+ void (*notify_update) (EBookBackend *backend,
+ const EContact *contact);
+
+ EDataBookDirect *
+ (*get_direct_book) (EBookBackend *backend);
+ void (*configure_direct) (EBookBackend *backend,
+ const gchar *config);
+
+ void (*sync) (EBookBackend *backend);
+
+ gboolean (*set_locale) (EBookBackend *backend,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error);
+ gchar * (*dup_locale) (EBookBackend *backend);
+ EDataBookCursor *
+ (*create_cursor) (EBookBackend *backend,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error);
+ gboolean (*delete_cursor) (EBookBackend *backend,
+ EDataBookCursor *cursor,
+ GError **error);
+
+ /* Signals */
+ void (*closed) (EBookBackend *backend,
+ const gchar *sender);
+ void (*shutdown) (EBookBackend *backend);
+};
+
+GType e_book_backend_get_type (void) G_GNUC_CONST;
+
+const gchar * e_book_backend_get_cache_dir (EBookBackend *backend);
+gchar * e_book_backend_dup_cache_dir (EBookBackend *backend);
+void e_book_backend_set_cache_dir (EBookBackend *backend,
+ const gchar *cache_dir);
+EDataBook * e_book_backend_ref_data_book (EBookBackend *backend);
+void e_book_backend_set_data_book (EBookBackend *backend,
+ EDataBook *data_book);
+GProxyResolver *
+ e_book_backend_ref_proxy_resolver
+ (EBookBackend *backend);
+ESourceRegistry *
+ e_book_backend_get_registry (EBookBackend *backend);
+gboolean e_book_backend_get_writable (EBookBackend *backend);
+void e_book_backend_set_writable (EBookBackend *backend,
+ gboolean writable);
+
+gboolean e_book_backend_is_opened (EBookBackend *backend);
+gboolean e_book_backend_is_readonly (EBookBackend *backend);
+
+gchar * e_book_backend_get_backend_property
+ (EBookBackend *backend,
+ const gchar *prop_name);
+gboolean e_book_backend_open_sync (EBookBackend *backend,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_backend_open (EBookBackend *backend,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean e_book_backend_open_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error);
+gboolean e_book_backend_refresh_sync (EBookBackend *backend,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_backend_refresh (EBookBackend *backend,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean e_book_backend_refresh_finish (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error);
+gboolean e_book_backend_create_contacts_sync
+ (EBookBackend *backend,
+ const gchar * const *vcards,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_backend_create_contacts (EBookBackend *backend,
+ const gchar * const *vcards,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean e_book_backend_create_contacts_finish
+ (EBookBackend *backend,
+ GAsyncResult *result,
+ GQueue *out_contacts,
+ GError **error);
+gboolean e_book_backend_modify_contacts_sync
+ (EBookBackend *backend,
+ const gchar * const *vcards,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_backend_modify_contacts (EBookBackend *backend,
+ const gchar * const *vcards,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean e_book_backend_modify_contacts_finish
+ (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error);
+gboolean e_book_backend_remove_contacts_sync
+ (EBookBackend *backend,
+ const gchar * const *uids,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_backend_remove_contacts (EBookBackend *backend,
+ const gchar * const *uids,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean e_book_backend_remove_contacts_finish
+ (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error);
+EContact * e_book_backend_get_contact_sync (EBookBackend *backend,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_backend_get_contact (EBookBackend *backend,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+EContact * e_book_backend_get_contact_finish
+ (EBookBackend *backend,
+ GAsyncResult *result,
+ GError **error);
+gboolean e_book_backend_get_contact_list_sync
+ (EBookBackend *backend,
+ const gchar *query,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_backend_get_contact_list (EBookBackend *backend,
+ const gchar *query,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean e_book_backend_get_contact_list_finish
+ (EBookBackend *backend,
+ GAsyncResult *result,
+ GQueue *out_contacts,
+ GError **error);
+gboolean e_book_backend_get_contact_list_uids_sync
+ (EBookBackend *backend,
+ const gchar *query,
+ GQueue *out_uids,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_backend_get_contact_list_uids
+ (EBookBackend *backend,
+ const gchar *query,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean e_book_backend_get_contact_list_uids_finish
+ (EBookBackend *backend,
+ GAsyncResult *result,
+ GQueue *out_uids,
+ GError **error);
+
+void e_book_backend_start_view (EBookBackend *backend,
+ EDataBookView *view);
+void e_book_backend_stop_view (EBookBackend *backend,
+ EDataBookView *view);
+void e_book_backend_add_view (EBookBackend *backend,
+ EDataBookView *view);
+void e_book_backend_remove_view (EBookBackend *backend,
+ EDataBookView *view);
+GList * e_book_backend_list_views (EBookBackend *backend);
+
+void e_book_backend_notify_update (EBookBackend *backend,
+ const EContact *contact);
+void e_book_backend_notify_remove (EBookBackend *backend,
+ const gchar *id);
+void e_book_backend_notify_complete (EBookBackend *backend);
+
+void e_book_backend_notify_error (EBookBackend *backend,
+ const gchar *message);
+void e_book_backend_notify_property_changed
+ (EBookBackend *backend,
+ const gchar *prop_name,
+ const gchar *prop_value);
+
+EDataBookDirect *
+ e_book_backend_get_direct_book (EBookBackend *backend);
+void e_book_backend_configure_direct (EBookBackend *backend,
+ const gchar *config);
+
+void e_book_backend_sync (EBookBackend *backend);
+
+gboolean e_book_backend_set_locale (EBookBackend *backend,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error);
+gchar * e_book_backend_dup_locale (EBookBackend *backend);
+
+EDataBookCursor *
+ e_book_backend_create_cursor (EBookBackend *backend,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error);
+gboolean e_book_backend_delete_cursor (EBookBackend *backend,
+ EDataBookCursor *cursor,
+ GError **error);
+
+GSimpleAsyncResult *
+ e_book_backend_prepare_for_completion
+ (EBookBackend *backend,
+ guint32 opid,
+ GQueue **result_queue);
+
+G_END_DECLS
+
+#endif /* E_BOOK_BACKEND_H */
diff --git a/src/addressbook/libedata-book/e-book-sqlite.c b/src/addressbook/libedata-book/e-book-sqlite.c
new file mode 100644
index 000000000..2eeded250
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-sqlite.c
@@ -0,0 +1,8543 @@
+/*-*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* e-book-sqlite.c
+ *
+ * Copyright (C) 2013 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+/**
+ * SECTION: e-book-sqlite
+ * @include: libedata-book/libedata-book.h
+ * @short_description: An SQLite storage facility for addressbooks
+ *
+ * The #EBookSqlite is an API for storing and looking up #EContacts
+ * in an SQLite database. It also supports a lean index mode via
+ * the #EbSqlVCardCallback, if you are in a situation where it is
+ * not convenient to store the vCards directly in the SQLite. It is
+ * however recommended to avoid storing contacts in separate storage
+ * if at all possible, as this will decrease performance of searches
+ * an also contribute to flash wear.
+ *
+ * The API is thread safe, with special considerations to be made
+ * around e_book_sqlite_lock() and e_book_sqlite_unlock() for
+ * the sake of isolating transactions across threads.
+ *
+ * Any operations which can take a lot of time to complete (depending
+ * on the size of your addressbook) can be cancelled using a #GCancellable.
+ *
+ * Depending on your summary configuration, your mileage will vary. Refer
+ * to the #ESourceBackendSummarySetup for configuring your addressbook
+ * for the type of usage you mean to make of it.
+ */
+
+#include "e-book-sqlite.h"
+
+#include <locale.h>
+#include <string.h>
+#include <errno.h>
+
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+
+#include <sqlite3.h>
+
+/* For e_sqlite3_vfs_init() */
+#include <libebackend/libebackend.h>
+
+#include "e-book-backend-sexp.h"
+
+#define E_BOOK_SQLITE_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_BOOK_SQLITE, EBookSqlitePrivate))
+
+/******************************************************
+ * Debugging Macros *
+ ******************************************************
+ * Run EDS with EBSQL_DEBUG=statements:explain to print
+ * all statements and explain query plans.
+ *
+ * Use any of the values below to select which debug
+ * to enable.
+ */
+#define EBSQL_ENV_DEBUG "EBSQL_DEBUG"
+
+typedef enum {
+ EBSQL_DEBUG_STATEMENTS = 1 << 0, /* Output all executed statements */
+ EBSQL_DEBUG_EXPLAIN = 1 << 1, /* Output SQLite's query plan for SELECT statements */
+ EBSQL_DEBUG_LOCKS = 1 << 2, /* Print which function locks and unlocks the mutex */
+ EBSQL_DEBUG_ERRORS = 1 << 3, /* Print all errors which are set */
+ EBSQL_DEBUG_SCHEMA = 1 << 4, /* Debugging the schema building / upgrading */
+ EBSQL_DEBUG_INSERT = 1 << 5, /* Debugging contact insertions */
+ EBSQL_DEBUG_FETCH_VCARD = 1 << 6, /* Print invocations of the EbSqlVCardCallback fallback */
+ EBSQL_DEBUG_CURSOR = 1 << 7, /* Print information about EbSqlCursor operations */
+ EBSQL_DEBUG_CONVERT_E164 = 1 << 8, /* Print information e164 phone number conversions in vcards */
+ EBSQL_DEBUG_REF_COUNTS = 1 << 9, /* Print about shared EBookSqlite instances, print when finalized */
+ EBSQL_DEBUG_CANCEL = 1 << 10, /* Print information about GCancellable cancellations */
+ EBSQL_DEBUG_PREFLIGHT = 1 << 11, /* Print information about query preflighting */
+ EBSQL_DEBUG_TIMING = 1 << 12, /* Print information about timing */
+} EbSqlDebugFlag;
+
+static const GDebugKey ebsql_debug_keys[] = {
+ { "statements", EBSQL_DEBUG_STATEMENTS },
+ { "explain", EBSQL_DEBUG_EXPLAIN },
+ { "locks", EBSQL_DEBUG_LOCKS },
+ { "errors", EBSQL_DEBUG_ERRORS },
+ { "schema", EBSQL_DEBUG_SCHEMA },
+ { "insert", EBSQL_DEBUG_INSERT },
+ { "fetch-vcard", EBSQL_DEBUG_FETCH_VCARD },
+ { "cursor", EBSQL_DEBUG_CURSOR },
+ { "e164", EBSQL_DEBUG_CONVERT_E164 },
+ { "ref-counts", EBSQL_DEBUG_REF_COUNTS },
+ { "cancel", EBSQL_DEBUG_CANCEL },
+ { "preflight", EBSQL_DEBUG_PREFLIGHT },
+ { "timing", EBSQL_DEBUG_TIMING },
+};
+
+static EbSqlDebugFlag ebsql_debug_flags = 0;
+
+static void
+ebsql_init_debug (void)
+{
+ static gboolean initialized = FALSE;
+
+ if (G_UNLIKELY (!initialized)) {
+ const gchar *env_string;
+
+ env_string = g_getenv (EBSQL_ENV_DEBUG);
+
+ if (env_string != NULL)
+ ebsql_debug_flags =
+ g_parse_debug_string (
+ env_string,
+ ebsql_debug_keys,
+ G_N_ELEMENTS (ebsql_debug_keys));
+ }
+}
+
+static const gchar *
+ebsql_error_str (EBookSqliteError code)
+{
+ switch (code) {
+ case E_BOOK_SQLITE_ERROR_ENGINE:
+ return "engine";
+ case E_BOOK_SQLITE_ERROR_CONSTRAINT:
+ return "constraint";
+ case E_BOOK_SQLITE_ERROR_CONTACT_NOT_FOUND:
+ return "contact not found";
+ case E_BOOK_SQLITE_ERROR_INVALID_QUERY:
+ return "invalid query";
+ case E_BOOK_SQLITE_ERROR_UNSUPPORTED_QUERY:
+ return "unsupported query";
+ case E_BOOK_SQLITE_ERROR_UNSUPPORTED_FIELD:
+ return "unsupported field";
+ case E_BOOK_SQLITE_ERROR_END_OF_LIST:
+ return "end of list";
+ case E_BOOK_SQLITE_ERROR_LOAD:
+ return "load";
+ }
+
+ return "(unknown)";
+}
+
+static const gchar *
+ebsql_origin_str (EbSqlCursorOrigin origin)
+{
+ switch (origin) {
+ case EBSQL_CURSOR_ORIGIN_CURRENT:
+ return "current";
+ case EBSQL_CURSOR_ORIGIN_BEGIN:
+ return "begin";
+ case EBSQL_CURSOR_ORIGIN_END:
+ return "end";
+ }
+
+ return "(invalid)";
+}
+
+#define EBSQL_NOTE(type,action) \
+ G_STMT_START { \
+ if (ebsql_debug_flags & EBSQL_DEBUG_##type) \
+ { action; }; \
+ } G_STMT_END
+
+#define EBSQL_LOCK_MUTEX(mutex) \
+ G_STMT_START { \
+ if (ebsql_debug_flags & EBSQL_DEBUG_LOCKS) { \
+ g_printerr ("%s: Locking %s\n", G_STRFUNC, #mutex); \
+ g_mutex_lock (mutex); \
+ g_printerr ("%s: Locked %s\n", G_STRFUNC, #mutex); \
+ } else { \
+ g_mutex_lock (mutex); \
+ } \
+ } G_STMT_END
+
+#define EBSQL_UNLOCK_MUTEX(mutex) \
+ G_STMT_START { \
+ if (ebsql_debug_flags & EBSQL_DEBUG_LOCKS) { \
+ g_printerr ("%s: Unlocking %s\n", G_STRFUNC, #mutex); \
+ g_mutex_unlock (mutex); \
+ g_printerr ("%s: Unlocked %s\n", G_STRFUNC, #mutex); \
+ } else { \
+ g_mutex_unlock (mutex); \
+ } \
+ } G_STMT_END
+
+/* Format strings are passed through dgettext(), need to be reformatted */
+#define EBSQL_SET_ERROR(error, code, fmt, args...) \
+ G_STMT_START { \
+ if (ebsql_debug_flags & EBSQL_DEBUG_ERRORS) { \
+ gchar *format = g_strdup_printf ( \
+ "ERR [%%s]: Set error code '%%s': %s\n", fmt); \
+ g_printerr (format, G_STRFUNC, \
+ ebsql_error_str (code), ## args); \
+ g_free (format); \
+ } \
+ g_set_error (error, E_BOOK_SQLITE_ERROR, code, fmt, ## args); \
+ } G_STMT_END
+
+#define EBSQL_SET_ERROR_LITERAL(error, code, detail) \
+ G_STMT_START { \
+ if (ebsql_debug_flags & EBSQL_DEBUG_ERRORS) { \
+ g_printerr ("ERR [%s]: " \
+ "Set error code %s: %s\n", \
+ G_STRFUNC, \
+ ebsql_error_str (code), detail); \
+ } \
+ g_set_error_literal (error, E_BOOK_SQLITE_ERROR, code, detail); \
+ } G_STMT_END
+
+/* EBSQL_LOCK_OR_RETURN:
+ * @ebsql: The #EBookSqlite
+ * @cancellable: A #GCancellable passed into an API
+ * @val: Value to return if this check fails
+ *
+ * This will first lock the mutex and then check if
+ * the passed cancellable is valid or invalid, it can
+ * be invalid if it differs from a cancellable passed
+ * to a toplevel transaction via e_book_sqlite_lock().
+ *
+ * If the check fails, the lock is released and then
+ * @val is returned.
+ */
+#define EBSQL_LOCK_OR_RETURN(ebsql, cancellable, val) \
+ G_STMT_START { \
+ EBSQL_LOCK_MUTEX (&(ebsql)->priv->lock); \
+ if (cancellable != NULL && (ebsql)->priv->cancel && \
+ (ebsql)->priv->cancel != cancellable) { \
+ g_warning ("The GCancellable passed to `%s' " \
+ "is not the same as the cancel object " \
+ "passed to e_book_sqlite_lock()", \
+ G_STRFUNC); \
+ EBSQL_UNLOCK_MUTEX (&(ebsql)->priv->lock); \
+ return val; \
+ } \
+ } G_STMT_END
+
+/* Set an error code from an sqlite_exec() or sqlite_step() return value & error message */
+#define EBSQL_SET_ERROR_FROM_SQLITE(error, code, message) \
+ G_STMT_START { \
+ if (code == SQLITE_CONSTRAINT) { \
+ EBSQL_SET_ERROR_LITERAL (error, \
+ E_BOOK_SQLITE_ERROR_CONSTRAINT, \
+ errmsg); \
+ } else if (code == SQLITE_ABORT) { \
+ if (ebsql_debug_flags & EBSQL_DEBUG_ERRORS) { \
+ g_printerr ("ERR [%s]: Set cancelled error\n", \
+ G_STRFUNC); \
+ } \
+ g_set_error (error, \
+ G_IO_ERROR, \
+ G_IO_ERROR_CANCELLED, \
+ "Operation cancelled: %s", errmsg); \
+ } else { \
+ EBSQL_SET_ERROR (error, \
+ E_BOOK_SQLITE_ERROR_ENGINE, \
+ "SQLite error code `%d': %s", \
+ code, errmsg); \
+ } \
+ } G_STMT_END
+
+#define FOLDER_VERSION 11
+#define INSERT_MULTI_STMT_BYTES 128
+#define COLUMN_DEFINITION_BYTES 32
+#define GENERATED_QUERY_BYTES 1024
+
+#define DEFAULT_FOLDER_ID "folder_id"
+
+/* We use a 64 bitmask to track which auxiliary tables
+ * are needed to satisfy a query, it's doubtful that
+ * anyone will need an addressbook with 64 fields configured
+ * in the summary.
+ */
+#define EBSQL_MAX_SUMMARY_FIELDS 64
+
+/* The number of SQLite virtual machine instructions that are
+ * evaluated at a time, the user passed GCancellable is
+ * checked between each batch of evaluated instructions.
+ */
+#define EBSQL_CANCEL_BATCH_SIZE 200
+
+/* Number of contacts to relocalize at a time
+ * while relocalizing the whole database
+ */
+#define EBSQL_UPGRADE_BATCH_SIZE 20
+
+#define EBSQL_ESCAPE_SEQUENCE "ESCAPE '^'"
+
+/* Names for custom functions */
+#define EBSQL_FUNC_COMPARE_VCARD "compare_vcard"
+#define EBSQL_FUNC_FETCH_VCARD "fetch_vcard"
+#define EBSQL_FUNC_EQPHONE_EXACT "eqphone_exact"
+#define EBSQL_FUNC_EQPHONE_NATIONAL "eqphone_national"
+#define EBSQL_FUNC_EQPHONE_SHORT "eqphone_short"
+
+/* Fallback collations are generated as with a prefix and an EContactField name */
+#define EBSQL_COLLATE_PREFIX "ebsql_"
+
+/* A special vcard attribute that we use only for private vcards */
+#define EBSQL_VCARD_SORT_KEY "X-EVOLUTION-SORT-KEY"
+
+/* Suffixes for column names used to store specialized data */
+#define EBSQL_SUFFIX_REVERSE "reverse"
+#define EBSQL_SUFFIX_SORT_KEY "localized"
+#define EBSQL_SUFFIX_PHONE "phone"
+#define EBSQL_SUFFIX_COUNTRY "country"
+
+/* Track EBookIndexType's in a bit mask */
+#define INDEX_FLAG(type) (1 << E_BOOK_INDEX_##type)
+
+/* This macro is used to reffer to vcards in statements */
+#define EBSQL_VCARD_FRAGMENT(ebsql) \
+ ((ebsql)->priv->vcard_callback ? \
+ EBSQL_FUNC_FETCH_VCARD " (summary.uid, summary.bdata)" : \
+ "summary.vcard")
+
+/* Signatures for some of the SQLite callbacks which we pass around */
+typedef void (*EbSqlCustomFunc) (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv);
+typedef gint (*EbSqlRowFunc) (gpointer ref,
+ gint n_cols,
+ gchar **cols,
+ gchar **names);
+
+/* Some forward declarations */
+static gboolean ebsql_init_statements (EBookSqlite *ebsql,
+ GError **error);
+static gboolean ebsql_insert_contact (EBookSqlite *ebsql,
+ EbSqlChangeType change_type,
+ EContact *contact,
+ const gchar *original_vcard,
+ const gchar *extra,
+ gboolean replace,
+ GError **error);
+static gboolean ebsql_exec (EBookSqlite *ebsql,
+ const gchar *stmt,
+ EbSqlRowFunc callback,
+ gpointer data,
+ GCancellable *cancellable,
+ GError **error);
+
+typedef struct {
+ EContactField field_id; /* The EContact field */
+ GType type; /* The GType (only support string or gboolean) */
+ const gchar *dbname; /* The key for this field in the sqlite3 table */
+ gint index; /* Types of searches this field should support (see EBookIndexType) */
+ gchar *aux_table; /* Name of auxiliary table for this field, for multivalued fields only */
+ gchar *aux_table_symbolic; /* Symolic name of auxiliary table used in queries */
+} SummaryField;
+
+struct _EBookSqlitePrivate {
+
+ /* Parameters and settings */
+ gchar *path; /* Full file name of the file we're operating on (used for hash table entries) */
+ gchar *locale; /* The current locale */
+ gchar *region_code; /* Region code (for phone number parsing) */
+ gchar *folderid; /* The summary table name (configurable, for support of legacy
+ * databases created by EBookSqliteDB) */
+
+ EbSqlVCardCallback vcard_callback; /* User callback to fetch vcards instead of storing them */
+ EbSqlChangeCallback change_callback; /* User callback to catch change notifications */
+ gpointer user_data; /* Data & Destroy notifier for the above callbacks */
+ GDestroyNotify user_data_destroy;
+
+ /* Summary configuration */
+ SummaryField *summary_fields;
+ gint n_summary_fields;
+
+ GMutex lock; /* Main API lock */
+ GMutex updates_lock; /* Lock used for calls to e_book_sqlite_lock_updates () */
+ guint32 in_transaction; /* Nested transaction counter */
+ EbSqlLockType lock_type; /* The lock type acquired for the current transaction */
+ GCancellable *cancel; /* User passed GCancellable, we abort an operation if cancelled */
+
+ ECollator *collator; /* The ECollator to create sort keys for any sortable fields */
+
+ /* SQLite resources */
+ sqlite3 *db;
+ sqlite3_stmt *insert_stmt; /* Insert statement for main summary table */
+ sqlite3_stmt *replace_stmt; /* Replace statement for main summary table */
+ GHashTable *multi_deletes; /* Delete statement for each auxiliary table */
+ GHashTable *multi_inserts; /* Insert statement for each auxiliary table */
+
+ ESource *source;
+};
+
+enum {
+ BEFORE_INSERT_CONTACT,
+ BEFORE_REMOVE_CONTACT,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE_WITH_CODE (EBookSqlite, e_book_sqlite, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL))
+G_DEFINE_QUARK (e-book-backend-sqlite-error-quark,
+ e_book_sqlite_error)
+
+/* The ColumnInfo struct is used to constant data
+ * and dynamically allocated data, the 'type' and
+ * 'extra' members are however always constant.
+ */
+typedef struct {
+ gchar *name;
+ const gchar *type;
+ const gchar *extra;
+ gchar *index;
+} ColumnInfo;
+
+static ColumnInfo main_table_columns[] = {
+ { (gchar *) "folder_id", "TEXT", "PRIMARY KEY", NULL },
+ { (gchar *) "version", "INTEGER", NULL, NULL },
+ { (gchar *) "multivalues", "TEXT", NULL, NULL },
+ { (gchar *) "lc_collate", "TEXT", NULL, NULL },
+ { (gchar *) "countrycode", "VARCHAR(2)", NULL, NULL },
+};
+
+/* Default summary configuration */
+static EContactField default_summary_fields[] = {
+ E_CONTACT_UID,
+ E_CONTACT_REV,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_NICKNAME,
+ E_CONTACT_FULL_NAME,
+ E_CONTACT_GIVEN_NAME,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_EMAIL,
+ E_CONTACT_TEL,
+ E_CONTACT_IS_LIST,
+ E_CONTACT_LIST_SHOW_ADDRESSES,
+ E_CONTACT_WANTS_HTML,
+ E_CONTACT_X509_CERT,
+};
+
+/* Create indexes on full_name and email fields as autocompletion
+ * queries would mainly rely on this.
+ *
+ * Add sort keys for name fields as those are likely targets for
+ * cursor usage.
+ */
+static EContactField default_indexed_fields[] = {
+ E_CONTACT_FULL_NAME,
+ E_CONTACT_NICKNAME,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_GIVEN_NAME,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_EMAIL,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_GIVEN_NAME
+};
+
+static EBookIndexType default_index_types[] = {
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_SORT_KEY,
+ E_BOOK_INDEX_SORT_KEY,
+ E_BOOK_INDEX_SORT_KEY
+};
+
+/******************************************************
+ * Summary Fields *
+ ******************************************************/
+static ColumnInfo *
+column_info_new (SummaryField *field,
+ const gchar *folderid,
+ const gchar *column_suffix,
+ const gchar *column_type,
+ const gchar *column_extra,
+ const gchar *idx_prefix)
+{
+ ColumnInfo *info;
+
+ info = g_slice_new0 (ColumnInfo);
+ info->type = column_type;
+ info->extra = column_extra;
+
+ if (!info->type) {
+ if (field->type == G_TYPE_STRING)
+ info->type = "TEXT";
+ else if (field->type == G_TYPE_BOOLEAN || field->type == E_TYPE_CONTACT_CERT)
+ info->type = "INTEGER";
+ else if (field->type == E_TYPE_CONTACT_ATTR_LIST)
+ info->type = "TEXT";
+ else
+ g_warn_if_reached ();
+ }
+
+ if (field->type == E_TYPE_CONTACT_ATTR_LIST)
+ /* Attribute lists are on their own table */
+ info->name = g_strconcat (
+ "value",
+ column_suffix ? "_" : NULL,
+ column_suffix,
+ NULL);
+ else
+ /* Regular fields are named by their 'dbname' */
+ info->name = g_strconcat (
+ field->dbname,
+ column_suffix ? "_" : NULL,
+ column_suffix,
+ NULL);
+
+ if (idx_prefix)
+ info->index = g_strconcat (
+ idx_prefix,
+ "_", field->dbname,
+ "_", folderid,
+ NULL);
+
+ return info;
+}
+
+static void
+column_info_free (ColumnInfo *info)
+{
+ if (info) {
+ g_free (info->name);
+ g_free (info->index);
+ g_slice_free (ColumnInfo, info);
+ }
+}
+
+static gint
+summary_field_array_index (GArray *array,
+ EContactField field)
+{
+ gint i;
+
+ for (i = 0; i < array->len; i++) {
+ SummaryField *iter = &g_array_index (array, SummaryField, i);
+ if (field == iter->field_id)
+ return i;
+ }
+
+ return -1;
+}
+
+static SummaryField *
+summary_field_append (GArray *array,
+ const gchar *folderid,
+ EContactField field_id,
+ GError **error)
+{
+ const gchar *dbname = NULL;
+ GType type = G_TYPE_INVALID;
+ gint idx;
+ SummaryField new_field = { 0, };
+
+ if (field_id < 1 || field_id >= E_CONTACT_FIELD_LAST) {
+ EBSQL_SET_ERROR (
+ error, E_BOOK_SQLITE_ERROR_UNSUPPORTED_FIELD,
+ _("Unsupported contact field '%d' specified in summary"),
+ field_id);
+ return NULL;
+ }
+
+ /* Avoid including the same field twice in the summary */
+ idx = summary_field_array_index (array, field_id);
+ if (idx >= 0)
+ return &g_array_index (array, SummaryField, idx);
+
+ /* Resolve some exceptions, we store these
+ * specific contact fields with different names
+ * than those found in the EContactField table
+ */
+ switch (field_id) {
+ case E_CONTACT_UID:
+ dbname = "uid";
+ break;
+ case E_CONTACT_IS_LIST:
+ dbname = "is_list";
+ break;
+ default:
+ dbname = e_contact_field_name (field_id);
+ break;
+ }
+
+ type = e_contact_field_type (field_id);
+
+ if (type != G_TYPE_STRING &&
+ type != G_TYPE_BOOLEAN &&
+ type != E_TYPE_CONTACT_CERT &&
+ type != E_TYPE_CONTACT_ATTR_LIST) {
+ EBSQL_SET_ERROR (
+ error, E_BOOK_SQLITE_ERROR_UNSUPPORTED_FIELD,
+ _("Contact field '%s' of type '%s' specified in summary, "
+ "but only boolean, string and string list field types are supported"),
+ e_contact_pretty_name (field_id), g_type_name (type));
+ return NULL;
+ }
+
+ if (type == E_TYPE_CONTACT_ATTR_LIST) {
+ new_field.aux_table = g_strconcat (folderid, "_", dbname, "_list", NULL);
+ new_field.aux_table_symbolic = g_strconcat (dbname, "_list", NULL);
+ }
+
+ new_field.field_id = field_id;
+ new_field.dbname = dbname;
+ new_field.type = type;
+ new_field.index = 0;
+ g_array_append_val (array, new_field);
+
+ return &g_array_index (array, SummaryField, array->len - 1);
+}
+
+static gboolean
+summary_field_remove (GArray *array,
+ EContactField field)
+{
+ gint idx;
+
+ idx = summary_field_array_index (array, field);
+ if (idx < 0)
+ return FALSE;
+
+ g_array_remove_index_fast (array, idx);
+ return TRUE;
+}
+
+static void
+summary_fields_add_indexes (GArray *array,
+ EContactField *indexes,
+ EBookIndexType *index_types,
+ gint n_indexes)
+{
+ gint i, j;
+
+ for (i = 0; i < array->len; i++) {
+ SummaryField *sfield = &g_array_index (array, SummaryField, i);
+
+ for (j = 0; j < n_indexes; j++) {
+ if (sfield->field_id == indexes[j])
+ sfield->index |= (1 << index_types[j]);
+
+ }
+ }
+}
+
+static inline gint
+summary_field_get_index (EBookSqlite *ebsql,
+ EContactField field_id)
+{
+ gint i;
+
+ for (i = 0; i < ebsql->priv->n_summary_fields; i++) {
+ if (ebsql->priv->summary_fields[i].field_id == field_id)
+ return i;
+ }
+
+ return -1;
+}
+
+static inline SummaryField *
+summary_field_get (EBookSqlite *ebsql,
+ EContactField field_id)
+{
+ gint index;
+
+ index = summary_field_get_index (ebsql, field_id);
+ if (index >= 0)
+ return &(ebsql->priv->summary_fields[index]);
+
+ return NULL;
+}
+
+static GSList *
+summary_field_list_columns (SummaryField *field,
+ const gchar *folderid)
+{
+ GSList *columns = NULL;
+ ColumnInfo *info;
+
+ /* Doesn't hurt to verify a bit more here, this shouldn't happen though */
+ g_return_val_if_fail (
+ field->type == G_TYPE_STRING ||
+ field->type == G_TYPE_BOOLEAN ||
+ field->type == E_TYPE_CONTACT_CERT ||
+ field->type == E_TYPE_CONTACT_ATTR_LIST,
+ NULL);
+
+ /* Normal / default column */
+ info = column_info_new (
+ field, folderid, NULL, NULL,
+ (field->field_id == E_CONTACT_UID) ? "PRIMARY KEY" : NULL,
+ (field->index & INDEX_FLAG (PREFIX)) != 0 ? "INDEX" : NULL);
+ columns = g_slist_prepend (columns, info);
+
+ /* Localized column, for storing sort keys */
+ if (field->type == G_TYPE_STRING && (field->index & INDEX_FLAG (SORT_KEY))) {
+ info = column_info_new (field, folderid, EBSQL_SUFFIX_SORT_KEY, "TEXT", NULL, "SINDEX");
+ columns = g_slist_prepend (columns, info);
+ }
+
+ /* Suffix match column */
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ info = column_info_new (field, folderid, EBSQL_SUFFIX_REVERSE, "TEXT", NULL, "RINDEX");
+ columns = g_slist_prepend (columns, info);
+ }
+
+ /* Phone match columns */
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (PHONE)) != 0) {
+
+ /* One indexed column for storing the national number */
+ info = column_info_new (field, folderid, EBSQL_SUFFIX_PHONE, "TEXT", NULL, "PINDEX");
+ columns = g_slist_prepend (columns, info);
+
+ /* One integer column for storing the country code */
+ info = column_info_new (field, folderid, EBSQL_SUFFIX_COUNTRY, "INTEGER", "DEFAULT 0", NULL);
+ columns = g_slist_prepend (columns, info);
+ }
+
+ return g_slist_reverse (columns);
+}
+
+static void
+summary_fields_array_free (SummaryField *fields,
+ gint n_fields)
+{
+ gint i;
+
+ for (i = 0; i < n_fields; i++) {
+ g_free (fields[i].aux_table);
+ g_free (fields[i].aux_table_symbolic);
+ }
+
+ g_free (fields);
+}
+
+/******************************************************
+ * Sharing EBookSqlite instances *
+ ******************************************************/
+static GHashTable *db_connections = NULL;
+static GMutex dbcon_lock;
+
+static EBookSqlite *
+ebsql_ref_from_hash (const gchar *path)
+{
+ EBookSqlite *ebsql = NULL;
+
+ if (db_connections != NULL) {
+ ebsql = g_hash_table_lookup (db_connections, path);
+ }
+
+ if (ebsql) {
+ EBSQL_NOTE (REF_COUNTS, g_printerr ("EBookSqlite ref count increased from hash table reference\n"));
+ g_object_ref (ebsql);
+ }
+
+ return ebsql;
+}
+
+static void
+ebsql_register_to_hash (EBookSqlite *ebsql,
+ const gchar *path)
+{
+ if (db_connections == NULL)
+ db_connections = g_hash_table_new_full (
+ (GHashFunc) g_str_hash,
+ (GEqualFunc) g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) NULL);
+ g_hash_table_insert (db_connections, g_strdup (path), ebsql);
+}
+
+static void
+ebsql_unregister_from_hash (EBookSqlite *ebsql)
+{
+ EBookSqlitePrivate *priv = ebsql->priv;
+
+ EBSQL_LOCK_MUTEX (&dbcon_lock);
+ if (db_connections != NULL) {
+ if (priv->path != NULL) {
+ g_hash_table_remove (db_connections, priv->path);
+
+ if (g_hash_table_size (db_connections) == 0) {
+ g_hash_table_destroy (db_connections);
+ db_connections = NULL;
+ }
+
+ }
+ }
+ EBSQL_UNLOCK_MUTEX (&dbcon_lock);
+}
+
+/************************************************************
+ * SQLite helper functions *
+ ************************************************************/
+
+/* For EBSQL_DEBUG_EXPLAIN */
+static gint
+ebsql_debug_query_plan_cb (gpointer ref,
+ gint n_cols,
+ gchar **cols,
+ gchar **name)
+{
+ gint i;
+
+ for (i = 0; i < n_cols; i++) {
+ if (strcmp (name[i], "detail") == 0) {
+ g_printerr (" PLAN: %s\n", cols[i]);
+ break;
+ }
+ }
+
+ return 0;
+}
+
+/* Collect a GList of column names in the main summary table */
+static gint
+get_columns_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ GSList **columns = (GSList **) ref;
+ gint i;
+
+ for (i = 0; i < col; i++) {
+ if (strcmp (name[i], "name") == 0) {
+
+ /* Keep comparing for the legacy 'bdata' column */
+ if (strcmp (cols[i], "vcard") != 0 &&
+ strcmp (cols[i], "bdata") != 0) {
+ gchar *column = g_strdup (cols[i]);
+
+ *columns = g_slist_prepend (*columns, column);
+ }
+ break;
+ }
+ }
+ return 0;
+}
+
+/* Collect the first string result */
+static gint
+get_string_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ gchar **ret = ref;
+
+ *ret = g_strdup (cols [0]);
+
+ return 0;
+}
+
+/* Collect the first integer result */
+static gint
+get_int_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ gint *ret = ref;
+
+ *ret = cols [0] ? g_ascii_strtoll (cols[0], NULL, 10) : 0;
+
+ return 0;
+}
+
+/* Collect the result of a SELECT count(*) statement */
+static gint
+get_count_cb (gpointer ref,
+ gint n_cols,
+ gchar **cols,
+ gchar **name)
+{
+ gint64 count = 0;
+ gint *ret = ref;
+ gint i;
+
+ for (i = 0; i < n_cols; i++) {
+ if (name[i] && strncmp (name[i], "count", 5) == 0) {
+ count = g_ascii_strtoll (cols[i], NULL, 10);
+
+ break;
+ }
+ }
+
+ *ret = count;
+
+ return 0;
+}
+
+/* Report if there was at least one result */
+static gint
+get_exists_cb (gpointer ref,
+ gint col,
+ gchar **cols,
+ gchar **name)
+{
+ gboolean *exists = ref;
+
+ *exists = TRUE;
+
+ return 0;
+}
+
+static EbSqlSearchData *
+search_data_from_results (gint ncol,
+ gchar **cols,
+ gchar **names)
+{
+ EbSqlSearchData *data = g_slice_new0 (EbSqlSearchData);
+ gint i;
+ const gchar *name;
+
+ for (i = 0; i < ncol; i++) {
+
+ if (!names[i] || !cols[i])
+ continue;
+
+ name = names[i];
+ if (!strncmp (name, "summary.", 8))
+ name += 8;
+
+ /* These come through differently depending on the configuration,
+ * search within text is good enough
+ */
+ if (!g_ascii_strcasecmp (name, "uid")) {
+ data->uid = g_strdup (cols[i]);
+ } else if (!g_ascii_strcasecmp (name, "vcard") ||
+ !g_ascii_strncasecmp (name, "fetch_vcard", 11)) {
+ data->vcard = g_strdup (cols[i]);
+ } else if (!g_ascii_strcasecmp (name, "bdata")) {
+ data->extra = g_strdup (cols[i]);
+ }
+ }
+
+ return data;
+}
+
+static gint
+collect_full_results_cb (gpointer ref,
+ gint ncol,
+ gchar **cols,
+ gchar **names)
+{
+ EbSqlSearchData *data;
+ GSList **vcard_data = ref;
+
+ data = search_data_from_results (ncol, cols, names);
+
+ *vcard_data = g_slist_prepend (*vcard_data, data);
+
+ return 0;
+}
+
+static gint
+collect_uid_results_cb (gpointer ref,
+ gint ncol,
+ gchar **cols,
+ gchar **names)
+{
+ GSList **uids = ref;
+
+ if (cols[0])
+ *uids = g_slist_prepend (*uids, g_strdup (cols [0]));
+
+ return 0;
+}
+
+static gint
+collect_lean_results_cb (gpointer ref,
+ gint ncol,
+ gchar **cols,
+ gchar **names)
+{
+ GSList **vcard_data = ref;
+ EbSqlSearchData *search_data = g_slice_new0 (EbSqlSearchData);
+ EContact *contact = e_contact_new ();
+ gchar *vcard;
+ gint i;
+
+ /* parse through cols, this will be useful if the api starts supporting field restrictions */
+ for (i = 0; i < ncol; i++) {
+ if (!names[i] || !cols[i])
+ continue;
+
+ /* Only UID & REV can be used to create contacts from the summary columns */
+ if (!g_ascii_strcasecmp (names[i], "uid")) {
+ e_contact_set (contact, E_CONTACT_UID, cols[i]);
+ search_data->uid = g_strdup (cols[i]);
+ } else if (!g_ascii_strcasecmp (names[i], "Rev")) {
+ e_contact_set (contact, E_CONTACT_REV, cols[i]);
+ } else if (!g_ascii_strcasecmp (names[i], "bdata")) {
+ search_data->extra = g_strdup (cols[i]);
+ }
+ }
+
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+ search_data->vcard = vcard;
+ *vcard_data = g_slist_prepend (*vcard_data, search_data);
+
+ g_object_unref (contact);
+ return 0;
+}
+
+static void
+ebsql_string_append_vprintf (GString *string,
+ const gchar *fmt,
+ va_list args)
+{
+ gchar *stmt;
+
+ /* Unfortunately, sqlite3_vsnprintf() doesnt tell us
+ * how many bytes it would have needed if it doesnt fit
+ * into the target buffer, so we can't avoid this
+ * really disgusting memory dup.
+ */
+ stmt = sqlite3_vmprintf (fmt, args);
+ g_string_append (string, stmt);
+ sqlite3_free (stmt);
+}
+
+static void
+ebsql_string_append_printf (GString *string,
+ const gchar *fmt,
+ ...)
+{
+ va_list args;
+
+ va_start (args, fmt);
+ ebsql_string_append_vprintf (string, fmt, args);
+ va_end (args);
+}
+
+/* Appends an identifier suitable to identify the
+ * column to test in the context of a query.
+ *
+ * The suffix is for special indexed columns (such as
+ * reverse values, sort keys, phone numbers, etc).
+ */
+static void
+ebsql_string_append_column (GString *string,
+ SummaryField *field,
+ const gchar *suffix)
+{
+ if (field->aux_table) {
+ g_string_append (string, field->aux_table_symbolic);
+ g_string_append (string, ".value");
+ } else {
+ g_string_append (string, "summary.");
+ g_string_append (string, field->dbname);
+ }
+
+ if (suffix) {
+ g_string_append_c (string, '_');
+ g_string_append (string, suffix);
+ }
+}
+
+static gboolean
+ebsql_exec_vprintf (EBookSqlite *ebsql,
+ const gchar *fmt,
+ EbSqlRowFunc callback,
+ gpointer data,
+ GCancellable *cancellable,
+ GError **error,
+ va_list args)
+{
+ gboolean success;
+ gchar *stmt;
+
+ stmt = sqlite3_vmprintf (fmt, args);
+ success = ebsql_exec (ebsql, stmt, callback, data, cancellable, error);
+ sqlite3_free (stmt);
+
+ return success;
+}
+
+static gboolean
+ebsql_exec_printf (EBookSqlite *ebsql,
+ const gchar *fmt,
+ EbSqlRowFunc callback,
+ gpointer data,
+ GCancellable *cancellable,
+ GError **error,
+ ...)
+{
+ gboolean success;
+ va_list args;
+
+ va_start (args, error);
+ success = ebsql_exec_vprintf (ebsql, fmt, callback, data, cancellable, error, args);
+ va_end (args);
+
+ return success;
+}
+
+static inline void
+ebsql_exec_maybe_debug (EBookSqlite *ebsql,
+ const gchar *stmt)
+{
+ if (ebsql_debug_flags & EBSQL_DEBUG_EXPLAIN &&
+ strncmp (stmt, "SELECT", 6) == 0) {
+ g_printerr ("EXPLAIN BEGIN\n STMT: %s\n", stmt);
+ ebsql_exec_printf (ebsql, "EXPLAIN QUERY PLAN %s",
+ ebsql_debug_query_plan_cb,
+ NULL, NULL, NULL, stmt);
+ g_printerr ("EXPLAIN END\n");
+ } else {
+ EBSQL_NOTE (STATEMENTS, g_printerr ("STMT: %s\n", stmt));
+ }
+}
+
+static gboolean
+ebsql_exec (EBookSqlite *ebsql,
+ const gchar *stmt,
+ EbSqlRowFunc callback,
+ gpointer data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean had_cancel;
+ gchar *errmsg = NULL;
+ gint ret = -1, retries = 0;
+ gint64 t1 = 0, t2;
+
+ /* Debug output for statements and query plans */
+ ebsql_exec_maybe_debug (ebsql, stmt);
+
+ /* Just convenience to set the cancellable on an execution
+ * without a transaction, error checking on the cancellable
+ * is done with EBSQL_LOCK_OR_RETURN()
+ */
+ if (ebsql->priv->cancel) {
+ had_cancel = TRUE;
+ } else {
+ ebsql->priv->cancel = cancellable;
+ had_cancel = FALSE;
+ }
+
+ if ((ebsql_debug_flags & EBSQL_DEBUG_TIMING) != 0 &&
+ strncmp (stmt, "EXPLAIN QUERY PLAN ", 19) != 0)
+ t1 = g_get_monotonic_time();
+
+ ret = sqlite3_exec (ebsql->priv->db, stmt, callback, data, &errmsg);
+
+ while (ret == SQLITE_BUSY || ret == SQLITE_LOCKED || ret == -1) {
+ /* try for ~15 seconds, then give up */
+ if (retries > 150)
+ break;
+ retries++;
+
+ if (errmsg) {
+ sqlite3_free (errmsg);
+ errmsg = NULL;
+ }
+ g_thread_yield ();
+ g_usleep (100 * 1000); /* Sleep for 100 ms */
+
+ if (t1)
+ t1 = g_get_monotonic_time();
+
+ ret = sqlite3_exec (ebsql->priv->db, stmt, callback, data, &errmsg);
+ }
+
+ if (!had_cancel)
+ ebsql->priv->cancel = NULL;
+
+ if (t1) {
+ t2 = g_get_monotonic_time();
+ g_printerr ("TIME: %" G_GINT64_FORMAT " ms\n", (t2 - t1) / 1000);
+ }
+ if (ret != SQLITE_OK) {
+ EBSQL_SET_ERROR_FROM_SQLITE (error, ret, errmsg);
+ sqlite3_free (errmsg);
+ return FALSE;
+ }
+
+ if (errmsg)
+ sqlite3_free (errmsg);
+
+ return TRUE;
+}
+
+static gboolean
+ebsql_start_transaction (EBookSqlite *ebsql,
+ EbSqlLockType lock_type,
+ GCancellable *cancel,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (ebsql != NULL, FALSE);
+ g_return_val_if_fail (ebsql->priv != NULL, FALSE);
+ g_return_val_if_fail (ebsql->priv->db != NULL, FALSE);
+
+ ebsql->priv->in_transaction++;
+ g_return_val_if_fail (ebsql->priv->in_transaction > 0, FALSE);
+
+ if (ebsql->priv->in_transaction == 1) {
+
+ /* No cancellable should be set at transaction start time */
+ if (ebsql->priv->cancel) {
+ g_warning (
+ "Starting a transaction with a cancellable already set. "
+ "Clearing previously set cancellable");
+ g_clear_object (&ebsql->priv->cancel);
+ }
+
+ /* Hold on to the cancel object until the end of the transaction */
+ if (cancel)
+ ebsql->priv->cancel = g_object_ref (cancel);
+
+ /* It's important to make the distinction between a
+ * transaction which will read or one which will write.
+ *
+ * While it's not well documented, when receiving the SQLITE_BUSY
+ * error status, one can only safely retry at the beginning of
+ * the transaction.
+ *
+ * If a transaction is 'upgraded' to require a writer lock
+ * half way through the transaction and SQLITE_BUSY is returned,
+ * the whole transaction would need to be retried from the beginning.
+ */
+ ebsql->priv->lock_type = lock_type;
+
+ switch (lock_type) {
+ case EBSQL_LOCK_READ:
+ success = ebsql_exec (ebsql, "BEGIN", NULL, NULL, NULL, error);
+ break;
+ case EBSQL_LOCK_WRITE:
+ success = ebsql_exec (ebsql, "BEGIN IMMEDIATE", NULL, NULL, NULL, error);
+ break;
+ }
+
+ } else {
+
+ /* Warn about cases where where a read transaction might be upgraded */
+ if (lock_type == EBSQL_LOCK_WRITE && ebsql->priv->lock_type == EBSQL_LOCK_READ)
+ g_warning (
+ "A nested transaction wants to write, "
+ "but the outermost transaction was started "
+ "without a writer lock.");
+ }
+
+ return success;
+}
+
+static gboolean
+ebsql_commit_transaction (EBookSqlite *ebsql,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (ebsql != NULL, FALSE);
+ g_return_val_if_fail (ebsql->priv != NULL, FALSE);
+ g_return_val_if_fail (ebsql->priv->db != NULL, FALSE);
+
+ g_return_val_if_fail (ebsql->priv->in_transaction > 0, FALSE);
+
+ ebsql->priv->in_transaction--;
+
+ if (ebsql->priv->in_transaction == 0) {
+ success = ebsql_exec (ebsql, "COMMIT", NULL, NULL, NULL, error);
+
+ /* The outermost transaction is finished, let's release
+ * our reference to the user's cancel object here */
+ g_clear_object (&ebsql->priv->cancel);
+ }
+
+ return success;
+}
+
+static gboolean
+ebsql_rollback_transaction (EBookSqlite *ebsql,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (ebsql != NULL, FALSE);
+ g_return_val_if_fail (ebsql->priv != NULL, FALSE);
+ g_return_val_if_fail (ebsql->priv->db != NULL, FALSE);
+
+ g_return_val_if_fail (ebsql->priv->in_transaction > 0, FALSE);
+
+ ebsql->priv->in_transaction--;
+
+ if (ebsql->priv->in_transaction == 0) {
+ success = ebsql_exec (ebsql, "ROLLBACK", NULL, NULL, NULL, error);
+
+ /* The outermost transaction is finished, let's release
+ * our reference to the user's cancel object here */
+ g_clear_object (&ebsql->priv->cancel);
+ }
+ return success;
+}
+
+static sqlite3_stmt *
+ebsql_prepare_statement (EBookSqlite *ebsql,
+ const gchar *stmt_str,
+ GError **error)
+{
+ sqlite3_stmt *stmt;
+ const gchar *stmt_tail = NULL;
+ gint ret;
+
+ ret = sqlite3_prepare_v2 (ebsql->priv->db, stmt_str, strlen (stmt_str), &stmt, &stmt_tail);
+
+ if (ret != SQLITE_OK) {
+ const gchar *errmsg = sqlite3_errmsg (ebsql->priv->db);
+ EBSQL_SET_ERROR_LITERAL (
+ error,
+ E_BOOK_SQLITE_ERROR_ENGINE,
+ errmsg);
+ } else if (stmt == NULL) {
+ EBSQL_SET_ERROR_LITERAL (
+ error,
+ E_BOOK_SQLITE_ERROR_ENGINE,
+ "Unknown error preparing SQL statement");
+ }
+
+ if (stmt_tail && stmt_tail[0])
+ g_warning ("Part of this statement was not parsed: %s", stmt_tail);
+
+ return stmt;
+}
+
+/* Convenience for running statements. After successfully
+ * binding all parameters, just return with this.
+ */
+static gboolean
+ebsql_complete_statement (EBookSqlite *ebsql,
+ sqlite3_stmt *stmt,
+ gint ret,
+ GError **error)
+{
+ if (ret == SQLITE_OK)
+ ret = sqlite3_step (stmt);
+
+ if (ret != SQLITE_OK && ret != SQLITE_DONE) {
+ const gchar *errmsg = sqlite3_errmsg (ebsql->priv->db);
+ EBSQL_SET_ERROR_FROM_SQLITE (error, ret, errmsg);
+ }
+
+ /* Reset / Clear at the end, regardless of error state */
+ sqlite3_reset (stmt);
+ sqlite3_clear_bindings (stmt);
+
+ return (ret == SQLITE_OK || ret == SQLITE_DONE);
+}
+
+/******************************************************
+ * Functions installed into the SQLite *
+ ******************************************************/
+
+/* Implementation for REGEXP keyword */
+static void
+ebsql_regexp (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ GRegex *regex;
+ const gchar *expression;
+ const gchar *text;
+
+ /* Reuse the same GRegex for all REGEXP queries with the same expression */
+ regex = sqlite3_get_auxdata (context, 0);
+ if (!regex) {
+ GError *error = NULL;
+
+ expression = (const gchar *) sqlite3_value_text (argv[0]);
+
+ regex = g_regex_new (expression, 0, 0, &error);
+
+ if (!regex) {
+ sqlite3_result_error (
+ context,
+ error ? error->message :
+ _("Error parsing regular expression"),
+ -1);
+ g_clear_error (&error);
+ return;
+ }
+
+ /* SQLite will take care of freeing the GRegex when we're done with the query */
+ sqlite3_set_auxdata (context, 0, regex, (GDestroyNotify) g_regex_unref);
+ }
+
+ /* Now perform the comparison */
+ text = (const gchar *) sqlite3_value_text (argv[1]);
+ if (text != NULL) {
+ gboolean match;
+
+ match = g_regex_match (regex, text, 0, NULL);
+ sqlite3_result_int (context, match ? 1 : 0);
+ }
+}
+
+/* Implementation of EBSQL_FUNC_COMPARE_VCARD (fallback for non-summary queries) */
+static void
+ebsql_compare_vcard (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ EBookBackendSExp *sexp = NULL;
+ const gchar *text;
+ const gchar *vcard;
+
+ /* Reuse the same sexp for all queries with the same search expression */
+ sexp = sqlite3_get_auxdata (context, 0);
+ if (!sexp) {
+
+ /* The first argument will be reused for many rows */
+ text = (const gchar *) sqlite3_value_text (argv[0]);
+ if (text) {
+ sexp = e_book_backend_sexp_new (text);
+ sqlite3_set_auxdata (
+ context, 0,
+ sexp,
+ g_object_unref);
+ }
+
+ /* This shouldn't happen, catch invalid sexp in preflight */
+ if (!sexp) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ }
+
+ /* Reuse the same vcard as much as possible (it can be referred to more than
+ * once in the query, so it can be reused for multiple comparisons on the same row)
+ *
+ * This may look extensive, but as the vcard might be resolved by calling a
+ * EbSqlVCardCallback, it's important to reuse this string as much as possible.
+ *
+ * See ebsql_fetch_vcard() for details.
+ */
+ vcard = sqlite3_get_auxdata (context, 1);
+ if (!vcard) {
+ vcard = (const gchar *) sqlite3_value_text (argv[1]);
+
+ if (vcard)
+ sqlite3_set_auxdata (context, 1, g_strdup (vcard), g_free);
+ }
+
+ /* A NULL vcard can never match */
+ if (vcard == NULL || *vcard == '\0') {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ /* Compare this vcard */
+ if (e_book_backend_sexp_match_vcard (sexp, vcard))
+ sqlite3_result_int (context, 1);
+ else
+ sqlite3_result_int (context, 0);
+}
+
+static void
+ebsql_eqphone (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv,
+ EPhoneNumberMatch requested_match)
+{
+ EBookSqlite *ebsql = sqlite3_user_data (context);
+ EPhoneNumber *input_phone = NULL, *row_phone = NULL;
+ EPhoneNumberMatch match = E_PHONE_NUMBER_MATCH_NONE;
+ const gchar *text;
+
+ /* Reuse the same phone number for all queries with the same phone number argument */
+ input_phone = sqlite3_get_auxdata (context, 0);
+ if (!input_phone) {
+
+ /* The first argument will be reused for many rows */
+ text = (const gchar *) sqlite3_value_text (argv[0]);
+ if (text) {
+
+ /* Ignore errors, they are fine for phone numbers */
+ input_phone = e_phone_number_from_string (text, ebsql->priv->region_code, NULL);
+
+ /* SQLite will take care of freeing the EPhoneNumber when we're done with the expression */
+ if (input_phone)
+ sqlite3_set_auxdata (
+ context, 0,
+ input_phone,
+ (GDestroyNotify) e_phone_number_free);
+ }
+ }
+
+ /* This shouldn't happen, as we catch invalid phone number queries in preflight
+ */
+ if (!input_phone) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ /* Parse the phone number for this row */
+ text = (const gchar *) sqlite3_value_text (argv[1]);
+ if (text != NULL) {
+ row_phone = e_phone_number_from_string (text, ebsql->priv->region_code, NULL);
+
+ /* And perform the comparison */
+ if (row_phone) {
+ match = e_phone_number_compare (input_phone, row_phone);
+
+ e_phone_number_free (row_phone);
+ }
+ }
+
+ /* Now report the result */
+ if (match != E_PHONE_NUMBER_MATCH_NONE &&
+ match <= requested_match)
+ sqlite3_result_int (context, 1);
+ else
+ sqlite3_result_int (context, 0);
+}
+
+/* Exact phone number match function: EBSQL_FUNC_EQPHONE_EXACT */
+static void
+ebsql_eqphone_exact (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ebsql_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_EXACT);
+}
+
+/* National phone number match function: EBSQL_FUNC_EQPHONE_NATIONAL */
+static void
+ebsql_eqphone_national (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ebsql_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_NATIONAL);
+}
+
+/* Short phone number match function: EBSQL_FUNC_EQPHONE_SHORT */
+static void
+ebsql_eqphone_short (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ebsql_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_SHORT);
+}
+
+/* Implementation of EBSQL_FUNC_FETCH_VCARD (fallback for shallow addressbooks) */
+static void
+ebsql_fetch_vcard (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ EBookSqlite *ebsql = sqlite3_user_data (context);
+ const gchar *uid;
+ const gchar *extra;
+ gchar *vcard = NULL;
+
+ uid = (const gchar *) sqlite3_value_text (argv[0]);
+ extra = (const gchar *) sqlite3_value_text (argv[1]);
+
+ /* Call our delegate to generate the vcard */
+ if (ebsql->priv->vcard_callback)
+ vcard = ebsql->priv->vcard_callback (
+ uid, extra, ebsql->priv->user_data);
+
+ EBSQL_NOTE (
+ FETCH_VCARD,
+ g_printerr (
+ "fetch_vcard (%s, %s) %s",
+ uid, extra, vcard ? "Got VCard" : "No VCard"));
+
+ sqlite3_result_text (context, vcard, -1, g_free);
+}
+
+typedef struct {
+ const gchar *name;
+ EbSqlCustomFunc func;
+ gint arguments;
+} EbSqlCustomFuncTab;
+
+static EbSqlCustomFuncTab ebsql_custom_functions[] = {
+ { "regexp", ebsql_regexp, 2 }, /* regexp (expression, column_data) */
+ { EBSQL_FUNC_COMPARE_VCARD, ebsql_compare_vcard, 2 }, /* compare_vcard (sexp, vcard) */
+ { EBSQL_FUNC_FETCH_VCARD, ebsql_fetch_vcard, 2 }, /* fetch_vcard (uid, extra) */
+ { EBSQL_FUNC_EQPHONE_EXACT, ebsql_eqphone_exact, 2 }, /* eqphone_exact (search_input, column_data) */
+ { EBSQL_FUNC_EQPHONE_NATIONAL, ebsql_eqphone_national, 2 }, /* eqphone_national (search_input, column_data) */
+ { EBSQL_FUNC_EQPHONE_SHORT, ebsql_eqphone_short, 2 }, /* eqphone_national (search_input, column_data) */
+};
+
+/******************************************************
+ * Fallback Collation Sequences *
+ ******************************************************
+ *
+ * The fallback simply compares vcards, vcards which have been
+ * stored on the cursor will have a preencoded key (these
+ * utilities encode & decode that key).
+ */
+static gchar *
+ebsql_encode_vcard_sort_key (const gchar *sort_key)
+{
+ EVCard *vcard = e_vcard_new ();
+ gchar *base64;
+ gchar *encoded;
+
+ /* Encode this otherwise e-vcard messes it up */
+ base64 = g_base64_encode ((const guchar *) sort_key, strlen (sort_key));
+ e_vcard_append_attribute_with_value (
+ vcard,
+ e_vcard_attribute_new (NULL, EBSQL_VCARD_SORT_KEY),
+ base64);
+ encoded = e_vcard_to_string (vcard, EVC_FORMAT_VCARD_30);
+
+ g_free (base64);
+ g_object_unref (vcard);
+
+ return encoded;
+}
+
+static gchar *
+ebsql_decode_vcard_sort_key_from_vcard (EVCard *vcard)
+{
+ EVCardAttribute *attr;
+ GList *values = NULL;
+ gchar *sort_key = NULL;
+ gchar *base64 = NULL;
+
+ attr = e_vcard_get_attribute (vcard, EBSQL_VCARD_SORT_KEY);
+ if (attr)
+ values = e_vcard_attribute_get_values (attr);
+
+ if (values && values->data) {
+ gsize len;
+
+ base64 = g_strdup (values->data);
+
+ sort_key = (gchar *) g_base64_decode (base64, &len);
+ g_free (base64);
+ }
+
+ return sort_key;
+}
+
+static gchar *
+ebsql_decode_vcard_sort_key (const gchar *encoded)
+{
+ EVCard *vcard;
+ gchar *sort_key;
+
+ vcard = e_vcard_new_from_string (encoded);
+ sort_key = ebsql_decode_vcard_sort_key_from_vcard (vcard);
+ g_object_unref (vcard);
+
+ return sort_key;
+}
+
+typedef struct {
+ EBookSqlite *ebsql;
+ EContactField field;
+} EbSqlCollData;
+
+static gint
+ebsql_fallback_collator (gpointer ref,
+ gint len1,
+ gconstpointer data1,
+ gint len2,
+ gconstpointer data2)
+{
+ EbSqlCollData *data = (EbSqlCollData *) ref;
+ EBookSqlitePrivate *priv;
+ EContact *contact1, *contact2;
+ const gchar *str1, *str2;
+ gchar *key1, *key2;
+ gchar *tmp;
+ gint result = 0;
+
+ priv = data->ebsql->priv;
+
+ str1 = (const gchar *) data1;
+ str2 = (const gchar *) data2;
+
+ /* Construct 2 contacts (we're comparing vcards) */
+ contact1 = e_contact_new ();
+ contact2 = e_contact_new ();
+ e_vcard_construct_full (E_VCARD (contact1), str1, len1, NULL);
+ e_vcard_construct_full (E_VCARD (contact2), str2, len2, NULL);
+
+ /* Extract first key */
+ key1 = ebsql_decode_vcard_sort_key_from_vcard (E_VCARD (contact1));
+ if (!key1) {
+ tmp = e_contact_get (contact1, data->field);
+ if (tmp)
+ key1 = e_collator_generate_key (priv->collator, tmp, NULL);
+ g_free (tmp);
+ }
+ if (!key1)
+ key1 = g_strdup ("");
+
+ /* Extract second key */
+ key2 = ebsql_decode_vcard_sort_key_from_vcard (E_VCARD (contact2));
+ if (!key2) {
+ tmp = e_contact_get (contact2, data->field);
+ if (tmp)
+ key2 = e_collator_generate_key (priv->collator, tmp, NULL);
+ g_free (tmp);
+ }
+ if (!key2)
+ key2 = g_strdup ("");
+
+ result = strcmp (key1, key2);
+
+ g_free (key1);
+ g_free (key2);
+ g_object_unref (contact1);
+ g_object_unref (contact2);
+
+ return result;
+}
+
+static EbSqlCollData *
+ebsql_coll_data_new (EBookSqlite *ebsql,
+ EContactField field)
+{
+ EbSqlCollData *data = g_slice_new (EbSqlCollData);
+
+ data->ebsql = ebsql;
+ data->field = field;
+
+ return data;
+}
+
+static void
+ebsql_coll_data_free (EbSqlCollData *data)
+{
+ if (data)
+ g_slice_free (EbSqlCollData, data);
+}
+
+/* COLLATE functions are generated on demand only */
+static void
+ebsql_generate_collator (gpointer ref,
+ sqlite3 *db,
+ gint eTextRep,
+ const gchar *coll_name)
+{
+ EBookSqlite *ebsql = (EBookSqlite *) ref;
+ EbSqlCollData *data;
+ EContactField field;
+ const gchar *field_name;
+
+ field_name = coll_name + strlen (EBSQL_COLLATE_PREFIX);
+ field = e_contact_field_id (field_name);
+
+ /* This should be caught before reaching here, just an extra check */
+ if (field == 0 || field >= E_CONTACT_FIELD_LAST ||
+ e_contact_field_type (field) != G_TYPE_STRING) {
+ g_warning ("Specified collation on invalid contact field");
+ return;
+ }
+
+ data = ebsql_coll_data_new (ebsql, field);
+ sqlite3_create_collation_v2 (
+ db, coll_name, SQLITE_UTF8,
+ data, ebsql_fallback_collator,
+ (GDestroyNotify) ebsql_coll_data_free);
+}
+
+/**********************************************************
+ * Cancel long operations with GCancellable *
+ **********************************************************/
+static gint
+ebsql_check_cancel (gpointer ref)
+{
+ EBookSqlite *ebsql = (EBookSqlite *) ref;
+
+ if (ebsql->priv->cancel &&
+ g_cancellable_is_cancelled (ebsql->priv->cancel)) {
+ EBSQL_NOTE (
+ CANCEL,
+ g_printerr ("CANCEL: An operation was cancelled\n"));
+ return -1;
+ }
+
+ return 0;
+}
+
+/**********************************************************
+ * Database Initialization *
+ **********************************************************/
+static inline gint
+main_table_index_by_name (const gchar *name)
+{
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (main_table_columns); i++) {
+ if (g_strcmp0 (name, main_table_columns[i].name) == 0)
+ return i;
+ }
+
+ return -1;
+}
+
+static gint
+check_main_table_columns (gpointer data,
+ gint n_cols,
+ gchar **cols,
+ gchar **name)
+{
+ guint *columns_mask = (guint *) data;
+ gint i;
+
+ for (i = 0; i < n_cols; i++) {
+
+ if (g_strcmp0 (name[i], "name") == 0) {
+ gint idx = main_table_index_by_name (cols[i]);
+
+ if (idx >= 0)
+ *columns_mask |= (1 << idx);
+
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static gboolean
+ebsql_init_sqlite (EBookSqlite *ebsql,
+ const gchar *filename,
+ GError **error)
+{
+ gint ret, i;
+
+ e_sqlite3_vfs_init ();
+
+ ret = sqlite3_open (filename, &ebsql->priv->db);
+
+ /* Handle GCancellable */
+ sqlite3_progress_handler (
+ ebsql->priv->db,
+ EBSQL_CANCEL_BATCH_SIZE,
+ ebsql_check_cancel,
+ ebsql);
+
+ /* Install our custom functions */
+ for (i = 0; ret == SQLITE_OK && i < G_N_ELEMENTS (ebsql_custom_functions); i++)
+ ret = sqlite3_create_function (
+ ebsql->priv->db,
+ ebsql_custom_functions[i].name,
+ ebsql_custom_functions[i].arguments,
+ SQLITE_UTF8, ebsql,
+ ebsql_custom_functions[i].func,
+ NULL, NULL);
+
+ /* Fallback COLLATE implementations generated on demand */
+ if (ret == SQLITE_OK)
+ ret = sqlite3_collation_needed (
+ ebsql->priv->db, ebsql, ebsql_generate_collator);
+
+ if (ret != SQLITE_OK) {
+ if (!ebsql->priv->db) {
+ EBSQL_SET_ERROR_LITERAL (
+ error,
+ E_BOOK_SQLITE_ERROR_LOAD,
+ _("Insufficient memory"));
+ } else {
+ const gchar *errmsg = sqlite3_errmsg (ebsql->priv->db);
+
+ EBSQL_SET_ERROR (
+ error,
+ E_BOOK_SQLITE_ERROR_ENGINE,
+ "Can't open database %s: %s\n",
+ filename, errmsg);
+ sqlite3_close (ebsql->priv->db);
+ }
+ return FALSE;
+ }
+
+ ebsql_exec (ebsql, "ATTACH DATABASE ':memory:' AS mem", NULL, NULL, NULL, NULL);
+ ebsql_exec (ebsql, "PRAGMA foreign_keys = ON", NULL, NULL, NULL, NULL);
+ ebsql_exec (ebsql, "PRAGMA case_sensitive_like = ON", NULL, NULL, NULL, NULL);
+
+ return TRUE;
+}
+
+static inline void
+format_column_declaration (GString *string,
+ ColumnInfo *info)
+{
+ g_string_append (string, info->name);
+ g_string_append_c (string, ' ');
+
+ g_string_append (string, info->type);
+
+ if (info->extra) {
+ g_string_append_c (string, ' ');
+ g_string_append (string, info->extra);
+ }
+}
+
+static inline gboolean
+ensure_column_index (EBookSqlite *ebsql,
+ const gchar *table,
+ ColumnInfo *info,
+ GError **error)
+{
+ if (!info->index)
+ return TRUE;
+
+ return ebsql_exec_printf (
+ ebsql,
+ "CREATE INDEX IF NOT EXISTS %Q ON %Q (%s)",
+ NULL, NULL, NULL, error,
+ info->index, table, info->name);
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_resolve_folderid (EBookSqlite *ebsql,
+ gint *previous_schema,
+ gint *already_exists,
+ GError **error)
+{
+ gint n_folders = 0;
+ gint version = 0;
+ gchar *loaded_folder_id = NULL;
+ gboolean success;
+
+ success = ebsql_exec (
+ ebsql, "SELECT count(*) FROM sqlite_master "
+ "WHERE type='table' AND name='folders';",
+ get_count_cb, &n_folders, NULL, error);
+
+ if (success && n_folders > 1) {
+ EBSQL_SET_ERROR_LITERAL (
+ error,
+ E_BOOK_SQLITE_ERROR_LOAD,
+ _("Cannot upgrade contacts database from a legacy "
+ "database with more than one addressbook. "
+ "Delete one of the entries in the 'folders' table first."));
+ success = FALSE;
+ }
+
+ if (success && n_folders == 1)
+ success = ebsql_exec (
+ ebsql, "SELECT folder_id FROM folders LIMIT 1",
+ get_string_cb, &loaded_folder_id, NULL, error);
+
+ if (success && n_folders == 1)
+ success = ebsql_exec (
+ ebsql, "SELECT version FROM folders LIMIT 1",
+ get_int_cb, &version, NULL, error);
+
+ if (success && n_folders == 1) {
+ g_free (ebsql->priv->folderid);
+ ebsql->priv->folderid = loaded_folder_id;
+ } else {
+ g_free (loaded_folder_id);
+ }
+
+ if (n_folders == 1)
+ *already_exists = TRUE;
+ else
+ *already_exists = FALSE;
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: main folder id resolved as '%s', "
+ "already existing tables: %d loaded version: %d (%s)\n",
+ ebsql->priv->folderid, n_folders, version,
+ success ? "success" : "failed"));
+
+ *previous_schema = version;
+
+ return success;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_init_folders (EBookSqlite *ebsql,
+ gint previous_schema,
+ GError **error)
+{
+ GString *string;
+ guint existing_columns_mask = 0, i;
+ gboolean success;
+
+ string = g_string_sized_new (COLUMN_DEFINITION_BYTES * G_N_ELEMENTS (main_table_columns));
+ g_string_append (string, "CREATE TABLE IF NOT EXISTS folders (");
+ for (i = 0; i < G_N_ELEMENTS (main_table_columns); i++) {
+
+ if (i > 0)
+ g_string_append (string, ", ");
+
+ format_column_declaration (string, &(main_table_columns[i]));
+ }
+ g_string_append_c (string, ')');
+
+ /* Create main folders table */
+ success = ebsql_exec (ebsql, string->str, NULL, NULL, NULL, error);
+ g_string_free (string, TRUE);
+
+ /* Check which columns in the main table already exist */
+ if (success)
+ success = ebsql_exec (
+ ebsql, "PRAGMA table_info (folders)",
+ check_main_table_columns, &existing_columns_mask,
+ NULL, error);
+
+ /* Add columns which may be missing */
+ for (i = 0; success && i < G_N_ELEMENTS (main_table_columns); i++) {
+ ColumnInfo *info = &(main_table_columns[i]);
+
+ if ((existing_columns_mask & (1 << i)) != 0)
+ continue;
+
+ success = ebsql_exec_printf (
+ ebsql, "ALTER TABLE folders ADD COLUMN %s %s %s",
+ NULL, NULL, NULL, error, info->name, info->type,
+ info->extra ? info->extra : "");
+ }
+
+ /* Special case upgrade for schema versions 3 & 4.
+ *
+ * Drops the reverse_multivalues column.
+ */
+ if (success && previous_schema >= 3 && previous_schema < 5) {
+
+ success = ebsql_exec (
+ ebsql,
+ "UPDATE folders SET "
+ "multivalues = REPLACE(RTRIM(REPLACE("
+ "multivalues || ':', ':', "
+ "CASE reverse_multivalues "
+ "WHEN 0 THEN ';prefix ' "
+ "ELSE ';prefix;suffix ' "
+ "END)), ' ', ':'), "
+ "reverse_multivalues = NULL",
+ NULL, NULL, NULL, error);
+ }
+
+ /* Finish the eventual upgrade by storing the current schema version.
+ */
+ if (success && previous_schema >= 1 && previous_schema < FOLDER_VERSION)
+ success = ebsql_exec_printf (
+ ebsql, "UPDATE folders SET version = %d",
+ NULL, NULL, NULL, error, FOLDER_VERSION);
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: Initialized main folders table (%s)\n",
+ success ? "success" : "failed"));
+
+ return success;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_init_keys (EBookSqlite *ebsql,
+ GError **error)
+{
+ gboolean success;
+
+ /* Create a child table to store key/value pairs for a folder. */
+ success = ebsql_exec (
+ ebsql,
+ "CREATE TABLE IF NOT EXISTS keys ("
+ " key TEXT PRIMARY KEY,"
+ " value TEXT,"
+ " folder_id TEXT REFERENCES folders)",
+ NULL, NULL, NULL, error);
+
+ /* Add an index on the keys */
+ if (success)
+ success = ebsql_exec (
+ ebsql,
+ "CREATE INDEX IF NOT EXISTS keysindex ON keys (folder_id)",
+ NULL, NULL, NULL, error);
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: Initialized keys table (%s)\n",
+ success ? "success" : "failed"));
+
+ return success;
+}
+
+static gchar *
+format_multivalues (EBookSqlite *ebsql)
+{
+ gint i;
+ GString *string;
+ gboolean first = TRUE;
+
+ string = g_string_new (NULL);
+
+ for (i = 0; i < ebsql->priv->n_summary_fields; i++) {
+ if (ebsql->priv->summary_fields[i].type == E_TYPE_CONTACT_ATTR_LIST) {
+ if (first)
+ first = FALSE;
+ else
+ g_string_append_c (string, ':');
+
+ g_string_append (string, ebsql->priv->summary_fields[i].dbname);
+
+ /* E_BOOK_INDEX_SORT_KEY is not supported in the multivalue fields */
+ if ((ebsql->priv->summary_fields[i].index & INDEX_FLAG (PREFIX)) != 0)
+ g_string_append (string, ";prefix");
+ if ((ebsql->priv->summary_fields[i].index & INDEX_FLAG (SUFFIX)) != 0)
+ g_string_append (string, ";suffix");
+ if ((ebsql->priv->summary_fields[i].index & INDEX_FLAG (PHONE)) != 0)
+ g_string_append (string, ";phone");
+ }
+ }
+
+ return g_string_free (string, string->len == 0);
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_add_folder (EBookSqlite *ebsql,
+ GError **error)
+{
+ gboolean success;
+ gchar *multivalues;
+ const gchar *lc_collate;
+
+ multivalues = format_multivalues (ebsql);
+ lc_collate = setlocale (LC_COLLATE, NULL);
+
+ success = ebsql_exec_printf (
+ ebsql,
+ "INSERT OR IGNORE INTO folders"
+ " ( folder_id, version, multivalues, lc_collate ) "
+ "VALUES ( %Q, %d, %Q, %Q ) ",
+ NULL, NULL, NULL, error,
+ ebsql->priv->folderid, FOLDER_VERSION, multivalues, lc_collate);
+
+ g_free (multivalues);
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: Added '%s' entry to main folder (%s)\n",
+ ebsql->priv->folderid, success ? "success" : "failed"));
+
+ return success;
+}
+
+static gboolean
+ebsql_email_list_exists (EBookSqlite *ebsql)
+{
+ gint n_tables = 0;
+ gboolean success;
+
+ success = ebsql_exec_printf (
+ ebsql, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='%q_email_list';",
+ get_count_cb, &n_tables, NULL, NULL,
+ ebsql->priv->folderid);
+
+ if (!success)
+ return FALSE;
+
+ return n_tables == 1;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_introspect_summary (EBookSqlite *ebsql,
+ gint previous_schema,
+ GSList **introspected_columns,
+ GError **error)
+{
+ gboolean success;
+ GSList *summary_columns = NULL, *l;
+ GArray *summary_fields = NULL;
+ gchar *multivalues = NULL;
+ gint i, j;
+
+ success = ebsql_exec_printf (
+ ebsql, "PRAGMA table_info (%Q);",
+ get_columns_cb, &summary_columns, NULL, error,
+ ebsql->priv->folderid);
+
+ if (!success)
+ goto introspect_summary_finish;
+
+ summary_columns = g_slist_reverse (summary_columns);
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+
+ /* Introspect the normal summary fields */
+ for (l = summary_columns; l; l = l->next) {
+ EContactField field_id;
+ const gchar *col = l->data;
+ gchar *p;
+ gint computed = 0;
+ gchar *freeme = NULL;
+
+ /* Note that we don't have any way to introspect
+ * E_BOOK_INDEX_PREFIX, this is not important because if
+ * the prefix index is specified, it will be created
+ * the first time the SQLite tables are created, so
+ * it's not important to ensure prefix indexes after
+ * introspecting the summary.
+ */
+
+ /* Check if we're parsing a reverse field */
+ if ((p = strstr (col, "_" EBSQL_SUFFIX_REVERSE)) != NULL) {
+ computed = INDEX_FLAG (SUFFIX);
+ freeme = g_strndup (col, p - col);
+ col = freeme;
+ } else if ((p = strstr (col, "_" EBSQL_SUFFIX_PHONE)) != NULL) {
+ computed = INDEX_FLAG (PHONE);
+ freeme = g_strndup (col, p - col);
+ col = freeme;
+ } else if ((p = strstr (col, "_" EBSQL_SUFFIX_COUNTRY)) != NULL) {
+ computed = INDEX_FLAG (PHONE);
+ freeme = g_strndup (col, p - col);
+ col = freeme;
+ } else if ((p = strstr (col, "_" EBSQL_SUFFIX_SORT_KEY)) != NULL) {
+ computed = INDEX_FLAG (SORT_KEY);
+ freeme = g_strndup (col, p - col);
+ col = freeme;
+ }
+
+ /* First check exception fields */
+ if (g_ascii_strcasecmp (col, "uid") == 0)
+ field_id = E_CONTACT_UID;
+ else if (g_ascii_strcasecmp (col, "is_list") == 0)
+ field_id = E_CONTACT_IS_LIST;
+ else
+ field_id = e_contact_field_id (col);
+
+ /* Check for parse error */
+ if (field_id == 0) {
+ EBSQL_SET_ERROR (
+ error,
+ E_BOOK_SQLITE_ERROR_UNSUPPORTED_FIELD,
+ _("Error introspecting unknown summary field '%s'"),
+ col);
+ success = FALSE;
+ g_free (freeme);
+ break;
+ }
+
+ /* Computed columns are always declared after the normal columns,
+ * if a reverse field is encountered we need to set the suffix
+ * index on the coresponding summary field
+ */
+ if (computed) {
+ gint field_idx;
+ SummaryField *iter;
+
+ field_idx = summary_field_array_index (summary_fields, field_id);
+ if (field_idx >= 0) {
+ iter = &g_array_index (summary_fields, SummaryField, field_idx);
+ iter->index |= computed;
+ }
+
+ } else {
+ summary_field_append (
+ summary_fields, ebsql->priv->folderid,
+ field_id, NULL);
+ }
+
+ g_free (freeme);
+ }
+
+ if (!success)
+ goto introspect_summary_finish;
+
+ /* Introspect the multivalied summary fields */
+ success = ebsql_exec_printf (
+ ebsql,
+ "SELECT multivalues FROM folders "
+ "WHERE folder_id = %Q",
+ get_string_cb, &multivalues, NULL, error,
+ ebsql->priv->folderid);
+
+ if (!success)
+ goto introspect_summary_finish;
+
+ if (!multivalues || !*multivalues) {
+ g_free (multivalues);
+ multivalues = NULL;
+
+ /* The migration from a previous version didn't store this default multivalue
+ reference, thus the next backend open (not the immediate one after migration),
+ didn't know about this table, which has a FOREIGN KEY constraint, thus an item
+ delete caused a 'FOREIGN KEY constraint failed' error.
+ */
+ if (ebsql_email_list_exists (ebsql))
+ multivalues = g_strdup ("email;prefix");
+ }
+
+ if (multivalues) {
+ gchar **fields = g_strsplit (multivalues, ":", 0);
+
+ for (i = 0; fields[i] != NULL; i++) {
+ EContactField field_id;
+ SummaryField *iter;
+ gchar **params;
+
+ params = g_strsplit (fields[i], ";", 0);
+ field_id = e_contact_field_id (params[0]);
+ iter = summary_field_append (
+ summary_fields,
+ ebsql->priv->folderid,
+ field_id, NULL);
+
+ if (iter) {
+ for (j = 1; params[j]; ++j) {
+ /* Sort keys not supported for multivalued fields */
+ if (strcmp (params[j], "prefix") == 0) {
+ iter->index |= INDEX_FLAG (PREFIX);
+ } else if (strcmp (params[j], "suffix") == 0) {
+ iter->index |= INDEX_FLAG (SUFFIX);
+ } else if (strcmp (params[j], "phone") == 0) {
+ iter->index |= INDEX_FLAG (PHONE);
+ }
+ }
+ }
+
+ g_strfreev (params);
+ }
+
+ g_strfreev (fields);
+ }
+
+ /* HARD CODE UP AHEAD
+ *
+ * Now we're finished introspecting, if the summary is from a previous version,
+ * we need to add any summary fields which we're added to the default summary
+ * since the schema version which was introduced here
+ */
+ if (previous_schema >= 1) {
+ SummaryField *summary_field;
+
+ if (previous_schema < 8) {
+
+ /* We used to keep 4 email fields in the summary, before we supported
+ * the multivaliued E_CONTACT_EMAIL... convert the old summary to use
+ * the multivaliued field instead.
+ */
+ if (summary_field_array_index (summary_fields, E_CONTACT_EMAIL_1) >= 0 &&
+ summary_field_array_index (summary_fields, E_CONTACT_EMAIL_2) >= 0 &&
+ summary_field_array_index (summary_fields, E_CONTACT_EMAIL_3) >= 0 &&
+ summary_field_array_index (summary_fields, E_CONTACT_EMAIL_4) >= 0) {
+
+ summary_field_remove (summary_fields, E_CONTACT_EMAIL_1);
+ summary_field_remove (summary_fields, E_CONTACT_EMAIL_2);
+ summary_field_remove (summary_fields, E_CONTACT_EMAIL_3);
+ summary_field_remove (summary_fields, E_CONTACT_EMAIL_4);
+
+ summary_field = summary_field_append (
+ summary_fields,
+ ebsql->priv->folderid,
+ E_CONTACT_EMAIL, NULL);
+ summary_field->index |= INDEX_FLAG (PREFIX);
+ }
+
+ /* Regardless of whether it was a default summary or not, add the sort
+ * keys to anything less than Schema 8 (as long as those fields are at least
+ * in the summary)
+ */
+ if ((i = summary_field_array_index (summary_fields, E_CONTACT_FILE_AS)) >= 0) {
+ summary_field = &g_array_index (summary_fields, SummaryField, i);
+ summary_field->index |= INDEX_FLAG (SORT_KEY);
+ }
+
+ if ((i = summary_field_array_index (summary_fields, E_CONTACT_GIVEN_NAME)) >= 0) {
+ summary_field = &g_array_index (summary_fields, SummaryField, i);
+ summary_field->index |= INDEX_FLAG (SORT_KEY);
+ }
+
+ if ((i = summary_field_array_index (summary_fields, E_CONTACT_FAMILY_NAME)) >= 0) {
+ summary_field = &g_array_index (summary_fields, SummaryField, i);
+ summary_field->index |= INDEX_FLAG (SORT_KEY);
+ }
+ }
+
+ if (previous_schema < 9) {
+ if (summary_field_array_index (summary_fields, E_CONTACT_X509_CERT) < 0) {
+ summary_field_append (summary_fields, ebsql->priv->folderid,
+ E_CONTACT_X509_CERT, NULL);
+ }
+ }
+
+ if (previous_schema < 10) {
+ if ((i = summary_field_array_index (summary_fields, E_CONTACT_NICKNAME)) >= 0) {
+ summary_field = &g_array_index (summary_fields, SummaryField, i);
+ summary_field->index |= INDEX_FLAG (PREFIX);
+ }
+
+ if ((i = summary_field_array_index (summary_fields, E_CONTACT_FILE_AS)) >= 0) {
+ summary_field = &g_array_index (summary_fields, SummaryField, i);
+ summary_field->index |= INDEX_FLAG (PREFIX);
+ }
+
+ if ((i = summary_field_array_index (summary_fields, E_CONTACT_GIVEN_NAME)) >= 0) {
+ summary_field = &g_array_index (summary_fields, SummaryField, i);
+ summary_field->index |= INDEX_FLAG (PREFIX);
+ }
+
+ if ((i = summary_field_array_index (summary_fields, E_CONTACT_FAMILY_NAME)) >= 0) {
+ summary_field = &g_array_index (summary_fields, SummaryField, i);
+ summary_field->index |= INDEX_FLAG (PREFIX);
+ }
+
+ }
+ }
+
+ introspect_summary_finish:
+
+ /* Apply the introspected summary fields */
+ if (success) {
+ summary_fields_array_free (
+ ebsql->priv->summary_fields,
+ ebsql->priv->n_summary_fields);
+
+ ebsql->priv->n_summary_fields = summary_fields->len;
+ ebsql->priv->summary_fields = (SummaryField *) g_array_free (summary_fields, FALSE);
+
+ *introspected_columns = summary_columns;
+ } else if (summary_fields) {
+ gint n_fields;
+ SummaryField *fields;
+
+ /* Properly free the array */
+ n_fields = summary_fields->len;
+ fields = (SummaryField *) g_array_free (summary_fields, FALSE);
+ summary_fields_array_free (fields, n_fields);
+
+ g_slist_free_full (summary_columns, (GDestroyNotify) g_free);
+ }
+
+ g_free (multivalues);
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: Introspected summary (%s)\n",
+ success ? "success" : "failed"));
+
+ return success;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_init_contacts (EBookSqlite *ebsql,
+ GSList *introspected_columns,
+ GError **error)
+{
+ gint i;
+ gboolean success = TRUE;
+ GString *string;
+ GSList *summary_columns = NULL, *l;
+
+ /* Get a list of all columns and indexes which should be present
+ * in the main summary table */
+ for (i = 0; i < ebsql->priv->n_summary_fields; i++) {
+ SummaryField *field = &(ebsql->priv->summary_fields[i]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST) {
+ l = summary_field_list_columns (field, ebsql->priv->folderid);
+ summary_columns = g_slist_concat (summary_columns, l);
+ }
+ }
+
+ /* Create the main contacts table for this folder
+ */
+ string = g_string_sized_new (32 * g_slist_length (summary_columns));
+ g_string_append (string, "CREATE TABLE IF NOT EXISTS %Q (");
+
+ for (l = summary_columns; l; l = l->next) {
+ ColumnInfo *info = l->data;
+
+ if (l != summary_columns)
+ g_string_append (string, ", ");
+
+ format_column_declaration (string, info);
+ }
+ g_string_append (string, ", vcard TEXT, bdata TEXT)");
+
+ success = ebsql_exec_printf (
+ ebsql, string->str,
+ NULL, NULL, NULL, error,
+ ebsql->priv->folderid);
+
+ g_string_free (string, TRUE);
+
+ /* If we introspected something, let's first adjust the contacts table
+ * so that it includes the right columns */
+ if (introspected_columns) {
+
+ /* Add any missing columns which are in the summary fields but
+ * not found in the contacts table
+ */
+ for (l = summary_columns; success && l; l = l->next) {
+ ColumnInfo *info = l->data;
+
+ if (g_slist_find_custom (introspected_columns,
+ info->name, (GCompareFunc) g_ascii_strcasecmp))
+ continue;
+
+ success = ebsql_exec_printf (
+ ebsql,
+ "ALTER TABLE %Q ADD COLUMN %s %s %s",
+ NULL, NULL, NULL, error,
+ ebsql->priv->folderid,
+ info->name, info->type,
+ info->extra ? info->extra : "");
+ }
+ }
+
+ /* Add indexes to columns in the main contacts table
+ */
+ for (l = summary_columns; success && l; l = l->next) {
+ ColumnInfo *info = l->data;
+
+ success = ensure_column_index (ebsql, ebsql->priv->folderid, info, error);
+ }
+
+ g_slist_free_full (summary_columns, (GDestroyNotify) column_info_free);
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: Initialized summary table '%s' (%s)\n",
+ ebsql->priv->folderid, success ? "success" : "failed"));
+
+ return success;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_init_aux_tables (EBookSqlite *ebsql,
+ gint previous_schema,
+ GError **error)
+{
+ GString *string;
+ gboolean success = TRUE;
+ GSList *aux_columns = NULL, *l;
+ gchar *tmp;
+ gint i;
+
+ /* Drop the general 'folder_id_lists' table which was used prior to
+ * version 8 of the schema
+ */
+ if (previous_schema >= 1 && previous_schema < 8) {
+ tmp = g_strconcat (ebsql->priv->folderid, "_lists", NULL);
+ success = ebsql_exec_printf (
+ ebsql, "DROP TABLE IF EXISTS %Q",
+ NULL, NULL, NULL, error, tmp);
+ g_free (tmp);
+ }
+
+ for (i = 0; success && i < ebsql->priv->n_summary_fields; i++) {
+ SummaryField *field = &(ebsql->priv->summary_fields[i]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ aux_columns = summary_field_list_columns (field, ebsql->priv->folderid);
+
+ /* Create the auxiliary table for this multi valued field */
+ string = g_string_sized_new (
+ COLUMN_DEFINITION_BYTES * 3 +
+ COLUMN_DEFINITION_BYTES * g_slist_length (aux_columns));
+
+ g_string_append (string, "CREATE TABLE IF NOT EXISTS %Q (uid TEXT NOT NULL REFERENCES %Q (uid)");
+ for (l = aux_columns; l; l = l->next) {
+ ColumnInfo *info = l->data;
+
+ g_string_append (string, ", ");
+ format_column_declaration (string, info);
+ }
+ g_string_append_c (string, ')');
+
+ success = ebsql_exec_printf (
+ ebsql, string->str, NULL, NULL, NULL, error,
+ field->aux_table, ebsql->priv->folderid);
+ g_string_free (string, TRUE);
+
+ if (success) {
+
+ /* Create an index on the implied 'uid' column, this is important
+ * when replacing (modifying) contacts, since we need to remove
+ * all rows in an auxiliary table which matches a given UID.
+ *
+ * This index speeds up the constraint in a statement such as:
+ *
+ * DELETE from email_list WHERE email_list.uid = 'contact uid'
+ */
+ tmp = g_strconcat (
+ "UID_INDEX",
+ "_", field->dbname,
+ "_", ebsql->priv->folderid,
+ NULL);
+ ebsql_exec_printf (
+ ebsql,
+ "CREATE INDEX IF NOT EXISTS %Q ON %Q (%s)",
+ NULL, NULL, NULL, error,
+ tmp, field->aux_table, "uid");
+ g_free (tmp);
+ }
+
+ /* Add indexes to columns in this auxiliary table
+ */
+ for (l = aux_columns; success && l; l = l->next) {
+ ColumnInfo *info = l->data;
+
+ success = ensure_column_index (ebsql, field->aux_table, info, error);
+ }
+
+ g_slist_free_full (aux_columns, (GDestroyNotify) column_info_free);
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: Initialized auxiliary table '%s'\n",
+ field->aux_table));
+ }
+
+ if (success) {
+ gchar *multivalues;
+
+ multivalues = format_multivalues (ebsql);
+
+ success = ebsql_exec_printf (
+ ebsql,
+ "UPDATE folders SET multivalues=%Q WHERE folder_id=%Q",
+ NULL, NULL, NULL, error,
+ multivalues, ebsql->priv->folderid);
+
+ g_free (multivalues);
+ }
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: Initialized auxiliary tables (%s)\n",
+ success ? "success" : "failed"));
+
+ return success;
+}
+
+static gboolean
+ebsql_upgrade_one (EBookSqlite *ebsql,
+ EbSqlChangeType change_type,
+ EbSqlSearchData *result,
+ GError **error)
+{
+ EContact *contact = NULL;
+ gboolean success;
+
+ /* It can be we're opening a light summary which was created without
+ * storing the vcards, such as was used in EDS versions 3.2 to 3.6.
+ *
+ * In this case we just want to skip the contacts we can't load
+ * and leave them as is in the SQLite, they will be added from
+ * the old BDB in the case of a migration anyway.
+ */
+ if (result->vcard)
+ contact = e_contact_new_from_vcard_with_uid (result->vcard, result->uid);
+
+ if (contact == NULL)
+ return TRUE;
+
+ success = ebsql_insert_contact (
+ ebsql, change_type, contact,
+ result->vcard, result->extra,
+ TRUE, error);
+
+ g_object_unref (contact);
+
+ return success;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_upgrade (EBookSqlite *ebsql,
+ EbSqlChangeType change_type,
+ GError **error)
+{
+ gchar *uid = NULL;
+ gint n_results;
+ gboolean success = TRUE;
+
+ do {
+ GSList *batch = NULL, *l;
+ EbSqlSearchData *result = NULL;
+
+ if (uid == NULL) {
+ success = ebsql_exec_printf (
+ ebsql,
+ "SELECT summary.uid, %s, summary.bdata FROM %Q AS summary "
+ "ORDER BY summary.uid ASC LIMIT %d",
+ collect_full_results_cb, &batch, NULL, error,
+ EBSQL_VCARD_FRAGMENT (ebsql),
+ ebsql->priv->folderid, EBSQL_UPGRADE_BATCH_SIZE);
+ } else {
+ success = ebsql_exec_printf (
+ ebsql,
+ "SELECT summary.uid, %s, summary.bdata FROM %Q AS summary "
+ "WHERE summary.uid > %Q "
+ "ORDER BY summary.uid ASC LIMIT %d",
+ collect_full_results_cb, &batch, NULL, error,
+ EBSQL_VCARD_FRAGMENT (ebsql),
+ ebsql->priv->folderid, uid, EBSQL_UPGRADE_BATCH_SIZE);
+ }
+
+ /* Reverse the list, we want to walk through it forwards */
+ batch = g_slist_reverse (batch);
+ for (l = batch; success && l; l = l->next) {
+ result = l->data;
+ success = ebsql_upgrade_one (
+ ebsql,
+ change_type,
+ result,
+ error);
+ }
+
+ /* result is now the last one in the list */
+ if (result) {
+ g_free (uid);
+ uid = result->uid;
+ result->uid = NULL;
+ }
+
+ n_results = g_slist_length (batch);
+ g_slist_free_full (batch, (GDestroyNotify) e_book_sqlite_search_data_free);
+
+ } while (success && n_results == EBSQL_UPGRADE_BATCH_SIZE);
+
+ g_free (uid);
+
+ /* Store the new locale & country code */
+ if (success)
+ success = ebsql_exec_printf (
+ ebsql, "UPDATE folders SET countrycode = %Q WHERE folder_id = %Q",
+ NULL, NULL, NULL, error,
+ ebsql->priv->region_code, ebsql->priv->folderid);
+
+ if (success)
+ success = ebsql_exec_printf (
+ ebsql, "UPDATE folders SET lc_collate = %Q WHERE folder_id = %Q",
+ NULL, NULL, NULL, error,
+ ebsql->priv->locale, ebsql->priv->folderid);
+
+ return success;
+}
+
+static gboolean
+ebsql_set_locale_internal (EBookSqlite *ebsql,
+ const gchar *locale,
+ GError **error)
+{
+ EBookSqlitePrivate *priv = ebsql->priv;
+ ECollator *collator;
+
+ g_return_val_if_fail (locale && locale[0], FALSE);
+
+ if (g_strcmp0 (priv->locale, locale) != 0) {
+ gchar *country_code = NULL;
+
+ collator = e_collator_new_interpret_country (
+ locale, &country_code, error);
+ if (collator == NULL)
+ return FALSE;
+
+ /* Assign region code parsed from the locale by ICU */
+ g_free (priv->region_code);
+ priv->region_code = country_code;
+
+ /* Assign locale */
+ g_free (priv->locale);
+ priv->locale = g_strdup (locale);
+
+ /* Assign collator */
+ if (ebsql->priv->collator)
+ e_collator_unref (ebsql->priv->collator);
+ ebsql->priv->collator = collator;
+ }
+
+ return TRUE;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_init_legacy_keys (EBookSqlite *ebsql,
+ gint previous_schema,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ /* Schema 8 is when we moved from EBookSqlite */
+ if (previous_schema >= 1 && previous_schema < 8) {
+ gint is_populated = 0;
+ gchar *sync_data = NULL;
+
+ /* We need to hold on to the value of any previously set 'is_populated' flag */
+ success = ebsql_exec_printf (
+ ebsql, "SELECT is_populated FROM folders WHERE folder_id = %Q",
+ get_int_cb, &is_populated, NULL, error, ebsql->priv->folderid);
+
+ if (success) {
+ /* We can't use e_book_sqlite_set_key_value_int() at this
+ * point as that would hold the access locks
+ */
+ success = ebsql_exec_printf (
+ ebsql, "INSERT or REPLACE INTO keys (key, value, folder_id) values (%Q, %Q, %Q)",
+ NULL, NULL, NULL, error,
+ E_BOOK_SQL_IS_POPULATED_KEY,
+ is_populated ? "1" : "0",
+ ebsql->priv->folderid);
+ }
+
+ /* Repeat for 'sync_data' */
+ success = success && ebsql_exec_printf (
+ ebsql, "SELECT sync_data FROM folders WHERE folder_id = %Q",
+ get_string_cb, &sync_data, NULL, error, ebsql->priv->folderid);
+
+ if (success) {
+ success = ebsql_exec_printf (
+ ebsql, "INSERT or REPLACE INTO keys (key, value, folder_id) values (%Q, %Q, %Q)",
+ NULL, NULL, NULL, error,
+ E_BOOK_SQL_SYNC_DATA_KEY,
+ sync_data, ebsql->priv->folderid);
+
+ g_free (sync_data);
+ }
+ }
+
+ return success;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebsql_init_locale (EBookSqlite *ebsql,
+ gint previous_schema,
+ gboolean already_exists,
+ GError **error)
+{
+ gchar *stored_lc_collate = NULL;
+ gchar *stored_region_code = NULL;
+ const gchar *lc_collate = NULL;
+ gboolean success = TRUE;
+ gboolean relocalize_needed = FALSE;
+
+ /* Get the locale setting for this addressbook */
+ if (already_exists) {
+ success = ebsql_exec_printf (
+ ebsql, "SELECT lc_collate FROM folders WHERE folder_id = %Q",
+ get_string_cb, &stored_lc_collate, NULL, error, ebsql->priv->folderid);
+
+ if (success)
+ success = ebsql_exec_printf (
+ ebsql, "SELECT countrycode FROM folders WHERE folder_id = %Q",
+ get_string_cb, &stored_region_code, NULL, error, ebsql->priv->folderid);
+
+ lc_collate = stored_lc_collate;
+ }
+
+ /* When creating a new addressbook, or upgrading from a version
+ * where we did not have any locale setting; default to system locale,
+ * we must absolutely always have a locale set.
+ */
+ if (!lc_collate || !lc_collate[0])
+ lc_collate = setlocale (LC_COLLATE, NULL);
+ if (!lc_collate || !lc_collate[0])
+ lc_collate = setlocale (LC_ALL, NULL);
+ if (!lc_collate || !lc_collate[0])
+ lc_collate = "en_US.utf8";
+
+ /* Before touching any data, make sure we have a valid ECollator,
+ * this will also resolve our region code
+ */
+ if (success)
+ success = ebsql_set_locale_internal (ebsql, lc_collate, error);
+
+ /* Check if we need to relocalize */
+ if (success) {
+ /* Need to relocalize the whole thing if the schema has been upgraded to version 7 */
+ if (previous_schema >= 1 && previous_schema < 11)
+ relocalize_needed = TRUE;
+
+ /* We may need to relocalize for a country code change */
+ else if (g_strcmp0 (ebsql->priv->region_code, stored_region_code) != 0)
+ relocalize_needed = TRUE;
+ }
+
+ /* Reinsert all contacts with new locale & country code */
+ if (success && relocalize_needed)
+ success = ebsql_upgrade (ebsql, EBSQL_CHANGE_LAST, error);
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: Initialized locale as '%s' (%s)\n",
+ ebsql->priv->locale, success ? "success" : "failed"));
+
+ g_free (stored_region_code);
+ g_free (stored_lc_collate);
+
+ return success;
+}
+
+static EBookSqlite *
+ebsql_new_internal (const gchar *path,
+ ESource *source,
+ EbSqlVCardCallback vcard_callback,
+ EbSqlChangeCallback change_callback,
+ gpointer user_data,
+ GDestroyNotify user_data_destroy,
+ SummaryField *fields,
+ gint n_fields,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookSqlite *ebsql;
+ gchar *dirname = NULL;
+ gint previous_schema = 0;
+ gboolean already_exists = FALSE;
+ gboolean success = TRUE;
+ GSList *introspected_columns = NULL;
+
+ g_return_val_if_fail (path != NULL, NULL);
+
+ EBSQL_LOCK_MUTEX (&dbcon_lock);
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr ("SCHEMA: Creating new EBookSqlite at path '%s'\n", path));
+
+ ebsql = ebsql_ref_from_hash (path);
+ if (ebsql) {
+ EBSQL_NOTE (SCHEMA, g_printerr ("SCHEMA: An EBookSqlite already existed\n"));
+ goto exit;
+ }
+
+ ebsql = g_object_new (E_TYPE_BOOK_SQLITE, NULL);
+ ebsql->priv->path = g_strdup (path);
+ ebsql->priv->folderid = g_strdup (DEFAULT_FOLDER_ID);
+ ebsql->priv->summary_fields = fields;
+ ebsql->priv->n_summary_fields = n_fields;
+ ebsql->priv->vcard_callback = vcard_callback;
+ ebsql->priv->change_callback = change_callback;
+ ebsql->priv->user_data = user_data;
+ ebsql->priv->user_data_destroy = user_data_destroy;
+ if (source != NULL)
+ ebsql->priv->source = g_object_ref (source);
+ else
+ ebsql->priv->source = NULL;
+
+ EBSQL_NOTE (REF_COUNTS, g_printerr ("EBookSqlite initially created\n"));
+
+ /* Ensure existance of the directories leading up to 'path' */
+ dirname = g_path_get_dirname (path);
+ if (g_mkdir_with_parents (dirname, 0777) < 0) {
+ EBSQL_SET_ERROR (
+ error,
+ E_BOOK_SQLITE_ERROR_LOAD,
+ "Can not make parent directory: %s",
+ g_strerror (errno));
+ success = FALSE;
+ goto exit;
+ }
+
+ /* The additional instance lock is unneccesarry because of the global
+ * lock held here, but let's keep it locked because we hold it while
+ * executing any SQLite code throughout this code
+ */
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+
+ /* Initialize the SQLite (set some parameters and add some custom hooks) */
+ if (!ebsql_init_sqlite (ebsql, path, error)) {
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ success = FALSE;
+ goto exit;
+ }
+
+ /* Lets do it all atomically inside a single transaction */
+ if (!ebsql_start_transaction (ebsql, EBSQL_LOCK_WRITE, cancellable, error)) {
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ success = FALSE;
+ goto exit;
+ }
+
+ /* When loading addressbooks created by EBookBackendSqlite, we
+ * need to fetch the 'folderid' which was in use for that existing
+ * addressbook before introspecting it's summary and upgrading
+ * the schema.
+ */
+ if (success)
+ success = ebsql_resolve_folderid (
+ ebsql,
+ &previous_schema,
+ &already_exists,
+ error);
+
+ /* Initialize main folders table, also retrieve the current
+ * schema version if the table already exists
+ */
+ if (success)
+ success = ebsql_init_folders (ebsql, previous_schema, error);
+
+ /* Initialize the key/value table */
+ if (success)
+ success = ebsql_init_keys (ebsql, error);
+
+ /* Determine if the addressbook already existed, and fill out
+ * some information in the main folder table
+ */
+ if (success && !already_exists)
+ success = ebsql_add_folder (ebsql, error);
+
+ /* If the addressbook did exist, then check how it's configured.
+ *
+ * Let the existing summary information override the current
+ * one asked for by our callers.
+ *
+ * Some summary fields are also adjusted for schema upgrades
+ */
+ if (success && already_exists)
+ success = ebsql_introspect_summary (
+ ebsql,
+ previous_schema,
+ &introspected_columns,
+ error);
+
+ /* Add the contacts table, ensure the right columns are defined
+ * to handle our summary configuration
+ */
+ if (success)
+ success = ebsql_init_contacts (
+ ebsql,
+ introspected_columns,
+ error);
+
+ /* Add any auxiliary tables which we might need to support our
+ * summary configuration.
+ *
+ * Any fields which represent a 'list-of-strings' require an
+ * auxiliary table to store them in.
+ */
+ if (success)
+ success = ebsql_init_aux_tables (ebsql, previous_schema, error);
+
+ /* At this point we have resolved our schema, let's build our
+ * precompiled statements, we might use them to re-insert contacts
+ * in the next step
+ */
+ if (success)
+ success = ebsql_init_statements (ebsql, error);
+
+ /* When porting from older schemas, we need to port the old 'is-populated' flag */
+ if (success)
+ success = ebsql_init_legacy_keys (ebsql, previous_schema, error);
+
+ /* Load / resolve the current locale setting
+ *
+ * Also perform the overall upgrade in this step
+ * in the case that an upgrade happened, or a locale
+ * change is detected... all rows need to be renormalized
+ * for this.
+ */
+ if (success)
+ success = ebsql_init_locale (
+ ebsql, previous_schema,
+ already_exists, error);
+
+ if (success)
+ success = ebsql_commit_transaction (ebsql, error);
+ else
+ /* The GError is already set. */
+ ebsql_rollback_transaction (ebsql, NULL);
+
+ /* Release the instance lock and register to the global hash */
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ if (success)
+ ebsql_register_to_hash (ebsql, path);
+
+ exit:
+
+ /* Cleanup and exit */
+ EBSQL_UNLOCK_MUTEX (&dbcon_lock);
+
+ /* If we failed somewhere, give up on creating the 'ebsql',
+ * otherwise add it to the hash table
+ */
+ if (!success)
+ g_clear_object (&ebsql);
+
+ EBSQL_NOTE (
+ SCHEMA,
+ g_printerr (
+ "SCHEMA: %s the new EBookSqlite\n",
+ success ? "Successfully created" : "Failed to create"));
+
+ g_slist_free_full (introspected_columns, (GDestroyNotify) g_free);
+ g_free (dirname);
+
+ return ebsql;
+}
+
+/**********************************************************
+ * Inserting Contacts *
+ **********************************************************/
+static gchar *
+convert_phone (const gchar *normal,
+ const gchar *region_code,
+ gint *out_country_code)
+{
+ EPhoneNumber *number = NULL;
+ gchar *national_number = NULL;
+ gint country_code = 0;
+
+ /* Don't warn about erronous phone number strings, it's a perfectly normal
+ * use case for users to enter notes instead of phone numbers in the phone
+ * number contact fields, such as "Ask Jenny for Lisa's phone number"
+ */
+ if (normal && e_phone_number_is_supported ())
+ number = e_phone_number_from_string (normal, region_code, NULL);
+
+ if (number) {
+ EPhoneNumberCountrySource source;
+
+ national_number = e_phone_number_get_national_number (number);
+ country_code = e_phone_number_get_country_code (number, &source);
+ e_phone_number_free (number);
+
+ if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT)
+ country_code = 0;
+ }
+
+ if (out_country_code)
+ *out_country_code = country_code;
+
+ return national_number;
+}
+
+static gchar *
+remove_leading_zeros (gchar *number)
+{
+ gchar *trimmed = NULL;
+ gchar *tmp = number;
+
+ g_return_val_if_fail (NULL != number, NULL);
+
+ while ('0' == *tmp)
+ tmp++;
+ trimmed = g_strdup (tmp);
+ g_free (number);
+
+ return trimmed;
+}
+
+typedef struct {
+ gint country_code;
+ gchar *national;
+} E164Number;
+
+static E164Number *
+ebsql_e164_number_new (gint country_code,
+ gchar *national)
+{
+ E164Number *number = g_slice_new (E164Number);
+
+ number->country_code = country_code;
+ number->national = g_strdup (national);
+
+ return number;
+}
+
+static void
+ebsql_e164_number_free (E164Number *number)
+{
+ if (number) {
+ g_free (number->national);
+ g_slice_free (E164Number, number);
+ }
+}
+
+static gint
+ebsql_e164_number_find (E164Number *number_a,
+ E164Number *number_b)
+{
+ gint ret;
+
+ ret = number_a->country_code - number_b->country_code;
+
+ if (ret == 0)
+ ret = g_strcmp0 (
+ number_a->national,
+ number_b->national);
+
+ return ret;
+}
+
+static GList *
+extract_e164_attribute_params (EContact *contact)
+{
+ EVCard *vcard = E_VCARD (contact);
+ GList *extracted = NULL;
+ GList *attr_list;
+
+ for (attr_list = e_vcard_get_attributes (vcard); attr_list; attr_list = attr_list->next) {
+ EVCardAttribute *const attr = attr_list->data;
+ EVCardAttributeParam *param = NULL;
+ GList *param_list, *values, *l;
+ gchar *this_national = NULL;
+ gint this_country = 0;
+
+ /* We only attach E164 parameters to TEL attributes. */
+ if (strcmp (e_vcard_attribute_get_name (attr), EVC_TEL) != 0)
+ continue;
+
+ /* Find already exisiting parameter, so that we can reuse it. */
+ for (param_list = e_vcard_attribute_get_params (attr); param_list; param_list = param_list->next) {
+ if (strcmp (e_vcard_attribute_param_get_name (param_list->data), EVC_X_E164) == 0) {
+ param = param_list->data;
+ break;
+ }
+ }
+
+ if (!param)
+ continue;
+
+ values = e_vcard_attribute_param_get_values (param);
+ for (l = values; l; l = l->next) {
+ const gchar *value = l->data;
+
+ if (value[0] == '+')
+ this_country = g_ascii_strtoll (&value[1], NULL, 10);
+ else if (this_national == NULL)
+ this_national = g_strdup (value);
+ }
+
+ if (this_national) {
+ E164Number *number;
+
+ EBSQL_NOTE (
+ CONVERT_E164,
+ g_printerr (
+ "Extracted e164 number from '%s' with "
+ "country = %d national = %s\n",
+ (gchar *) e_contact_get_const (contact, E_CONTACT_UID),
+ this_country, this_national));
+
+ number = ebsql_e164_number_new (
+ this_country, this_national);
+ extracted = g_list_prepend (extracted, number);
+ }
+
+ g_free (this_national);
+
+ /* Clear the values, we'll insert new ones */
+ e_vcard_attribute_param_remove_values (param);
+ e_vcard_attribute_remove_param (attr, EVC_X_E164);
+ }
+
+ EBSQL_NOTE (
+ CONVERT_E164,
+ g_printerr (
+ "Extracted %d numbers from '%s'\n",
+ g_list_length (extracted),
+ (gchar *) e_contact_get_const (contact, E_CONTACT_UID)));
+
+ return extracted;
+}
+
+static gboolean
+update_e164_attribute_params (EBookSqlite *ebsql,
+ EContact *contact,
+ const gchar *default_region)
+{
+ GList *original_numbers = NULL;
+ GList *attr_list;
+ gboolean changed = FALSE;
+ gint n_numbers = 0;
+ EVCard *vcard = E_VCARD (contact);
+
+ original_numbers = extract_e164_attribute_params (contact);
+
+ for (attr_list = e_vcard_get_attributes (vcard); attr_list; attr_list = attr_list->next) {
+ EVCardAttribute *const attr = attr_list->data;
+ EVCardAttributeParam *param = NULL;
+ const gchar *original_number = NULL;
+ gchar *country_string;
+ GList *values;
+ E164Number number = { 0, NULL };
+
+ /* We only attach E164 parameters to TEL attributes. */
+ if (strcmp (e_vcard_attribute_get_name (attr), EVC_TEL) != 0)
+ continue;
+
+ /* Fetch the TEL value */
+ values = e_vcard_attribute_get_values (attr);
+
+ /* Compute E164 number based on the TEL value */
+ if (values && values->data) {
+ original_number = (const gchar *) values->data;
+ number.national = convert_phone (
+ original_number,
+ ebsql->priv->region_code,
+ &(number.country_code));
+ }
+
+ if (number.national == NULL)
+ continue;
+
+ /* Count how many we successfully parsed in this region code */
+ n_numbers++;
+
+ /* Check if we have a differing e164 number, if there is no match
+ * in the old existing values then the vcard changed
+ */
+ if (!g_list_find_custom (original_numbers, &number,
+ (GCompareFunc) ebsql_e164_number_find))
+ changed = TRUE;
+
+ if (number.country_code != 0)
+ country_string = g_strdup_printf ("+%d", number.country_code);
+ else
+ country_string = g_strdup ("");
+
+ param = e_vcard_attribute_param_new (EVC_X_E164);
+ e_vcard_attribute_add_param (attr, param);
+
+ /* Assign the parameter values. It seems odd that we revert
+ * the order of NN and CC, but at least EVCard's parser doesn't
+ * permit an empty first param value. Which of course could be
+ * fixed - in order to create a nice potential IOP problem with
+ ** other vCard parsers. */
+ e_vcard_attribute_param_add_values (param, number.national, country_string, NULL);
+
+ EBSQL_NOTE (
+ CONVERT_E164,
+ g_printerr (
+ "Converted '%s' to e164 number with country = %d "
+ "national = %s for '%s' (changed %s)\n",
+ original_number, number.country_code, number.national,
+ (gchar *) e_contact_get_const (contact, E_CONTACT_UID),
+ changed ? "yes" : "no"));
+
+ g_free (number.national);
+ g_free (country_string);
+ }
+
+ if (!changed &&
+ n_numbers != g_list_length (original_numbers))
+ changed = TRUE;
+
+ EBSQL_NOTE (
+ CONVERT_E164,
+ g_printerr (
+ "Converted %d e164 numbers for '%s' which previously had %d e164 numbers\n",
+ n_numbers,
+ (gchar *) e_contact_get_const (contact, E_CONTACT_UID),
+ g_list_length (original_numbers)));
+
+ g_list_free_full (original_numbers, (GDestroyNotify) ebsql_e164_number_free);
+
+ return changed;
+}
+
+static sqlite3_stmt *
+ebsql_prepare_multi_delete (EBookSqlite *ebsql,
+ SummaryField *field,
+ GError **error)
+{
+ sqlite3_stmt *stmt = NULL;
+ gchar *stmt_str;
+
+ stmt_str = sqlite3_mprintf ("DELETE FROM %Q WHERE uid = :uid", field->aux_table);
+ stmt = ebsql_prepare_statement (ebsql, stmt_str, error);
+ sqlite3_free (stmt_str);
+
+ return stmt;
+}
+
+static gboolean
+ebsql_run_multi_delete (EBookSqlite *ebsql,
+ SummaryField *field,
+ const gchar *uid,
+ GError **error)
+{
+ sqlite3_stmt *stmt;
+ gint ret;
+
+ stmt = g_hash_table_lookup (ebsql->priv->multi_deletes, GUINT_TO_POINTER (field->field_id));
+
+ /* This can return an error if a previous call to sqlite3_step() had errors,
+ * so let's just ignore any error in this case
+ */
+ sqlite3_reset (stmt);
+
+ /* Clear all previously set values */
+ ret = sqlite3_clear_bindings (stmt);
+
+ /* Set the UID host parameter statically */
+ if (ret == SQLITE_OK)
+ ret = sqlite3_bind_text (stmt, 1, uid, -1, SQLITE_STATIC);
+
+ /* Run the statement */
+ return ebsql_complete_statement (ebsql, stmt, ret, error);
+}
+
+static sqlite3_stmt *
+ebsql_prepare_multi_insert (EBookSqlite *ebsql,
+ SummaryField *field,
+ GError **error)
+{
+ sqlite3_stmt *stmt = NULL;
+ GString *string;
+
+ string = g_string_sized_new (INSERT_MULTI_STMT_BYTES);
+ ebsql_string_append_printf (string, "INSERT INTO %Q (uid, value", field->aux_table);
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0)
+ g_string_append (string, ", value_" EBSQL_SUFFIX_REVERSE);
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ g_string_append (string, ", value_" EBSQL_SUFFIX_PHONE);
+ g_string_append (string, ", value_" EBSQL_SUFFIX_COUNTRY);
+ }
+
+ g_string_append (string, ") VALUES (:uid, :value");
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0)
+ g_string_append (string, ", :value_" EBSQL_SUFFIX_REVERSE);
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ g_string_append (string, ", :value_" EBSQL_SUFFIX_PHONE);
+ g_string_append (string, ", :value_" EBSQL_SUFFIX_COUNTRY);
+ }
+
+ g_string_append_c (string, ')');
+
+ stmt = ebsql_prepare_statement (ebsql, string->str, error);
+ g_string_free (string, TRUE);
+
+ return stmt;
+}
+
+static gboolean
+ebsql_run_multi_insert_one (EBookSqlite *ebsql,
+ sqlite3_stmt *stmt,
+ SummaryField *field,
+ const gchar *uid,
+ const gchar *value,
+ GError **error)
+{
+ gchar *normal = e_util_utf8_normalize (value);
+ gchar *str;
+ gint ret, param_idx = 1;
+
+ /* :uid */
+ ret = sqlite3_bind_text (stmt, param_idx++, uid, -1, SQLITE_STATIC);
+
+ if (ret == SQLITE_OK) /* :value */
+ ret = sqlite3_bind_text (stmt, param_idx++, normal, -1, g_free);
+
+ if (ret == SQLITE_OK && (field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ if (normal)
+ str = g_utf8_strreverse (normal, -1);
+ else
+ str = NULL;
+
+ /* :value_reverse */
+ ret = sqlite3_bind_text (stmt, param_idx++, str, -1, g_free);
+ }
+
+ if (ret == SQLITE_OK && (field->index & INDEX_FLAG (PHONE)) != 0) {
+ gint country_code;
+
+ str = convert_phone (
+ normal, ebsql->priv->region_code,
+ &country_code);
+ str = remove_leading_zeros (str);
+
+ /* :value_phone */
+ ret = sqlite3_bind_text (stmt, param_idx++, str, -1, g_free);
+
+ /* :value_country */
+ if (ret == SQLITE_OK)
+ sqlite3_bind_int (stmt, param_idx++, country_code);
+
+ }
+
+ /* Run the statement */
+ return ebsql_complete_statement (ebsql, stmt, ret, error);
+}
+
+static gboolean
+ebsql_run_multi_insert (EBookSqlite *ebsql,
+ SummaryField *field,
+ const gchar *uid,
+ EContact *contact,
+ GError **error)
+{
+ sqlite3_stmt *stmt;
+ GList *values, *l;
+ gboolean success = TRUE;
+
+ stmt = g_hash_table_lookup (ebsql->priv->multi_inserts, GUINT_TO_POINTER (field->field_id));
+ values = e_contact_get (contact, field->field_id);
+
+ for (l = values; success && l != NULL; l = l->next) {
+ gchar *value = (gchar *) l->data;
+
+ success = ebsql_run_multi_insert_one (
+ ebsql, stmt, field, uid, value, error);
+ }
+
+ /* Free the list of allocated strings */
+ e_contact_attr_list_free (values);
+
+ return success;
+}
+
+static sqlite3_stmt *
+ebsql_prepare_insert (EBookSqlite *ebsql,
+ gboolean replace_existing,
+ GError **error)
+{
+ sqlite3_stmt *stmt;
+ GString *string;
+ gint i;
+
+ string = g_string_new ("");
+ if (replace_existing)
+ ebsql_string_append_printf (
+ string, "INSERT or REPLACE INTO %Q (",
+ ebsql->priv->folderid);
+ else
+ ebsql_string_append_printf (
+ string, "INSERT or FAIL INTO %Q (",
+ ebsql->priv->folderid);
+
+ /*
+ * First specify the column names for the insert, since it's possible we
+ * upgraded the DB and cannot be sure the order of the columns are ordered
+ * just how we like them to be.
+ */
+ for (i = 0; i < ebsql->priv->n_summary_fields; i++) {
+ SummaryField *field = &(ebsql->priv->summary_fields[i]);
+
+ /* Multi values go into a separate table/statement */
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST) {
+
+ /* Only add a ", " before every field except the first,
+ * this will not break because the first 2 fields (UID & REV)
+ * are string fields.
+ */
+ if (i > 0)
+ g_string_append (string, ", ");
+
+ g_string_append (string, field->dbname);
+ }
+
+ if (field->type == G_TYPE_STRING) {
+
+ if ((field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ g_string_append (string, ", ");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBSQL_SUFFIX_SORT_KEY);
+ }
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ g_string_append (string, ", ");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBSQL_SUFFIX_REVERSE);
+ }
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+
+ g_string_append (string, ", ");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBSQL_SUFFIX_PHONE);
+
+ g_string_append (string, ", ");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBSQL_SUFFIX_COUNTRY);
+ }
+ }
+ }
+ g_string_append (string, ", vcard, bdata)");
+
+ /*
+ * Now specify values for all of the column names we specified.
+ */
+ g_string_append (string, " VALUES (");
+ for (i = 0; i < ebsql->priv->n_summary_fields; i++) {
+ SummaryField *field = &(ebsql->priv->summary_fields[i]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST) {
+ /* Only add a ", " before every field except the first,
+ * this will not break because the first 2 fields (UID & REV)
+ * are string fields.
+ */
+ if (i > 0)
+ g_string_append (string, ", ");
+ }
+
+ if (field->type == G_TYPE_STRING || field->type == G_TYPE_BOOLEAN ||
+ field->type == E_TYPE_CONTACT_CERT) {
+
+ g_string_append_c (string, ':');
+ g_string_append (string, field->dbname);
+
+ if ((field->index & INDEX_FLAG (SORT_KEY)) != 0)
+ g_string_append_printf (string, ", :%s_" EBSQL_SUFFIX_SORT_KEY, field->dbname);
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0)
+ g_string_append_printf (string, ", :%s_" EBSQL_SUFFIX_REVERSE, field->dbname);
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ g_string_append_printf (string, ", :%s_" EBSQL_SUFFIX_PHONE, field->dbname);
+ g_string_append_printf (string, ", :%s_" EBSQL_SUFFIX_COUNTRY, field->dbname);
+ }
+
+ } else if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ g_warn_if_reached ();
+ }
+
+ g_string_append (string, ", :vcard, :bdata)");
+
+ stmt = ebsql_prepare_statement (ebsql, string->str, error);
+ g_string_free (string, TRUE);
+
+ return stmt;
+}
+
+static gboolean
+ebsql_init_statements (EBookSqlite *ebsql,
+ GError **error)
+{
+ sqlite3_stmt *stmt;
+ gint i;
+
+ ebsql->priv->insert_stmt = ebsql_prepare_insert (ebsql, FALSE, error);
+ if (!ebsql->priv->insert_stmt)
+ goto preparation_failed;
+
+ ebsql->priv->replace_stmt = ebsql_prepare_insert (ebsql, TRUE, error);
+ if (!ebsql->priv->replace_stmt)
+ goto preparation_failed;
+
+ ebsql->priv->multi_deletes =
+ g_hash_table_new_full (
+ g_direct_hash, g_direct_equal,
+ NULL,
+ (GDestroyNotify) sqlite3_finalize);
+ ebsql->priv->multi_inserts =
+ g_hash_table_new_full (
+ g_direct_hash, g_direct_equal,
+ NULL,
+ (GDestroyNotify) sqlite3_finalize);
+
+ for (i = 0; i < ebsql->priv->n_summary_fields; i++) {
+ SummaryField *field = &(ebsql->priv->summary_fields[i]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ stmt = ebsql_prepare_multi_insert (ebsql, field, error);
+ if (!stmt)
+ goto preparation_failed;
+
+ g_hash_table_insert (
+ ebsql->priv->multi_inserts,
+ GUINT_TO_POINTER (field->field_id),
+ stmt);
+
+ stmt = ebsql_prepare_multi_delete (ebsql, field, error);
+ if (!stmt)
+ goto preparation_failed;
+
+ g_hash_table_insert (
+ ebsql->priv->multi_deletes,
+ GUINT_TO_POINTER (field->field_id),
+ stmt);
+ }
+
+ return TRUE;
+
+ preparation_failed:
+
+ return FALSE;
+}
+
+static gboolean
+ebsql_run_insert (EBookSqlite *ebsql,
+ gboolean replace,
+ EContact *contact,
+ gchar *vcard,
+ const gchar *extra,
+ GError **error)
+{
+ EBookSqlitePrivate *priv;
+ sqlite3_stmt *stmt;
+ gint i, param_idx;
+ gint ret;
+ gboolean success;
+ GError *local_error = NULL;
+
+ priv = ebsql->priv;
+
+ if (replace)
+ stmt = ebsql->priv->replace_stmt;
+ else
+ stmt = ebsql->priv->insert_stmt;
+
+ /* This can return an error if a previous call to sqlite3_step() had errors,
+ * so let's just ignore any error in this case
+ */
+ sqlite3_reset (stmt);
+
+ /* Clear all previously set values */
+ ret = sqlite3_clear_bindings (stmt);
+
+ for (i = 0, param_idx = 1; ret == SQLITE_OK && i < ebsql->priv->n_summary_fields; i++) {
+ SummaryField *field = &(ebsql->priv->summary_fields[i]);
+
+ if (field->type == G_TYPE_STRING) {
+ gchar *val;
+ gchar *normal;
+ gchar *str;
+
+ val = e_contact_get (contact, field->field_id);
+
+ /* Special exception, never normalize/localize the UID or REV string */
+ if (field->field_id != E_CONTACT_UID &&
+ field->field_id != E_CONTACT_REV) {
+ normal = e_util_utf8_normalize (val);
+ } else
+ normal = g_strdup (val);
+
+ /* Takes ownership of 'normal' */
+ ret = sqlite3_bind_text (stmt, param_idx++, normal, -1, g_free);
+
+ if (ret == SQLITE_OK &&
+ (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ if (val)
+ str = e_collator_generate_key (ebsql->priv->collator, val, NULL);
+ else
+ str = g_strdup ("");
+
+ ret = sqlite3_bind_text (stmt, param_idx++, str, -1, g_free);
+ }
+
+ if (ret == SQLITE_OK &&
+ (field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ if (normal)
+ str = g_utf8_strreverse (normal, -1);
+ else
+ str = NULL;
+
+ ret = sqlite3_bind_text (stmt, param_idx++, str, -1, g_free);
+ }
+
+ if (ret == SQLITE_OK &&
+ (field->index & INDEX_FLAG (PHONE)) != 0) {
+ gint country_code;
+
+ str = convert_phone (
+ normal, ebsql->priv->region_code,
+ &country_code);
+ str = remove_leading_zeros (str);
+
+ ret = sqlite3_bind_text (stmt, param_idx++, str, -1, g_free);
+ if (ret == SQLITE_OK)
+ sqlite3_bind_int (stmt, param_idx++, country_code);
+ }
+
+ g_free (val);
+ } else if (field->type == G_TYPE_BOOLEAN) {
+ gboolean val;
+
+ val = e_contact_get (contact, field->field_id) ? TRUE : FALSE;
+
+ ret = sqlite3_bind_int (stmt, param_idx++, val ? 1 : 0);
+ } else if (field->type == E_TYPE_CONTACT_CERT) {
+ EContactCert *cert = NULL;
+
+ cert = e_contact_get (contact, field->field_id);
+
+ /* We don't actually store the cert; only a boolean to indicate
+ * that is *has* a cert. */
+ ret = sqlite3_bind_int (stmt, param_idx++, cert ? 1 : 0);
+ e_contact_cert_free (cert);
+ } else if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ g_warn_if_reached ();
+ }
+
+ if (ret == SQLITE_OK) {
+
+ EBSQL_NOTE (
+ INSERT,
+ g_printerr (
+ "Inserting vcard for contact with UID '%s'\n%s\n",
+ (gchar *) e_contact_get_const (contact, E_CONTACT_UID),
+ vcard ? vcard : "(no vcard)"));
+
+ /* If we have a priv->vcard_callback, then it's a shallow addressbook
+ * and we don't populate the vcard column, need to free it anyway
+ */
+ if (priv->vcard_callback != NULL) {
+ g_free (vcard);
+ vcard = NULL;
+ }
+
+ ret = sqlite3_bind_text (stmt, param_idx++, vcard, -1, g_free);
+ }
+
+ /* The extra data */
+ if (ret == SQLITE_OK)
+ ret = sqlite3_bind_text (stmt, param_idx++, g_strdup (extra), -1, g_free);
+
+ /* Run the statement */
+ success = ebsql_complete_statement (ebsql, stmt, ret, &local_error);
+
+ EBSQL_NOTE (
+ INSERT,
+ g_printerr (
+ "%s contact with UID '%s' and extra data '%s' vcard: %s (error: %s)\n",
+ success ? "Succesfully inserted" : "Failed to insert",
+ (gchar *) e_contact_get_const (contact, E_CONTACT_UID), extra,
+ vcard ? "yes" : "no",
+ local_error ? local_error->message : "(none)"));
+
+ if (!success)
+ g_propagate_error (error, local_error);
+
+ return success;
+}
+
+static gboolean
+ebsql_insert_contact (EBookSqlite *ebsql,
+ EbSqlChangeType change_type,
+ EContact *contact,
+ const gchar *original_vcard,
+ const gchar *extra,
+ gboolean replace,
+ GError **error)
+{
+ EBookSqlitePrivate *priv;
+ gboolean e164_changed = FALSE;
+ gboolean success;
+ gchar *uid, *vcard = NULL;
+
+ priv = ebsql->priv;
+ uid = e_contact_get (contact, E_CONTACT_UID);
+
+ /* Update E.164 parameters in vcard if needed */
+ e164_changed = update_e164_attribute_params (
+ ebsql, contact, priv->region_code);
+
+ if (e164_changed || original_vcard == NULL) {
+
+ /* Generate a new one if it changed (or if we don't have one) */
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+ if (e164_changed &&
+ change_type != EBSQL_CHANGE_LAST &&
+ ebsql->priv->change_callback)
+ ebsql->priv->change_callback (change_type,
+ uid, extra, vcard,
+ ebsql->priv->user_data);
+ } else {
+
+ vcard = g_strdup (original_vcard);
+ }
+
+ /* This actually consumes 'vcard' */
+ success = ebsql_run_insert (ebsql, replace, contact, vcard, extra, error);
+
+ /* Update attribute list table */
+ if (success) {
+ gint i;
+
+ for (i = 0; success && i < priv->n_summary_fields; i++) {
+ SummaryField *field = &(ebsql->priv->summary_fields[i]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ success = ebsql_run_multi_delete (
+ ebsql, field, uid, error);
+
+ if (success)
+ success = ebsql_run_multi_insert (
+ ebsql, field, uid, contact, error);
+ }
+ }
+
+ g_free (uid);
+
+ return success;
+}
+
+/***************************************************************
+ * Structures and utilities for preflight and query generation *
+ ***************************************************************/
+
+/* This enumeration is ordered by severity, higher values
+ * of PreflightStatus take precedence in error reporting.
+ */
+typedef enum {
+ PREFLIGHT_OK = 0,
+ PREFLIGHT_LIST_ALL,
+ PREFLIGHT_NOT_SUMMARIZED,
+ PREFLIGHT_INVALID,
+ PREFLIGHT_UNSUPPORTED,
+} PreflightStatus;
+
+#define EBSQL_STATUS_STR(status) \
+ ((status) == PREFLIGHT_OK ? "Ok" : \
+ (status) == PREFLIGHT_LIST_ALL ? "List all" : \
+ (status) == PREFLIGHT_NOT_SUMMARIZED ? "Not Summarized" : \
+ (status) == PREFLIGHT_INVALID ? "Invalid" : \
+ (status) == PREFLIGHT_UNSUPPORTED ? "Unsupported" : "(unknown status)")
+
+/* Whether we can satisfy the constraints or whether we
+ * need to do a fallback, we still need to call
+ * ebsql_generate_constraints()
+ */
+#define EBSQL_STATUS_GEN_CONSTRAINTS(status) \
+ ((status) == PREFLIGHT_OK || \
+ (status) == PREFLIGHT_NOT_SUMMARIZED)
+
+/* Internal extension of the EBookQueryTest enumeration */
+enum {
+ /* 'exists' is a supported query on a field, but not part of EBookQueryTest */
+ BOOK_QUERY_EXISTS = E_BOOK_QUERY_LAST,
+ BOOK_QUERY_EXISTS_VCARD,
+
+ /* From here the compound types start */
+ BOOK_QUERY_SUB_AND,
+ BOOK_QUERY_SUB_OR,
+ BOOK_QUERY_SUB_NOT,
+ BOOK_QUERY_SUB_END,
+
+ BOOK_QUERY_SUB_FIRST = BOOK_QUERY_SUB_AND,
+};
+
+#define EBSQL_QUERY_TYPE_STR(query) \
+ ((query) == BOOK_QUERY_EXISTS ? "exists" : \
+ (query) == BOOK_QUERY_EXISTS_VCARD ? "exists_vcard" : \
+ (query) == BOOK_QUERY_SUB_AND ? "AND" : \
+ (query) == BOOK_QUERY_SUB_OR ? "OR" : \
+ (query) == BOOK_QUERY_SUB_NOT ? "NOT" : \
+ (query) == BOOK_QUERY_SUB_END ? "END" : \
+ (query) == E_BOOK_QUERY_IS ? "is" : \
+ (query) == E_BOOK_QUERY_CONTAINS ? "contains" : \
+ (query) == E_BOOK_QUERY_BEGINS_WITH ? "begins-with" : \
+ (query) == E_BOOK_QUERY_ENDS_WITH ? "ends-with" : \
+ (query) == E_BOOK_QUERY_EQUALS_PHONE_NUMBER ? "eqphone" : \
+ (query) == E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER ? "eqphone-national" : \
+ (query) == E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER ? "eqphone-short" : \
+ (query) == E_BOOK_QUERY_REGEX_NORMAL ? "regex-normal" : \
+ (query) == E_BOOK_QUERY_REGEX_NORMAL ? "regex-raw" : "(unknown)")
+
+#define EBSQL_FIELD_ID_STR(field_id) \
+ ((field_id) == E_CONTACT_FIELD_LAST ? "x-evolution-any-field" : \
+ (field_id) == 0 ? "(not an EContactField)" : \
+ e_contact_field_name (field_id))
+
+#define IS_QUERY_PHONE(query) \
+ ((query) == E_BOOK_QUERY_EQUALS_PHONE_NUMBER || \
+ (query) == E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER || \
+ (query) == E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER)
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+} QueryElement;
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+} QueryDelimiter;
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+
+ EContactField field_id; /* The EContactField to compare */
+ SummaryField *field; /* The summary field for 'field' */
+ gchar *value; /* The value to compare with */
+
+} QueryFieldTest;
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+
+ /* Common fields from QueryFieldTest */
+ EContactField field_id; /* The EContactField to compare */
+ SummaryField *field; /* The summary field for 'field' */
+ gchar *value; /* The value to compare with */
+
+ /* Extension */
+ gchar *region; /* Region code from the query input */
+ gchar *national; /* Parsed national number */
+ gint country; /* Parsed country code */
+} QueryPhoneTest;
+
+/* Stack initializer for the PreflightContext struct below */
+#define PREFLIGHT_CONTEXT_INIT { PREFLIGHT_OK, NULL, 0, FALSE }
+
+typedef struct {
+ PreflightStatus status; /* result status */
+ GPtrArray *constraints; /* main query; may be NULL */
+ guint64 aux_mask; /* Bitmask of which auxiliary tables are needed in the query */
+ guint64 left_join_mask; /* Do we need to use a LEFT JOIN */
+} PreflightContext;
+
+static QueryElement *
+query_delimiter_new (guint query)
+{
+ QueryDelimiter *delim;
+
+ g_return_val_if_fail (query >= BOOK_QUERY_SUB_FIRST, NULL);
+
+ delim = g_slice_new (QueryDelimiter);
+ delim->query = query;
+
+ return (QueryElement *) delim;
+}
+
+static QueryFieldTest *
+query_field_test_new (guint query,
+ EContactField field)
+{
+ QueryFieldTest *test;
+
+ g_return_val_if_fail (query < BOOK_QUERY_SUB_FIRST, NULL);
+ g_return_val_if_fail (IS_QUERY_PHONE (query) == FALSE, NULL);
+
+ test = g_slice_new (QueryFieldTest);
+ test->query = query;
+ test->field_id = field;
+
+ /* Instead of g_slice_new0, NULL them out manually */
+ test->field = NULL;
+ test->value = NULL;
+
+ return test;
+}
+
+static QueryPhoneTest *
+query_phone_test_new (guint query,
+ EContactField field)
+{
+ QueryPhoneTest *test;
+
+ g_return_val_if_fail (IS_QUERY_PHONE (query), NULL);
+
+ test = g_slice_new (QueryPhoneTest);
+ test->query = query;
+ test->field_id = field;
+
+ /* Instead of g_slice_new0, NULL them out manually */
+ test->field = NULL;
+ test->value = NULL;
+
+ /* Extra QueryPhoneTest fields */
+ test->region = NULL;
+ test->national = NULL;
+ test->country = 0;
+
+ return test;
+}
+
+static void
+query_element_free (QueryElement *element)
+{
+ if (element) {
+
+ if (element->query >= BOOK_QUERY_SUB_FIRST) {
+ QueryDelimiter *delim = (QueryDelimiter *) element;
+
+ g_slice_free (QueryDelimiter, delim);
+ } else if (IS_QUERY_PHONE (element->query)) {
+ QueryPhoneTest *test = (QueryPhoneTest *) element;
+
+ g_free (test->value);
+ g_free (test->region);
+ g_free (test->national);
+ g_slice_free (QueryPhoneTest, test);
+ } else {
+ QueryFieldTest *test = (QueryFieldTest *) element;
+
+ g_free (test->value);
+ g_slice_free (QueryFieldTest, test);
+ }
+ }
+}
+
+/* We use ptr arrays for the QueryElement vectors */
+static inline void
+constraints_insert (GPtrArray *array,
+ gint idx,
+ gpointer data)
+{
+#if 0
+ g_ptr_array_insert (array, idx, data);
+#else
+ g_return_if_fail ((idx >= -1) && (idx < (gint) array->len + 1));
+
+ if (idx < 0)
+ idx = array->len;
+
+ g_ptr_array_add (array, NULL);
+
+ if (idx != (array->len - 1))
+ memmove (
+ &(array->pdata[idx + 1]),
+ &(array->pdata[idx]),
+ ((array->len - 1) - idx) * sizeof (gpointer));
+
+ array->pdata[idx] = data;
+#endif
+}
+
+static inline void
+constraints_insert_delimiter (GPtrArray *array,
+ gint idx,
+ guint query)
+{
+ QueryElement *delim;
+
+ delim = query_delimiter_new (query);
+ constraints_insert (array, idx, delim);
+}
+
+static inline void
+constraints_insert_field_test (GPtrArray *array,
+ gint idx,
+ SummaryField *field,
+ guint query,
+ const gchar *value)
+{
+ QueryFieldTest *test;
+
+ test = query_field_test_new (query, field->field_id);
+ test->field = field;
+ test->value = g_strdup (value);
+
+ constraints_insert (array, idx, test);
+}
+
+static void
+preflight_context_clear (PreflightContext *context)
+{
+ if (context) {
+ /* Free any allocated data, but leave the context values in place */
+ if (context->constraints)
+ g_ptr_array_free (context->constraints, TRUE);
+ context->constraints = NULL;
+ }
+}
+
+/* A small API to track the current sub-query context.
+ *
+ * I.e. sub contexts can be OR, AND, or NOT, in which
+ * field tests or other sub contexts are nested.
+ *
+ * The 'count' field is a simple counter of how deep the contexts are nested.
+ *
+ * The 'cond_count' field is to be used by the caller for its own purposes;
+ * it is incremented in sub_query_context_push() only if the inc_cond_count
+ * parameter is TRUE. This is used by query_preflight_check() in a complex
+ * fashion which is described there.
+ */
+typedef GQueue SubQueryContext;
+
+typedef struct {
+ guint sub_type; /* The type of this sub context */
+ guint count; /* The number of field tests so far in this context */
+ guint cond_count; /* User-specific conditional counter */
+} SubQueryData;
+
+#define sub_query_context_new g_queue_new
+#define sub_query_context_free(ctx) g_queue_free (ctx)
+
+static inline void
+sub_query_context_push (SubQueryContext *ctx,
+ guint sub_type, gboolean inc_cond_count)
+{
+ SubQueryData *data, *prev;
+
+ prev = g_queue_peek_tail (ctx);
+
+ data = g_slice_new (SubQueryData);
+ data->sub_type = sub_type;
+ data->count = 0;
+ data->cond_count = prev ? prev->cond_count : 0;
+ if (inc_cond_count)
+ data->cond_count++;
+
+ g_queue_push_tail (ctx, data);
+}
+
+static inline void
+sub_query_context_pop (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_pop_tail (ctx);
+ g_slice_free (SubQueryData, data);
+}
+
+static inline guint
+sub_query_context_peek_type (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_peek_tail (ctx);
+
+ return data->sub_type;
+}
+
+static inline guint
+sub_query_context_peek_cond_counter (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_peek_tail (ctx);
+
+ if (data)
+ return data->cond_count;
+ else
+ return 0;
+}
+
+/* Returns the context field test count before incrementing */
+static inline guint
+sub_query_context_increment (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_peek_tail (ctx);
+
+ if (data) {
+ data->count++;
+
+ return (data->count - 1);
+ }
+
+ /* If we're not in a sub context, just return 0 */
+ return 0;
+}
+
+/**********************************************************
+ * Querying preflighting *
+ **********************************************************
+ *
+ * The preflight checks are performed before a query might
+ * take place in order to evaluate whether the given query
+ * can be performed with the current summary configuration.
+ *
+ * After preflighting, all relevant data has been extracted
+ * from the search expression and the search expression need
+ * not be parsed again.
+ */
+
+/* The PreflightSubCallback is expected to return TRUE
+ * to keep iterating and FALSE to abort iteration.
+ *
+ * The sub_level is the counter of how deep the 'element'
+ * is nested in sub elements, the offset is the real offset
+ * of 'element' in the array passed to query_preflight_foreach_sub().
+ */
+typedef gboolean (* PreflightSubCallback) (QueryElement *element,
+ gint sub_level,
+ gint offset,
+ gpointer user_data);
+
+static void
+query_preflight_foreach_sub (QueryElement **elements,
+ gint n_elements,
+ gint offset,
+ gboolean include_delim,
+ PreflightSubCallback callback,
+ gpointer user_data)
+{
+ gint sub_counter = 1, i;
+
+ g_return_if_fail (offset >= 0 && offset < n_elements);
+ g_return_if_fail (elements[offset]->query >= BOOK_QUERY_SUB_FIRST);
+ g_return_if_fail (callback != NULL);
+
+ if (include_delim && !callback (elements[offset], 0, offset, user_data))
+ return;
+
+ for (i = (offset + 1); sub_counter > 0 && i < n_elements; i++) {
+
+ if (elements[i]->query >= BOOK_QUERY_SUB_FIRST) {
+
+ if (elements[i]->query == BOOK_QUERY_SUB_END)
+ sub_counter--;
+ else
+ sub_counter++;
+
+ if (include_delim &&
+ !callback (elements[i], sub_counter, i, user_data))
+ break;
+ } else {
+
+ if (!callback (elements[i], sub_counter, i, user_data))
+ break;
+ }
+ }
+}
+
+/* Table used in ESExp parsing below */
+static const struct {
+ const gchar *name; /* Name of the symbol to match for this parse phase */
+ gboolean subset; /* TRUE for the subset ESExpIFunc, otherwise the field check ESExpFunc */
+ guint test; /* Extended EBookQueryTest value */
+} check_symbols[] = {
+ { "and", TRUE, BOOK_QUERY_SUB_AND },
+ { "or", TRUE, BOOK_QUERY_SUB_OR },
+ { "not", TRUE, BOOK_QUERY_SUB_NOT },
+
+ { "contains", FALSE, E_BOOK_QUERY_CONTAINS },
+ { "is", FALSE, E_BOOK_QUERY_IS },
+ { "beginswith", FALSE, E_BOOK_QUERY_BEGINS_WITH },
+ { "endswith", FALSE, E_BOOK_QUERY_ENDS_WITH },
+ { "eqphone", FALSE, E_BOOK_QUERY_EQUALS_PHONE_NUMBER },
+ { "eqphone_national", FALSE, E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER },
+ { "eqphone_short", FALSE, E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER },
+ { "regex_normal", FALSE, E_BOOK_QUERY_REGEX_NORMAL },
+ { "regex_raw", FALSE, E_BOOK_QUERY_REGEX_RAW },
+ { "exists", FALSE, BOOK_QUERY_EXISTS },
+ { "exists_vcard", FALSE, BOOK_QUERY_EXISTS_VCARD }
+};
+
+/* Cheat our way into passing mode data to these funcs */
+static ESExpResult *
+func_check_subset (ESExp *f,
+ gint argc,
+ struct _ESExpTerm **argv,
+ gpointer data)
+{
+ ESExpResult *result, *sub_result;
+ GPtrArray *result_array;
+ QueryElement *element, **sub_elements;
+ gint i, j, len;
+ guint query_type;
+
+ query_type = GPOINTER_TO_UINT (data);
+
+ /* The compound query delimiter is the first element in this return array */
+ result_array = g_ptr_array_new_with_free_func ((GDestroyNotify) query_element_free);
+ element = query_delimiter_new (query_type);
+ g_ptr_array_add (result_array, element);
+
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT INIT: Open sub: %s\n",
+ EBSQL_QUERY_TYPE_STR (query_type)));
+
+ for (i = 0; i < argc; i++) {
+ sub_result = e_sexp_term_eval (f, argv[i]);
+
+ if (sub_result->type == ESEXP_RES_ARRAY_PTR) {
+ /* Steal the elements directly from the sub result */
+ sub_elements = (QueryElement **) sub_result->value.ptrarray->pdata;
+ len = sub_result->value.ptrarray->len;
+
+ for (j = 0; j < len; j++) {
+ element = sub_elements[j];
+ sub_elements[j] = NULL;
+
+ g_ptr_array_add (result_array, element);
+ }
+ }
+ e_sexp_result_free (f, sub_result);
+ }
+
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT INIT: Close sub: %s\n",
+ EBSQL_QUERY_TYPE_STR (query_type)));
+
+ /* The last element in this return array is the sub end delimiter */
+ element = query_delimiter_new (BOOK_QUERY_SUB_END);
+ g_ptr_array_add (result_array, element);
+
+ result = e_sexp_result_new (f, ESEXP_RES_ARRAY_PTR);
+ result->value.ptrarray = result_array;
+
+ return result;
+}
+
+static ESExpResult *
+func_check (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ ESExpResult *result;
+ GPtrArray *result_array;
+ QueryElement *element = NULL;
+ EContactField field_id = 0;
+ const gchar *query_name = NULL;
+ const gchar *query_value = NULL;
+ const gchar *query_extra = NULL;
+ guint query_type;
+
+ query_type = GPOINTER_TO_UINT (data);
+
+ if (argc == 1 && query_type == BOOK_QUERY_EXISTS &&
+ argv[0]->type == ESEXP_RES_STRING) {
+ query_name = argv[0]->value.string;
+
+ field_id = e_contact_field_id (query_name);
+ } else if (argc == 2 &&
+ argv[0]->type == ESEXP_RES_STRING &&
+ argv[1]->type == ESEXP_RES_STRING) {
+ query_name = argv[0]->value.string;
+ query_value = argv[1]->value.string;
+
+ /* We use E_CONTACT_FIELD_LAST to hold the special case of "x-evolution-any-field" */
+ if (g_strcmp0 (query_name, "x-evolution-any-field") == 0)
+ field_id = E_CONTACT_FIELD_LAST;
+ else
+ field_id = e_contact_field_id (query_name);
+
+ } else if (argc == 3 &&
+ argv[0]->type == ESEXP_RES_STRING &&
+ argv[1]->type == ESEXP_RES_STRING &&
+ argv[2]->type == ESEXP_RES_STRING) {
+ query_name = argv[0]->value.string;
+ query_value = argv[1]->value.string;
+ query_extra = argv[2]->value.string;
+
+ field_id = e_contact_field_id (query_name);
+ }
+
+ if (IS_QUERY_PHONE (query_type)) {
+ QueryPhoneTest *test;
+
+ /* Collect data from this field test */
+ test = query_phone_test_new (query_type, field_id);
+ test->value = g_strdup (query_value);
+ test->region = g_strdup (query_extra);
+
+ element = (QueryElement *) test;
+ } else {
+ QueryFieldTest *test;
+
+ /* Collect data from this field test */
+ test = query_field_test_new (query_type, field_id);
+ test->value = g_strdup (query_value);
+
+ element = (QueryElement *) test;
+ }
+
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT INIT: Adding field test: `%s' on field `%s' "
+ "(field name: %s query value: %s query extra: %s)\n",
+ EBSQL_QUERY_TYPE_STR (query_type),
+ EBSQL_FIELD_ID_STR (field_id),
+ query_name, query_value, query_extra));
+
+ /* Return an array with only one element, for lack of a pointer type ESExpResult */
+ result_array = g_ptr_array_new_with_free_func ((GDestroyNotify) query_element_free);
+ g_ptr_array_add (result_array, element);
+
+ result = e_sexp_result_new (f, ESEXP_RES_ARRAY_PTR);
+ result->value.ptrarray = result_array;
+
+ return result;
+}
+
+/* Initial stage of preflighting:
+ *
+ * o Parse the search expression and generate our array of QueryElements
+ * o Collect lengths of query terms
+ */
+static void
+query_preflight_initialize (PreflightContext *context,
+ const gchar *sexp)
+{
+ ESExp *sexp_parser;
+ ESExpResult *result;
+ gint esexp_error, i;
+
+ if (sexp == NULL || *sexp == '\0') {
+ context->status = PREFLIGHT_LIST_ALL;
+ return;
+ }
+
+ sexp_parser = e_sexp_new ();
+
+ for (i = 0; i < G_N_ELEMENTS (check_symbols); i++) {
+ if (check_symbols[i].subset) {
+ e_sexp_add_ifunction (
+ sexp_parser, 0, check_symbols[i].name,
+ func_check_subset,
+ GUINT_TO_POINTER (check_symbols[i].test));
+ } else {
+ e_sexp_add_function (
+ sexp_parser, 0, check_symbols[i].name,
+ func_check,
+ GUINT_TO_POINTER (check_symbols[i].test));
+ }
+ }
+
+ e_sexp_input_text (sexp_parser, sexp, strlen (sexp));
+ esexp_error = e_sexp_parse (sexp_parser);
+
+ if (esexp_error == -1) {
+ context->status = PREFLIGHT_INVALID;
+
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr ("PREFLIGHT INIT: Sexp parse error\n"));
+ } else {
+
+ result = e_sexp_eval (sexp_parser);
+ if (result) {
+
+ if (result->type == ESEXP_RES_ARRAY_PTR) {
+
+ /* Just steal the array away from the ESexpResult */
+ context->constraints = result->value.ptrarray;
+ result->value.ptrarray = NULL;
+
+ } else {
+ context->status = PREFLIGHT_INVALID;
+
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr ("PREFLIGHT INIT: ERROR, Did not get GPtrArray\n"));
+ }
+ }
+
+ e_sexp_result_free (sexp_parser, result);
+ }
+
+ g_object_unref (sexp_parser);
+
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT INIT: Completed with status %s\n",
+ EBSQL_STATUS_STR (context->status)));
+}
+
+typedef struct {
+ EBookSqlite *ebsql;
+ SummaryField *field;
+ gboolean condition;
+} AttrListCheckData;
+
+static gboolean
+check_has_attr_list_cb (QueryElement *element,
+ gint sub_level,
+ gint offset,
+ gpointer user_data)
+{
+ QueryFieldTest *test = (QueryFieldTest *) element;
+ AttrListCheckData *data = (AttrListCheckData *) user_data;
+
+ /* We havent resolved all the fields at this stage yet */
+ if (!test->field)
+ test->field = summary_field_get (data->ebsql, test->field_id);
+
+ if (test->field && test->field->type == E_TYPE_CONTACT_ATTR_LIST)
+ data->condition = TRUE;
+
+ /* Keep looping until we find one */
+ return (data->condition == FALSE);
+}
+
+static gboolean
+check_different_fields_cb (QueryElement *element,
+ gint sub_level,
+ gint offset,
+ gpointer user_data)
+{
+ QueryFieldTest *test = (QueryFieldTest *) element;
+ AttrListCheckData *data = (AttrListCheckData *) user_data;
+
+ /* We havent resolved all the fields at this stage yet */
+ if (!test->field)
+ test->field = summary_field_get (data->ebsql, test->field_id);
+
+ if (test->field && data->field && test->field != data->field)
+ data->condition = TRUE;
+ else
+ data->field = test->field;
+
+ /* Keep looping until we find one */
+ return (data->condition == FALSE);
+}
+
+/* What is done in this pass:
+ * o Viability of the query is analyzed, i.e. can it be done with the summary columns.
+ * o Phone numbers are parsed and loaded onto QueryPhoneTests
+ * o Bitmask of auxiliary tables is collected
+ */
+static void
+query_preflight_check (PreflightContext *context,
+ EBookSqlite *ebsql)
+{
+ gint i, n_elements;
+ QueryElement **elements;
+ SubQueryContext *ctx;
+
+ context->status = PREFLIGHT_OK;
+
+ if (context->constraints != NULL) {
+ elements = (QueryElement **) context->constraints->pdata;
+ n_elements = context->constraints->len;
+ } else {
+ elements = NULL;
+ n_elements = 0;
+ }
+
+ ctx = sub_query_context_new ();
+
+ for (i = 0; i < n_elements; i++) {
+ QueryFieldTest *test;
+ guint field_test;
+
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: Encountered: %s\n",
+ EBSQL_QUERY_TYPE_STR (elements[i]->query)));
+
+ if (elements[i]->query >= BOOK_QUERY_SUB_FIRST) {
+ AttrListCheckData data = { ebsql, NULL, FALSE };
+
+ switch (elements[i]->query) {
+ case BOOK_QUERY_SUB_OR:
+ /* An OR doesn't have to force us to use a LEFT JOIN, as long
+ as all its sub-conditions are on the same field. */
+ query_preflight_foreach_sub (elements,
+ n_elements,
+ i, FALSE,
+ check_different_fields_cb,
+ &data);
+ case BOOK_QUERY_SUB_AND:
+ sub_query_context_push (ctx, elements[i]->query, data.condition);
+ break;
+ case BOOK_QUERY_SUB_END:
+ sub_query_context_pop (ctx);
+ break;
+
+ /* It's too complicated to properly perform
+ * the unary NOT operator on a constraint which
+ * accesses attribute lists.
+ *
+ * Hint, if the contact has a "%.com" email address
+ * and a "%.org" email address, what do we return
+ * for (not (endswith "email" ".com") ?
+ *
+ * Currently we rely on DISTINCT to sort out
+ * muliple results from the attribute list tables,
+ * this breaks down with NOT.
+ */
+ case BOOK_QUERY_SUB_NOT:
+ query_preflight_foreach_sub (elements,
+ n_elements,
+ i, FALSE,
+ check_has_attr_list_cb,
+ &data);
+
+ if (data.condition) {
+ context->status = MAX (
+ context->status,
+ PREFLIGHT_NOT_SUMMARIZED);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Setting invalid for NOT (mutli-attribute), "
+ "new status: %s\n",
+ EBSQL_STATUS_STR (context->status)));
+ }
+ break;
+
+ default:
+ g_warn_if_reached ();
+ }
+
+ continue;
+ }
+
+ test = (QueryFieldTest *) elements[i];
+ field_test = (EBookQueryTest) test->query;
+
+ if (!test->field)
+ test->field = summary_field_get (ebsql, test->field_id);
+
+ /* Even if the field is not in the summary, we need to
+ * retport unsupported errors if phone number queries are
+ * issued while libphonenumber is unavailable
+ */
+ if (!test->field) {
+
+ /* Special case for e_book_query_any_field_contains().
+ *
+ * We interpret 'x-evolution-any-field' as E_CONTACT_FIELD_LAST
+ */
+ if (test->field_id == E_CONTACT_FIELD_LAST) {
+
+ /* If we search for a NULL or zero length string, it
+ * means 'get all contacts', that is considered a summary
+ * query but is handled differently (i.e. we just drop the
+ * field tests and run a regular query).
+ *
+ * This is only true if the 'any field contains' query is
+ * the only test in the constraints, however.
+ */
+ if (n_elements == 1 && (!test->value || !test->value[0])) {
+
+ context->status = MAX (context->status, PREFLIGHT_LIST_ALL);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Encountered lonesome 'x-evolution-any-field' with empty value, "
+ "new status: %s\n",
+ EBSQL_STATUS_STR (context->status)));
+ } else {
+
+ /* Searching for a value with 'x-evolution-any-field' is
+ * not a summary query.
+ */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Encountered 'x-evolution-any-field', "
+ "new status: %s\n",
+ EBSQL_STATUS_STR (context->status)));
+ }
+
+ } else {
+
+ /* Couldnt resolve the field, it's not a summary query */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Field `%s' not in the summary, new status: %s\n",
+ EBSQL_FIELD_ID_STR (test->field_id),
+ EBSQL_STATUS_STR (context->status)));
+ }
+ }
+
+ if (test->field && test->field->type == E_TYPE_CONTACT_CERT) {
+ /* For certificates, and later potentially other fields,
+ * the only information in the summary is the fact that
+ * they exist, or not. So the only check we can do from
+ * the summary is BOOK_QUERY_EXISTS. */
+ if (field_test != BOOK_QUERY_EXISTS) {
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Cannot perform '%s' check on existence summary field '%s', new status: %s\n",
+ EBSQL_QUERY_TYPE_STR (field_test),
+ EBSQL_FIELD_ID_STR (test->field_id),
+ EBSQL_STATUS_STR (context->status)));
+ }
+ /* Bypass the other checks below which are not appropriate. */
+ continue;
+ }
+
+ switch (field_test) {
+ case E_BOOK_QUERY_IS:
+ break;
+
+ case BOOK_QUERY_EXISTS:
+ case E_BOOK_QUERY_CONTAINS:
+ case E_BOOK_QUERY_BEGINS_WITH:
+ case E_BOOK_QUERY_ENDS_WITH:
+ case E_BOOK_QUERY_REGEX_NORMAL:
+
+ /* All of these queries can only apply to string fields,
+ * or fields which hold multiple strings
+ */
+ if (test->field) {
+ if (test->field->type != G_TYPE_STRING &&
+ test->field->type != E_TYPE_CONTACT_ATTR_LIST) {
+ context->status = MAX (context->status, PREFLIGHT_INVALID);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Refusing pattern match on boolean field `%s', new status: %s\n",
+ EBSQL_FIELD_ID_STR (test->field_id),
+ EBSQL_STATUS_STR (context->status)));
+ }
+ }
+
+ break;
+
+ case BOOK_QUERY_EXISTS_VCARD:
+ /* Exists vCard queries only supported in the fallback */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Exists vCard requires full data, new status: %s\n",
+ EBSQL_STATUS_STR (context->status)));
+ break;
+
+ case E_BOOK_QUERY_REGEX_RAW:
+ /* Raw regex queries only supported in the fallback */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Raw regexp requires full data, new status: %s\n",
+ EBSQL_STATUS_STR (context->status)));
+ break;
+
+ case E_BOOK_QUERY_EQUALS_PHONE_NUMBER:
+ case E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER:
+ case E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER:
+
+ /* Phone number queries are supported so long as they are in the summary,
+ * libphonenumber is available, and the phone number string is a valid one
+ */
+ if (!e_phone_number_is_supported ()) {
+
+ context->status = MAX (context->status, PREFLIGHT_UNSUPPORTED);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Usupported phone number query, new status: %s\n",
+ EBSQL_STATUS_STR (context->status)));
+ } else {
+ QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+ EPhoneNumberCountrySource source;
+ EPhoneNumber *number;
+ const gchar *region_code;
+
+ if (phone_test->region)
+ region_code = phone_test->region;
+ else
+ region_code = ebsql->priv->region_code;
+
+ number = e_phone_number_from_string (
+ phone_test->value,
+ region_code, NULL);
+
+ if (number == NULL) {
+
+ context->status = MAX (context->status, PREFLIGHT_INVALID);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Invalid phone number `%s', new status: %s\n",
+ phone_test->value,
+ EBSQL_STATUS_STR (context->status)));
+ } else {
+ /* Collect values we'll need later while generating field
+ * tests, no need to parse the phone number more than once
+ */
+ phone_test->national = e_phone_number_get_national_number (number);
+ phone_test->country = e_phone_number_get_country_code (number, &source);
+ phone_test->national = remove_leading_zeros (phone_test->national);
+
+ if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT)
+ phone_test->country = 0;
+
+ e_phone_number_free (number);
+ }
+ }
+ break;
+ }
+
+ if (test->field &&
+ test->field->type == E_TYPE_CONTACT_ATTR_LIST) {
+ gint aux_index = summary_field_get_index (ebsql, test->field_id);
+
+ /* It's really improbable that we ever get 64 fields in the summary
+ * In any case we warn about this in e_book_sqlite_new_full().
+ */
+ g_warn_if_fail (aux_index >= 0 && aux_index < EBSQL_MAX_SUMMARY_FIELDS);
+ context->aux_mask |= (1 << aux_index);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Adding auxiliary field `%s' to the mask\n",
+ EBSQL_FIELD_ID_STR (test->field_id)));
+
+ /* If this condition is a *requirement* for the overall query to
+ match a given record (i.e. there's no surrounding 'OR' but
+ only 'AND'), then we can use an inner join for the query and
+ it will be a lot more efficient. If records without this
+ condition can also match the overall condition, then we must
+ use LEFT JOIN. */
+ if (sub_query_context_peek_cond_counter (ctx)) {
+ context->left_join_mask |= (1 << aux_index);
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT CHECK: "
+ "Using LEFT JOIN because auxiliary field is not absolute requirement\n"));
+ }
+ }
+ }
+
+ sub_query_context_free (ctx);
+}
+
+/* Handle special case of E_CONTACT_FULL_NAME
+ *
+ * For any query which accesses the full name field,
+ * we need to also OR it with any of the related name
+ * fields, IF those are found in the summary as well.
+ */
+static void
+query_preflight_substitute_full_name (PreflightContext *context,
+ EBookSqlite *ebsql)
+{
+ gint i, j;
+
+ for (i = 0; context->constraints != NULL && i < context->constraints->len; i++) {
+ SummaryField *family_name, *given_name, *nickname;
+ QueryElement *element;
+ QueryFieldTest *test;
+
+ element = g_ptr_array_index (context->constraints, i);
+
+ if (element->query >= BOOK_QUERY_SUB_FIRST)
+ continue;
+
+ test = (QueryFieldTest *) element;
+ if (test->field_id != E_CONTACT_FULL_NAME)
+ continue;
+
+ family_name = summary_field_get (ebsql, E_CONTACT_FAMILY_NAME);
+ given_name = summary_field_get (ebsql, E_CONTACT_GIVEN_NAME);
+ nickname = summary_field_get (ebsql, E_CONTACT_NICKNAME);
+
+ /* If any of these are in the summary, then we'll construct
+ * a grouped OR statment for this E_CONTACT_FULL_NAME test */
+ if (family_name || given_name || nickname) {
+ /* Add the OR directly before the E_CONTACT_FULL_NAME test */
+ constraints_insert_delimiter (context->constraints, i, BOOK_QUERY_SUB_OR);
+
+ j = i + 2;
+
+ if (family_name)
+ constraints_insert_field_test (
+ context->constraints, j++,
+ family_name, test->query,
+ test->value);
+
+ if (given_name)
+ constraints_insert_field_test (
+ context->constraints, j++,
+ given_name, test->query,
+ test->value);
+
+ if (nickname)
+ constraints_insert_field_test (
+ context->constraints, j++,
+ nickname, test->query,
+ test->value);
+
+ constraints_insert_delimiter (context->constraints, j, BOOK_QUERY_SUB_END);
+
+ i = j;
+ }
+ }
+}
+
+static void
+query_preflight (PreflightContext *context,
+ EBookSqlite *ebsql,
+ const gchar *sexp)
+{
+ EBSQL_NOTE (PREFLIGHT, g_printerr ("PREFLIGHT BEGIN\n"));
+ query_preflight_initialize (context, sexp);
+
+ if (context->status == PREFLIGHT_OK) {
+
+ query_preflight_check (context, ebsql);
+
+ /* No need to change the constraints if we're not
+ * going to generate statements with it
+ */
+ if (context->status == PREFLIGHT_OK) {
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr ("PREFLIGHT: Substituting full name\n"));
+
+ /* Handle E_CONTACT_FULL_NAME substitutions */
+ query_preflight_substitute_full_name (context, ebsql);
+
+ } else {
+ EBSQL_NOTE (PREFLIGHT, g_printerr ("PREFLIGHT: Clearing context\n"));
+
+ /* We might use this context to perform a fallback query,
+ * so let's clear out all the constraints now
+ */
+ preflight_context_clear (context);
+ }
+ }
+
+ EBSQL_NOTE (
+ PREFLIGHT,
+ g_printerr (
+ "PREFLIGHT END (status: %s)\n",
+ EBSQL_STATUS_STR (context->status)));
+}
+
+/**********************************************************
+ * Field Test Generators *
+ **********************************************************
+ *
+ * This section contains the field test generators for
+ * various EBookQueryTest types. When implementing new
+ * query types, a new GenerateFieldTest needs to be created
+ * and added to the table below.
+ */
+
+typedef void (* GenerateFieldTest) (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test);
+
+/* This function escapes characters which need escaping
+ * for LIKE statements as well as the single quotes.
+ *
+ * The return value is not suitable to be formatted
+ * with %Q or %q
+ */
+static gchar *
+ebsql_normalize_for_like (QueryFieldTest *test,
+ gboolean reverse_string,
+ gboolean *escape_needed)
+{
+ GString *str;
+ size_t len;
+ gchar c;
+ gboolean escape_modifier_needed = FALSE;
+ const gchar *normal = NULL;
+ const gchar *ptr;
+ const gchar *str_to_escape;
+ gchar *reverse = NULL;
+ gchar *freeme = NULL;
+
+ if (test->field_id == E_CONTACT_UID ||
+ test->field_id == E_CONTACT_REV) {
+ normal = test->value;
+ } else {
+ freeme = e_util_utf8_normalize (test->value);
+ normal = freeme;
+ }
+
+ if (reverse_string) {
+ reverse = g_utf8_strreverse (normal, -1);
+ str_to_escape = reverse;
+ } else
+ str_to_escape = normal;
+
+ /* Just assume each character must be escaped. The result of this function
+ * is discarded shortly after calling this function. Therefore it's
+ * acceptable to possibly allocate twice the memory needed.
+ */
+ len = strlen (str_to_escape);
+ str = g_string_sized_new (2 * len + 4 + strlen (EBSQL_ESCAPE_SEQUENCE) - 1);
+
+ ptr = str_to_escape;
+ while ((c = *ptr++)) {
+ if (c == '\'') {
+ g_string_append_c (str, '\'');
+ } else if (c == '%' || c == '_' || c == '^') {
+ g_string_append_c (str, '^');
+ escape_modifier_needed = TRUE;
+ }
+
+ g_string_append_c (str, c);
+ }
+
+ if (escape_needed)
+ *escape_needed = escape_modifier_needed;
+
+ g_free (freeme);
+ g_free (reverse);
+
+ return g_string_free (str, FALSE);
+}
+
+static void
+field_test_query_is (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gchar *normal;
+
+ ebsql_string_append_column (string, field, NULL);
+
+ if (test->field_id == E_CONTACT_UID ||
+ test->field_id == E_CONTACT_REV) {
+ /* UID & REV fields are not normalized in the summary */
+ ebsql_string_append_printf (string, " = %Q", test->value);
+ } else {
+ normal = e_util_utf8_normalize (test->value);
+ ebsql_string_append_printf (string, " = %Q", normal);
+ g_free (normal);
+ }
+}
+
+static void
+field_test_query_contains (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gboolean need_escape;
+ gchar *escaped;
+
+ escaped = ebsql_normalize_for_like (test, FALSE, &need_escape);
+
+ g_string_append_c (string, '(');
+
+ ebsql_string_append_column (string, field, NULL);
+ g_string_append (string, " IS NOT NULL AND ");
+ ebsql_string_append_column (string, field, NULL);
+ g_string_append (string, " LIKE '%");
+ g_string_append (string, escaped);
+ g_string_append (string, "%'");
+
+ if (need_escape)
+ g_string_append (string, EBSQL_ESCAPE_SEQUENCE);
+
+ g_string_append_c (string, ')');
+
+ g_free (escaped);
+}
+
+static void
+field_test_query_begins_with (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gboolean need_escape;
+ gchar *escaped;
+
+ escaped = ebsql_normalize_for_like (test, FALSE, &need_escape);
+
+ g_string_append_c (string, '(');
+ ebsql_string_append_column (string, field, NULL);
+ g_string_append (string, " IS NOT NULL AND ");
+
+ ebsql_string_append_column (string, field, NULL);
+ g_string_append (string, " LIKE \'");
+ g_string_append (string, escaped);
+ g_string_append (string, "%\'");
+
+ if (need_escape)
+ g_string_append (string, EBSQL_ESCAPE_SEQUENCE);
+ g_string_append_c (string, ')');
+
+ g_free (escaped);
+}
+
+static void
+field_test_query_ends_with (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gboolean need_escape;
+ gchar *escaped;
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+
+ escaped = ebsql_normalize_for_like (test, TRUE, &need_escape);
+
+ g_string_append_c (string, '(');
+ ebsql_string_append_column (string, field, EBSQL_SUFFIX_REVERSE);
+ g_string_append (string, " IS NOT NULL AND ");
+
+ ebsql_string_append_column (string, field, EBSQL_SUFFIX_REVERSE);
+ g_string_append (string, " LIKE \'");
+ g_string_append (string, escaped);
+ g_string_append (string, "%\'");
+
+ } else {
+
+ escaped = ebsql_normalize_for_like (test, FALSE, &need_escape);
+ g_string_append_c (string, '(');
+
+ ebsql_string_append_column (string, field, NULL);
+ g_string_append (string, " IS NOT NULL AND ");
+
+ ebsql_string_append_column (string, field, NULL);
+ g_string_append (string, " LIKE \'%");
+ g_string_append (string, escaped);
+ g_string_append (string, "\'");
+ }
+
+ if (need_escape)
+ g_string_append (string, EBSQL_ESCAPE_SEQUENCE);
+
+ g_string_append_c (string, ')');
+ g_free (escaped);
+}
+
+static void
+field_test_query_eqphone (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+
+ g_string_append_c (string, '(');
+ ebsql_string_append_column (string, field, EBSQL_SUFFIX_PHONE);
+ ebsql_string_append_printf (string, " = %Q AND ", phone_test->national);
+
+ /* For exact matches, a country code qualifier is required by both
+ * query input and row input
+ */
+ ebsql_string_append_column (string, field, EBSQL_SUFFIX_COUNTRY);
+ g_string_append (string, " != 0 AND ");
+
+ ebsql_string_append_column (string, field, EBSQL_SUFFIX_COUNTRY);
+ ebsql_string_append_printf (string, " = %d", phone_test->country);
+ g_string_append_c (string, ')');
+
+ } else {
+
+ /* No indexed columns available, perform the fallback */
+ g_string_append (string, EBSQL_FUNC_EQPHONE_EXACT " (");
+ ebsql_string_append_column (string, field, NULL);
+ ebsql_string_append_printf (string, ", %Q)", test->value);
+ }
+}
+
+static void
+field_test_query_eqphone_national (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test)
+{
+
+ SummaryField *field = test->field;
+ QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+
+ /* Only a compound expression if there is a country code */
+ if (phone_test->country)
+ g_string_append_c (string, '(');
+
+ /* Generate: phone = %Q */
+ ebsql_string_append_column (string, field, EBSQL_SUFFIX_PHONE);
+ ebsql_string_append_printf (string, " = %Q", phone_test->national);
+
+ /* When doing a national search, no need to check country
+ * code unless the query number also has a country code
+ */
+ if (phone_test->country) {
+ /* Generate: (phone = %Q AND (country = 0 OR country = %d)) */
+ g_string_append (string, " AND (");
+ ebsql_string_append_column (string, field, EBSQL_SUFFIX_COUNTRY);
+ g_string_append (string, " = 0 OR ");
+ ebsql_string_append_column (string, field, EBSQL_SUFFIX_COUNTRY);
+ ebsql_string_append_printf (string, " = %d))", phone_test->country);
+
+ }
+
+ } else {
+
+ /* No indexed columns available, perform the fallback */
+ g_string_append (string, EBSQL_FUNC_EQPHONE_NATIONAL " (");
+ ebsql_string_append_column (string, field, NULL);
+ ebsql_string_append_printf (string, ", %Q)", test->value);
+ }
+}
+
+static void
+field_test_query_eqphone_short (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+
+ /* No quick way to do the short match */
+ g_string_append (string, EBSQL_FUNC_EQPHONE_SHORT " (");
+ ebsql_string_append_column (string, field, NULL);
+ ebsql_string_append_printf (string, ", %Q)", test->value);
+}
+
+static void
+field_test_query_regex_normal (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gchar *normal;
+
+ normal = e_util_utf8_normalize (test->value);
+
+ if (field->aux_table)
+ ebsql_string_append_printf (
+ string, "%s.value REGEXP %Q",
+ field->aux_table_symbolic,
+ normal);
+ else
+ ebsql_string_append_printf (
+ string, "summary.%s REGEXP %Q",
+ field->dbname,
+ normal);
+
+ g_free (normal);
+}
+
+static void
+field_test_query_exists (EBookSqlite *ebsql,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+
+ ebsql_string_append_column (string, field, NULL);
+
+ if (test->field->type == E_TYPE_CONTACT_CERT)
+ ebsql_string_append_printf (string, " IS NOT '0'");
+ else
+ ebsql_string_append_printf (string, " IS NOT NULL");
+}
+
+/* Lookup table for field test generators per EBookQueryTest,
+ *
+ * WARNING: This must stay in line with the EBookQueryTest definition.
+ */
+static const GenerateFieldTest field_test_func_table[] = {
+ field_test_query_is, /* E_BOOK_QUERY_IS */
+ field_test_query_contains, /* E_BOOK_QUERY_CONTAINS */
+ field_test_query_begins_with, /* E_BOOK_QUERY_BEGINS_WITH */
+ field_test_query_ends_with, /* E_BOOK_QUERY_ENDS_WITH */
+ field_test_query_eqphone, /* E_BOOK_QUERY_EQUALS_PHONE_NUMBER */
+ field_test_query_eqphone_national, /* E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER */
+ field_test_query_eqphone_short, /* E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER */
+ field_test_query_regex_normal, /* E_BOOK_QUERY_REGEX_NORMAL */
+ NULL /* Requires fallback */, /* E_BOOK_QUERY_REGEX_RAW */
+ field_test_query_exists, /* BOOK_QUERY_EXISTS */
+ NULL /* Requires fallback */ /* BOOK_QUERY_EXISTS_VCARD */
+};
+
+/**********************************************************
+ * Querying Contacts *
+ **********************************************************/
+
+/* The various search types indicate what should be fetched
+ */
+typedef enum {
+ SEARCH_FULL, /* Get a list of EbSqlSearchData */
+ SEARCH_UID_AND_REV, /* Get a list of EbSqlSearchData, with shallow vcards only containing UID & REV */
+ SEARCH_UID, /* Get a list of UID strings */
+ SEARCH_COUNT, /* Get the number of matching rows */
+} SearchType;
+
+static void
+ebsql_generate_constraints (EBookSqlite *ebsql,
+ GString *string,
+ GPtrArray *constraints,
+ const gchar *sexp)
+{
+ SubQueryContext *ctx;
+ QueryDelimiter *delim;
+ QueryFieldTest *test;
+ QueryElement **elements;
+ gint n_elements, i;
+
+ /* If there are no constraints, we generate the fallback constraint for 'sexp' */
+ if (constraints == NULL) {
+ ebsql_string_append_printf (
+ string,
+ EBSQL_FUNC_COMPARE_VCARD " (%Q, %s)",
+ sexp, EBSQL_VCARD_FRAGMENT (ebsql));
+ return;
+ }
+
+ elements = (QueryElement **) constraints->pdata;
+ n_elements = constraints->len;
+
+ ctx = sub_query_context_new ();
+
+ for (i = 0; i < n_elements; i++) {
+ GenerateFieldTest generate_test_func = NULL;
+
+ /* Seperate field tests with the appropriate grouping */
+ if (elements[i]->query != BOOK_QUERY_SUB_END &&
+ sub_query_context_increment (ctx) > 0) {
+ guint delim_type = sub_query_context_peek_type (ctx);
+
+ switch (delim_type) {
+ case BOOK_QUERY_SUB_AND:
+
+ g_string_append (string, " AND ");
+ break;
+
+ case BOOK_QUERY_SUB_OR:
+
+ g_string_append (string, " OR ");
+ break;
+
+ case BOOK_QUERY_SUB_NOT:
+
+ /* Nothing to do between children of NOT,
+ * there should only ever be one child of NOT anyway
+ */
+ break;
+
+ case BOOK_QUERY_SUB_END:
+ default:
+ g_warn_if_reached ();
+ }
+ }
+
+ if (elements[i]->query >= BOOK_QUERY_SUB_FIRST) {
+ delim = (QueryDelimiter *) elements[i];
+
+ switch (delim->query) {
+
+ case BOOK_QUERY_SUB_NOT:
+
+ /* NOT is a unary operator and as such
+ * comes before the opening parenthesis
+ */
+ g_string_append (string, "NOT ");
+
+ /* Fall through */
+
+ case BOOK_QUERY_SUB_AND:
+ case BOOK_QUERY_SUB_OR:
+
+ /* Open a grouped statement and push the context */
+ sub_query_context_push (ctx, delim->query, FALSE);
+ g_string_append_c (string, '(');
+ break;
+
+ case BOOK_QUERY_SUB_END:
+ /* Close a grouped statement and pop the context */
+ g_string_append_c (string, ')');
+ sub_query_context_pop (ctx);
+ break;
+ default:
+ g_warn_if_reached ();
+ }
+
+ continue;
+ }
+
+ /* Find the appropriate field test generator */
+ test = (QueryFieldTest *) elements[i];
+ if (test->query < G_N_ELEMENTS (field_test_func_table))
+ generate_test_func = field_test_func_table[test->query];
+
+ /* These should never happen, if it does it should be
+ * fixed in the preflight checks
+ */
+ g_warn_if_fail (generate_test_func != NULL);
+ g_warn_if_fail (test->field != NULL);
+
+ /* Generate the field test */
+ /* coverity[var_deref_op] */
+ generate_test_func (ebsql, string, test);
+ }
+
+ sub_query_context_free (ctx);
+}
+
+/* Generates the SELECT portion of the query, this will take care of
+ * preparing the context of the query, and add the needed JOIN statements
+ * based on which fields are referenced in the query expression.
+ *
+ * This also handles getting the correct callback and asking for the
+ * right data depending on the 'search_type'
+ */
+static EbSqlRowFunc
+ebsql_generate_select (EBookSqlite *ebsql,
+ GString *string,
+ SearchType search_type,
+ PreflightContext *context,
+ GError **error)
+{
+ EbSqlRowFunc callback = NULL;
+ gboolean add_auxiliary_tables = FALSE;
+ gint i;
+
+ if (context->status == PREFLIGHT_OK &&
+ context->aux_mask != 0)
+ add_auxiliary_tables = TRUE;
+
+ g_string_append (string, "SELECT ");
+ if (add_auxiliary_tables)
+ g_string_append (string, "DISTINCT ");
+
+ switch (search_type) {
+ case SEARCH_FULL:
+ callback = collect_full_results_cb;
+ g_string_append (string, "summary.uid, ");
+ g_string_append (string, EBSQL_VCARD_FRAGMENT (ebsql));
+ g_string_append (string, ", summary.bdata ");
+ break;
+ case SEARCH_UID_AND_REV:
+ callback = collect_lean_results_cb;
+ g_string_append (string, "summary.uid, summary.Rev, summary.bdata ");
+ break;
+ case SEARCH_UID:
+ callback = collect_uid_results_cb;
+ g_string_append (string, "summary.uid ");
+ break;
+ case SEARCH_COUNT:
+ callback = get_count_cb;
+ if (context->aux_mask != 0)
+ g_string_append (string, "count (DISTINCT summary.uid) ");
+ else
+ g_string_append (string, "count (*) ");
+ break;
+ }
+
+ ebsql_string_append_printf (string, "FROM %Q AS summary", ebsql->priv->folderid);
+
+ /* Add any required auxiliary tables into the query context */
+ if (add_auxiliary_tables) {
+ for (i = 0; i < ebsql->priv->n_summary_fields; i++) {
+
+ /* We cap this at EBSQL_MAX_SUMMARY_FIELDS (64 bits) at creation time */
+ if ((context->aux_mask & (1 << i)) != 0) {
+ SummaryField *field = &(ebsql->priv->summary_fields[i]);
+ gboolean left_join = (context->left_join_mask >> i) & 1;
+
+ /* Note the '+' in the JOIN statement.
+ *
+ * This plus makes the uid's index ineligable to participate
+ * in any indexing.
+ *
+ * Without this, the indexes which we prefer for prefix or
+ * suffix matching in the auxiliary tables are ignored and
+ * only considered on exact matches.
+ *
+ * This is crucial to ensure that the uid index does not
+ * compete with the value index in constraints such as:
+ *
+ * WHERE email_list.value LIKE "boogieman%"
+ */
+ ebsql_string_append_printf (
+ string, " %sJOIN %Q AS %s ON %s%s.uid = summary.uid",
+ left_join ? "LEFT " : "",
+ field->aux_table,
+ field->aux_table_symbolic,
+ left_join ? "" : "+",
+ field->aux_table_symbolic);
+ }
+ }
+ }
+
+ return callback;
+}
+
+static gboolean
+ebsql_is_autocomplete_query (PreflightContext *context)
+{
+ QueryFieldTest *test;
+ QueryElement **elements;
+ gint n_elements, i;
+ int non_aux_fields = 0;
+
+ if (context->status != PREFLIGHT_OK || context->aux_mask == 0)
+ return FALSE;
+
+ elements = (QueryElement **) context->constraints->pdata;
+ n_elements = context->constraints->len;
+
+ for (i = 0; i < n_elements; i++) {
+ test = (QueryFieldTest *) elements[i];
+
+ /* For these, check if the field being operated on is
+ an auxiliary field or not. */
+ if (elements[i]->query == E_BOOK_QUERY_BEGINS_WITH ||
+ elements[i]->query == E_BOOK_QUERY_ENDS_WITH ||
+ elements[i]->query == E_BOOK_QUERY_IS ||
+ elements[i]->query == BOOK_QUERY_EXISTS ||
+ elements[i]->query == E_BOOK_QUERY_CONTAINS) {
+ if (test->field->type != E_TYPE_CONTACT_ATTR_LIST)
+ non_aux_fields++;
+ continue;
+ }
+
+ /* Nothing else is allowed other than "(or" ... ")" */
+ if (elements[i]->query != BOOK_QUERY_SUB_OR &&
+ elements[i]->query != BOOK_QUERY_SUB_END)
+ return FALSE;
+ }
+
+ /* If there were no non-aux fields being queried, don't bother */
+ return non_aux_fields != 0;
+}
+
+static EbSqlRowFunc
+ebsql_generate_autocomplete_query (EBookSqlite *ebsql,
+ GString *string,
+ SearchType search_type,
+ PreflightContext *context,
+ GError **error)
+{
+ QueryElement **elements;
+ gint n_elements, i;
+ guint64 aux_mask = context->aux_mask;
+ guint64 left_join_mask = context->left_join_mask;
+ EbSqlRowFunc callback;
+ gboolean first = TRUE;
+
+ elements = (QueryElement **) context->constraints->pdata;
+ n_elements = context->constraints->len;
+
+ /* First the queries which use aux tables. */
+ for (i = 0; i < n_elements; i++) {
+ GenerateFieldTest generate_test_func = NULL;
+ QueryFieldTest *test;
+ gint aux_index;
+
+ if (elements[i]->query == BOOK_QUERY_SUB_OR ||
+ elements[i]->query == BOOK_QUERY_SUB_END)
+ continue;
+
+ test = (QueryFieldTest *) elements[i];
+ if (test->field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ aux_index = summary_field_get_index (ebsql, test->field_id);
+ g_warn_if_fail (aux_index >= 0 && aux_index < EBSQL_MAX_SUMMARY_FIELDS);
+ context->aux_mask = (1 << aux_index);
+ context->left_join_mask = 0;
+
+ callback = ebsql_generate_select (ebsql, string, search_type, context, error);
+ g_string_append (string, " WHERE ");
+ context->aux_mask = aux_mask;
+ context->left_join_mask = left_join_mask;
+ if (!callback)
+ return NULL;
+
+ generate_test_func = field_test_func_table[test->query];
+ generate_test_func (ebsql, string, test);
+
+ g_string_append (string, " UNION ");
+ }
+ /* Finally, generate the SELECT for the primary fields. */
+ context->aux_mask = 0;
+ callback = ebsql_generate_select (ebsql, string, search_type, context, error);
+ context->aux_mask = aux_mask;
+ if (!callback)
+ return NULL;
+
+ g_string_append (string, " WHERE ");
+
+ for (i = 0; i < n_elements; i++) {
+ GenerateFieldTest generate_test_func = NULL;
+ QueryFieldTest *test;
+
+ if (elements[i]->query == BOOK_QUERY_SUB_OR ||
+ elements[i]->query == BOOK_QUERY_SUB_END)
+ continue;
+
+ test = (QueryFieldTest *) elements[i];
+ if (test->field->type == E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ if (!first)
+ g_string_append (string, " OR ");
+ else
+ first = FALSE;
+
+ generate_test_func = field_test_func_table[test->query];
+ generate_test_func (ebsql, string, test);
+ }
+
+ return callback;
+}
+static gboolean
+ebsql_do_search_query (EBookSqlite *ebsql,
+ PreflightContext *context,
+ const gchar *sexp,
+ SearchType search_type,
+ GSList **return_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *string;
+ EbSqlRowFunc callback = NULL;
+ gboolean success = FALSE;
+
+ /* We might calculate a reasonable estimation of bytes
+ * during the preflight checks */
+ string = g_string_sized_new (GENERATED_QUERY_BYTES);
+
+ /* Extra special case. For the common case of the email composer's
+ addressbook autocompletion, we really want the most optimal query.
+ So check for it and use a basically hand-crafted one. */
+ if (ebsql_is_autocomplete_query(context)) {
+ callback = ebsql_generate_autocomplete_query (ebsql, string, search_type, context, error);
+ } else {
+ /* Generate the leading SELECT statement */
+ callback = ebsql_generate_select (
+ ebsql, string, search_type, context, error);
+
+ if (callback &&
+ EBSQL_STATUS_GEN_CONSTRAINTS (context->status)) {
+ /*
+ * Now generate the search expression on the main contacts table
+ */
+ g_string_append (string, " WHERE ");
+ ebsql_generate_constraints (
+ ebsql, string, context->constraints, sexp);
+ }
+ }
+
+ if (callback)
+ success = ebsql_exec (
+ ebsql, string->str,
+ callback, return_data,
+ cancellable, error);
+
+ g_string_free (string, TRUE);
+
+ return success;
+}
+
+/* ebsql_search_query:
+ * @ebsql: An EBookSqlite
+ * @sexp: The search expression, or NULL for all contacts
+ * @search_type: Indicates what kind of data should be returned
+ * @return_data: A list of data fetched from the DB, as specified by 'search_type'
+ * @error: Location to store any error which may have occurred
+ *
+ * This is the main common entry point for querying contacts.
+ *
+ * If the query cannot be satisfied with the summary, then
+ * a fallback will automatically be used.
+ */
+static gboolean
+ebsql_search_query (EBookSqlite *ebsql,
+ const gchar *sexp,
+ SearchType search_type,
+ GSList **return_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ PreflightContext context = PREFLIGHT_CONTEXT_INIT;
+ gboolean success = FALSE;
+
+ /* Now start with the query preflighting */
+ query_preflight (&context, ebsql, sexp);
+
+ switch (context.status) {
+ case PREFLIGHT_OK:
+ case PREFLIGHT_LIST_ALL:
+ case PREFLIGHT_NOT_SUMMARIZED:
+ /* No errors, let's really search */
+ success = ebsql_do_search_query (
+ ebsql, &context, sexp,
+ search_type, return_data,
+ cancellable, error);
+ break;
+
+ case PREFLIGHT_INVALID:
+ EBSQL_SET_ERROR (
+ error,
+ E_BOOK_SQLITE_ERROR_INVALID_QUERY,
+ _("Invalid query: %s"), sexp);
+ break;
+
+ case PREFLIGHT_UNSUPPORTED:
+ EBSQL_SET_ERROR_LITERAL (
+ error,
+ E_BOOK_SQLITE_ERROR_UNSUPPORTED_QUERY,
+ _("Query contained unsupported elements"));
+ break;
+ }
+
+ preflight_context_clear (&context);
+
+ return success;
+}
+
+/******************************************************************
+ * EbSqlCursor Implementation *
+ ******************************************************************/
+typedef struct _CursorState CursorState;
+
+struct _CursorState {
+ gchar **values; /* The current cursor position, results will be returned after this position */
+ gchar *last_uid; /* The current cursor contact UID position, used as a tie breaker */
+ EbSqlCursorOrigin position; /* The position is updated with the cursor state and is used to distinguish
+ * between the beginning and the ending of the cursor's contact list.
+ * While the cursor is in a non-null state, the position will be
+ * EBSQL_CURSOR_ORIGIN_CURRENT.
+ */
+};
+
+struct _EbSqlCursor {
+ EBookBackendSExp *sexp; /* An EBookBackendSExp based on the query, used by e_book_sqlite_cursor_compare () */
+ gchar *select_vcards; /* The first fragment when querying results */
+ gchar *select_count; /* The first fragment when querying contact counts */
+ gchar *query; /* The SQL query expression derived from the passed search expression */
+ gchar *order; /* The normal order SQL query fragment to append at the end, containing ORDER BY etc */
+ gchar *reverse_order; /* The reverse order SQL query fragment to append at the end, containing ORDER BY etc */
+
+ EContactField *sort_fields; /* The fields to sort in a query in the order or sort priority */
+ EBookCursorSortType *sort_types; /* The sort method to use for each field */
+ gint n_sort_fields; /* The amound of sort fields */
+
+ CursorState state;
+};
+
+static CursorState *cursor_state_copy (EbSqlCursor *cursor,
+ CursorState *state);
+static void cursor_state_free (EbSqlCursor *cursor,
+ CursorState *state);
+static void cursor_state_clear (EbSqlCursor *cursor,
+ CursorState *state,
+ EbSqlCursorOrigin position);
+static void cursor_state_set_from_contact (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ CursorState *state,
+ EContact *contact);
+static void cursor_state_set_from_vcard (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ CursorState *state,
+ const gchar *vcard);
+
+static CursorState *
+cursor_state_copy (EbSqlCursor *cursor,
+ CursorState *state)
+{
+ CursorState *copy;
+ gint i;
+
+ copy = g_slice_new0 (CursorState);
+ copy->values = g_new0 (gchar *, cursor->n_sort_fields);
+
+ for (i = 0; i < cursor->n_sort_fields; i++)
+ copy->values[i] = g_strdup (state->values[i]);
+
+ copy->last_uid = g_strdup (state->last_uid);
+ copy->position = state->position;
+
+ return copy;
+}
+
+static void
+cursor_state_free (EbSqlCursor *cursor,
+ CursorState *state)
+{
+ if (state) {
+ cursor_state_clear (cursor, state, EBSQL_CURSOR_ORIGIN_BEGIN);
+ g_free (state->values);
+ g_slice_free (CursorState, state);
+ }
+}
+
+static void
+cursor_state_clear (EbSqlCursor *cursor,
+ CursorState *state,
+ EbSqlCursorOrigin position)
+{
+ gint i;
+
+ for (i = 0; i < cursor->n_sort_fields; i++) {
+ g_free (state->values[i]);
+ state->values[i] = NULL;
+ }
+
+ g_free (state->last_uid);
+ state->last_uid = NULL;
+ state->position = position;
+}
+
+static void
+cursor_state_set_from_contact (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ CursorState *state,
+ EContact *contact)
+{
+ gint i;
+
+ cursor_state_clear (cursor, state, EBSQL_CURSOR_ORIGIN_BEGIN);
+
+ for (i = 0; i < cursor->n_sort_fields; i++) {
+ const gchar *string = e_contact_get_const (contact, cursor->sort_fields[i]);
+ SummaryField *field;
+ gchar *sort_key;
+
+ if (string)
+ sort_key = e_collator_generate_key (
+ ebsql->priv->collator,
+ string, NULL);
+ else
+ sort_key = g_strdup ("");
+
+ field = summary_field_get (ebsql, cursor->sort_fields[i]);
+
+ if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ state->values[i] = sort_key;
+ } else {
+ state->values[i] = ebsql_encode_vcard_sort_key (sort_key);
+ g_free (sort_key);
+ }
+ }
+
+ state->last_uid = e_contact_get (contact, E_CONTACT_UID);
+ state->position = EBSQL_CURSOR_ORIGIN_CURRENT;
+}
+
+static void
+cursor_state_set_from_vcard (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ CursorState *state,
+ const gchar *vcard)
+{
+ EContact *contact;
+
+ contact = e_contact_new_from_vcard (vcard);
+ cursor_state_set_from_contact (ebsql, cursor, state, contact);
+ g_object_unref (contact);
+}
+
+static gboolean
+ebsql_cursor_setup_query (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ const gchar *sexp,
+ GError **error)
+{
+ PreflightContext context = PREFLIGHT_CONTEXT_INIT;
+ GString *string;
+
+ /* Preflighting and error checking */
+ if (sexp) {
+ query_preflight (&context, ebsql, sexp);
+
+ if (context.status > PREFLIGHT_NOT_SUMMARIZED) {
+ EBSQL_SET_ERROR_LITERAL (
+ error,
+ E_BOOK_SQLITE_ERROR_INVALID_QUERY,
+ _("Invalid query for EbSqlCursor"));
+
+ preflight_context_clear (&context);
+ return FALSE;
+
+ }
+ }
+
+ /* Now we caught the errors, let's generate our queries and get out of here ... */
+ g_free (cursor->select_vcards);
+ g_free (cursor->select_count);
+ g_free (cursor->query);
+ g_clear_object (&(cursor->sexp));
+
+ /* Generate the leading SELECT portions that we need */
+ string = g_string_new ("");
+ ebsql_generate_select (ebsql, string, SEARCH_FULL, &context, NULL);
+ cursor->select_vcards = g_string_free (string, FALSE);
+
+ string = g_string_new ("");
+ ebsql_generate_select (ebsql, string, SEARCH_COUNT, &context, NULL);
+ cursor->select_count = g_string_free (string, FALSE);
+
+ if (sexp == NULL || context.status == PREFLIGHT_LIST_ALL) {
+ cursor->query = NULL;
+ cursor->sexp = NULL;
+ } else {
+ /* Generate the constraints for our queries
+ */
+ string = g_string_new (NULL);
+ ebsql_generate_constraints (
+ ebsql, string, context.constraints, sexp);
+ cursor->query = g_string_free (string, FALSE);
+ cursor->sexp = e_book_backend_sexp_new (sexp);
+ }
+
+ preflight_context_clear (&context);
+
+ return TRUE;
+}
+
+static gchar *
+ebsql_cursor_order_by_fragment (EBookSqlite *ebsql,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ gboolean reverse)
+{
+ GString *string;
+ gint i;
+
+ string = g_string_new ("ORDER BY ");
+
+ for (i = 0; i < n_sort_fields; i++) {
+ SummaryField *field = summary_field_get (ebsql, sort_fields[i]);
+
+ if (i > 0)
+ g_string_append (string, ", ");
+
+ if (field &&
+ (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ g_string_append (string, "summary.");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBSQL_SUFFIX_SORT_KEY " ");
+ } else {
+ g_string_append (string, EBSQL_VCARD_FRAGMENT (ebsql));
+ g_string_append (string, " COLLATE ");
+ g_string_append (string, EBSQL_COLLATE_PREFIX);
+ g_string_append (string, e_contact_field_name (sort_fields[i]));
+ g_string_append_c (string, ' ');
+ }
+
+ if (reverse)
+ g_string_append (string, (sort_types[i] == E_BOOK_CURSOR_SORT_ASCENDING ? "DESC" : "ASC"));
+ else
+ g_string_append (string, (sort_types[i] == E_BOOK_CURSOR_SORT_ASCENDING ? "ASC" : "DESC"));
+ }
+
+ /* Also order the UID, since it's our tie breaker */
+ if (n_sort_fields > 0)
+ g_string_append (string, ", ");
+
+ g_string_append (string, "summary.uid ");
+ g_string_append (string, reverse ? "DESC" : "ASC");
+
+ return g_string_free (string, FALSE);
+}
+
+static EbSqlCursor *
+ebsql_cursor_new (EBookSqlite *ebsql,
+ const gchar *sexp,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields)
+{
+ EbSqlCursor *cursor = g_slice_new0 (EbSqlCursor);
+
+ cursor->order = ebsql_cursor_order_by_fragment (
+ ebsql, sort_fields, sort_types, n_sort_fields, FALSE);
+ cursor->reverse_order = ebsql_cursor_order_by_fragment (
+ ebsql, sort_fields, sort_types, n_sort_fields, TRUE);
+
+ /* Sort parameters */
+ cursor->n_sort_fields = n_sort_fields;
+ cursor->sort_fields = g_memdup (sort_fields, sizeof (EContactField) * n_sort_fields);
+ cursor->sort_types = g_memdup (sort_types, sizeof (EBookCursorSortType) * n_sort_fields);
+
+ /* Cursor state */
+ cursor->state.values = g_new0 (gchar *, n_sort_fields);
+ cursor->state.last_uid = NULL;
+ cursor->state.position = EBSQL_CURSOR_ORIGIN_BEGIN;
+
+ return cursor;
+}
+
+static void
+ebsql_cursor_free (EbSqlCursor *cursor)
+{
+ if (cursor) {
+ cursor_state_clear (cursor, &(cursor->state), EBSQL_CURSOR_ORIGIN_BEGIN);
+ g_free (cursor->state.values);
+
+ g_clear_object (&(cursor->sexp));
+ g_free (cursor->select_vcards);
+ g_free (cursor->select_count);
+ g_free (cursor->query);
+ g_free (cursor->order);
+ g_free (cursor->reverse_order);
+ g_free (cursor->sort_fields);
+ g_free (cursor->sort_types);
+
+ g_slice_free (EbSqlCursor, cursor);
+ }
+}
+
+#define GREATER_OR_LESS(cursor, idx, reverse) \
+ (reverse ? \
+ (((EbSqlCursor *) cursor)->sort_types[idx] == E_BOOK_CURSOR_SORT_ASCENDING ? '<' : '>') : \
+ (((EbSqlCursor *) cursor)->sort_types[idx] == E_BOOK_CURSOR_SORT_ASCENDING ? '>' : '<'))
+
+static inline void
+ebsql_cursor_format_equality (EBookSqlite *ebsql,
+ GString *string,
+ EContactField field_id,
+ const gchar *value,
+ gchar equality)
+{
+ SummaryField *field = summary_field_get (ebsql, field_id);
+
+ if (field &&
+ (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+
+ g_string_append (string, "summary.");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBSQL_SUFFIX_SORT_KEY " ");
+
+ ebsql_string_append_printf (string, "%c %Q", equality, value);
+
+ } else {
+ ebsql_string_append_printf (
+ string, "(%s %c %Q ",
+ EBSQL_VCARD_FRAGMENT (ebsql),
+ equality, value);
+
+ g_string_append (string, "COLLATE " EBSQL_COLLATE_PREFIX);
+ g_string_append (string, e_contact_field_name (field_id));
+ g_string_append_c (string, ')');
+ }
+}
+
+static gchar *
+ebsql_cursor_constraints (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ CursorState *state,
+ gboolean reverse,
+ gboolean include_current_uid)
+{
+ GString *string;
+ gint i, j;
+
+ /* Example for:
+ * ORDER BY family_name ASC, given_name DESC
+ *
+ * Where current cursor values are:
+ * family_name = Jackson
+ * given_name = Micheal
+ *
+ * With reverse = FALSE
+ *
+ * (summary.family_name > 'Jackson') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name < 'Micheal') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid > 'last-uid')
+ *
+ * With reverse = TRUE (needed for moving the cursor backwards through results)
+ *
+ * (summary.family_name < 'Jackson') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name > 'Micheal') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid < 'last-uid')
+ *
+ */
+ string = g_string_new (NULL);
+
+ for (i = 0; i <= cursor->n_sort_fields; i++) {
+
+ /* Break once we hit a NULL value */
+ if ((i < cursor->n_sort_fields && state->values[i] == NULL) ||
+ (i == cursor->n_sort_fields && state->last_uid == NULL))
+ break;
+
+ /* Between each qualifier, add an 'OR' */
+ if (i > 0)
+ g_string_append (string, " OR ");
+
+ /* Begin qualifier */
+ g_string_append_c (string, '(');
+
+ /* Create the '=' statements leading up to the current tie breaker */
+ for (j = 0; j < i; j++) {
+ ebsql_cursor_format_equality (ebsql, string,
+ cursor->sort_fields[j],
+ state->values[j], '=');
+ g_string_append (string, " AND ");
+ }
+
+ if (i == cursor->n_sort_fields) {
+
+ /* The 'include_current_uid' clause is used for calculating
+ * the current position of the cursor, inclusive of the
+ * current position.
+ */
+ if (include_current_uid)
+ g_string_append_c (string, '(');
+
+ /* Append the UID tie breaker */
+ ebsql_string_append_printf (
+ string,
+ "summary.uid %c %Q",
+ reverse ? '<' : '>',
+ state->last_uid);
+
+ if (include_current_uid)
+ ebsql_string_append_printf (
+ string,
+ " OR summary.uid = %Q)",
+ state->last_uid);
+
+ } else {
+
+ /* SPECIAL CASE: If we have a parially set cursor state, then we must
+ * report next results that are inclusive of the final qualifier.
+ *
+ * This allows one to set the cursor with the family name set to 'J'
+ * and include the results for contact's Mr & Miss 'J'.
+ */
+ gboolean include_exact_match =
+ (reverse == FALSE &&
+ ((i + 1 < cursor->n_sort_fields && state->values[i + 1] == NULL) ||
+ (i + 1 == cursor->n_sort_fields && state->last_uid == NULL)));
+
+ if (include_exact_match)
+ g_string_append_c (string, '(');
+
+ /* Append the final qualifier for this field */
+ ebsql_cursor_format_equality (ebsql, string,
+ cursor->sort_fields[i],
+ state->values[i],
+ GREATER_OR_LESS (cursor, i, reverse));
+
+ if (include_exact_match) {
+ g_string_append (string, " OR ");
+ ebsql_cursor_format_equality (ebsql, string,
+ cursor->sort_fields[i],
+ state->values[i], '=');
+ g_string_append_c (string, ')');
+ }
+ }
+
+ /* End qualifier */
+ g_string_append_c (string, ')');
+ }
+
+ return g_string_free (string, FALSE);
+}
+
+static gboolean
+cursor_count_total_locked (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ gint *total,
+ GError **error)
+{
+ GString *query;
+ gboolean success;
+
+ query = g_string_new (cursor->select_count);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Execute the query */
+ success = ebsql_exec (ebsql, query->str, get_count_cb, total, NULL, error);
+
+ g_string_free (query, TRUE);
+
+ return success;
+}
+
+static gboolean
+cursor_count_position_locked (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ gint *position,
+ GError **error)
+{
+ GString *query;
+ gboolean success;
+
+ query = g_string_new (cursor->select_count);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Add the cursor constraints (if any) */
+ if (cursor->state.values[0] != NULL) {
+ gchar *constraints = NULL;
+
+ if (!cursor->query)
+ g_string_append (query, " WHERE ");
+ else
+ g_string_append (query, " AND ");
+
+ /* Here we do a reverse query, we're looking for all the
+ * results leading up to the current cursor value, including
+ * the cursor value
+ */
+ constraints = ebsql_cursor_constraints (
+ ebsql, cursor, &(cursor->state), TRUE, TRUE);
+
+ g_string_append_c (query, '(');
+ g_string_append (query, constraints);
+ g_string_append_c (query, ')');
+
+ g_free (constraints);
+ }
+
+ /* Execute the query */
+ success = ebsql_exec (ebsql, query->str, get_count_cb, position, NULL, error);
+
+ g_string_free (query, TRUE);
+
+ return success;
+}
+
+/**********************************************************
+ * GObjectClass *
+ **********************************************************/
+static void
+e_book_sqlite_dispose (GObject *object)
+{
+ EBookSqlite *ebsql = E_BOOK_SQLITE (object);
+
+ ebsql_unregister_from_hash (ebsql);
+
+ /* Chain up to parent's dispose() method. */
+ G_OBJECT_CLASS (e_book_sqlite_parent_class)->dispose (object);
+}
+
+static void
+e_book_sqlite_finalize (GObject *object)
+{
+ EBookSqlite *ebsql = E_BOOK_SQLITE (object);
+ EBookSqlitePrivate *priv = ebsql->priv;
+
+ summary_fields_array_free (
+ priv->summary_fields,
+ priv->n_summary_fields);
+
+ g_free (priv->folderid);
+ g_free (priv->path);
+ g_free (priv->locale);
+ g_free (priv->region_code);
+
+ if (priv->collator)
+ e_collator_unref (priv->collator);
+
+ g_clear_object (&priv->source);
+
+ g_mutex_clear (&priv->lock);
+ g_mutex_clear (&priv->updates_lock);
+
+ if (priv->multi_deletes)
+ g_hash_table_destroy (priv->multi_deletes);
+
+ if (priv->multi_inserts)
+ g_hash_table_destroy (priv->multi_inserts);
+
+ if (priv->user_data && priv->user_data_destroy)
+ priv->user_data_destroy (priv->user_data);
+
+ sqlite3_finalize (priv->insert_stmt);
+ sqlite3_finalize (priv->replace_stmt);
+ sqlite3_close (priv->db);
+
+ EBSQL_NOTE (REF_COUNTS, g_printerr ("EBookSqlite finalized\n"));
+
+ /* Chain up to parent's finalize() method. */
+ G_OBJECT_CLASS (e_book_sqlite_parent_class)->finalize (object);
+}
+
+static void
+e_book_sqlite_constructed (GObject *object)
+{
+ /* Chain up to parent's constructed() method. */
+ G_OBJECT_CLASS (e_book_sqlite_parent_class)->constructed (object);
+
+ e_extensible_load_extensions (E_EXTENSIBLE (object));
+}
+
+static gboolean
+ebsql_signals_accumulator (GSignalInvocationHint *ihint,
+ GValue *return_accu,
+ const GValue *handler_return,
+ gpointer data)
+{
+ gboolean handler_result;
+
+ handler_result = g_value_get_boolean (handler_return);
+ g_value_set_boolean (return_accu, handler_result);
+
+ return handler_result;
+}
+
+static gboolean
+ebsql_before_insert_contact_default (EBookSqlite *ebsql,
+ gpointer db,
+ EContact *contact,
+ const gchar *extra,
+ gboolean replace,
+ GCancellable *cancellable,
+ GError **error)
+{
+ return TRUE;
+}
+
+static gboolean
+ebsql_before_remove_contact_default (EBookSqlite *ebsql,
+ gpointer db,
+ const gchar *contact_uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ return TRUE;
+}
+
+static void
+e_book_sqlite_class_init (EBookSqliteClass *class)
+{
+ GObjectClass *object_class;
+
+ g_type_class_add_private (class, sizeof (EBookSqlitePrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->dispose = e_book_sqlite_dispose;
+ object_class->finalize = e_book_sqlite_finalize;
+ object_class->constructed = e_book_sqlite_constructed;
+
+ class->before_insert_contact = ebsql_before_insert_contact_default;
+ class->before_remove_contact = ebsql_before_remove_contact_default;
+
+ /* Parse the EBSQL_DEBUG environment variable */
+ ebsql_init_debug ();
+
+ signals[BEFORE_INSERT_CONTACT] = g_signal_new (
+ "before-insert-contact",
+ G_OBJECT_CLASS_TYPE (class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (EBookSqliteClass, before_insert_contact),
+ ebsql_signals_accumulator,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 6,
+ G_TYPE_POINTER,
+ G_TYPE_OBJECT,
+ G_TYPE_STRING,
+ G_TYPE_BOOLEAN,
+ G_TYPE_OBJECT,
+ G_TYPE_POINTER);
+
+ signals[BEFORE_REMOVE_CONTACT] = g_signal_new (
+ "before-remove-contact",
+ G_OBJECT_CLASS_TYPE (class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (EBookSqliteClass, before_remove_contact),
+ ebsql_signals_accumulator,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 4,
+ G_TYPE_POINTER,
+ G_TYPE_STRING,
+ G_TYPE_OBJECT,
+ G_TYPE_POINTER);
+}
+
+static void
+e_book_sqlite_init (EBookSqlite *ebsql)
+{
+ ebsql->priv = E_BOOK_SQLITE_GET_PRIVATE (ebsql);
+
+ g_mutex_init (&ebsql->priv->lock);
+ g_mutex_init (&ebsql->priv->updates_lock);
+}
+
+/**********************************************************
+ * API *
+ **********************************************************/
+static EBookSqlite *
+ebsql_new_default (const gchar *path,
+ ESource *source,
+ EbSqlVCardCallback vcard_callback,
+ EbSqlChangeCallback change_callback,
+ gpointer user_data,
+ GDestroyNotify user_data_destroy,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookSqlite *ebsql;
+ GArray *summary_fields;
+ gint i;
+
+ /* Create the default summary structs */
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+ for (i = 0; i < G_N_ELEMENTS (default_summary_fields); i++)
+ summary_field_append (summary_fields, DEFAULT_FOLDER_ID, default_summary_fields[i], NULL);
+
+ /* Add the default index flags */
+ summary_fields_add_indexes (
+ summary_fields,
+ default_indexed_fields,
+ default_index_types,
+ G_N_ELEMENTS (default_indexed_fields));
+
+ ebsql = ebsql_new_internal (
+ path, source,
+ vcard_callback, change_callback,
+ user_data, user_data_destroy,
+ (SummaryField *) summary_fields->data,
+ summary_fields->len,
+ cancellable, error);
+
+ g_array_free (summary_fields, FALSE);
+
+ return ebsql;
+}
+
+/**
+ * e_book_sqlite_new:
+ * @path: location to load or create the new database
+ * @source: an optional #ESource, associated with the #EBookSqlite, or %NULL
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Creates a new #EBookSqlite with the default summary configuration.
+ *
+ * Aside from the manditory fields %E_CONTACT_UID, %E_CONTACT_REV,
+ * the default configuration stores the following fields for quick
+ * performance of searches: %E_CONTACT_FILE_AS, %E_CONTACT_NICKNAME,
+ * %E_CONTACT_FULL_NAME, %E_CONTACT_GIVEN_NAME, %E_CONTACT_FAMILY_NAME,
+ * %E_CONTACT_EMAIL, %E_CONTACT_TEL, %E_CONTACT_IS_LIST, %E_CONTACT_LIST_SHOW_ADDRESSES,
+ * and %E_CONTACT_WANTS_HTML.
+ *
+ * The fields %E_CONTACT_FULL_NAME and %E_CONTACT_EMAIL are configured
+ * to respond extra quickly with the %E_BOOK_INDEX_PREFIX index flag.
+ *
+ * The fields %E_CONTACT_FILE_AS, %E_CONTACT_FAMILY_NAME and
+ * %E_CONTACT_GIVEN_NAME are configured to perform well with
+ * the #EbSqlCursor interface, using the %E_BOOK_INDEX_SORT_KEY
+ * index flag.
+ *
+ * Returns: (transfer full): A reference to a #EBookSqlite
+ *
+ * Since: 3.12
+ **/
+EBookSqlite *
+e_book_sqlite_new (const gchar *path,
+ ESource *source,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (path && path[0], NULL);
+
+ return ebsql_new_default (path, source, NULL, NULL, NULL, NULL, cancellable, error);
+}
+
+/**
+ * e_book_sqlite_new_full:
+ * @path: location to load or create the new database
+ * @source: an optional #ESource, associated with the #EBookSqlite, or %NULL
+ * @setup: (allow-none): an #ESourceBackendSummarySetup describing how the summary should be setup, or %NULL to use the default
+ * @vcard_callback: (allow-none) (scope async) (closure user_data): A function to resolve vcards
+ * @change_callback: (allow-none) (scope async) (closure user_data): A function to catch notifications of vcard changes
+ * @user_data: (allow-none): callback user data
+ * @user_data_destroy: (allow-none): A function to free @user_data automatically when the created #EBookSqlite is destroyed.
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Opens or creates a new addressbook at @path.
+ *
+ * Like e_book_sqlite_new(), but allows configuration of which contact fields
+ * will be stored for quick reference in the summary. The configuration indicated by
+ * @setup will only be taken into account when initially creating the underlying table,
+ * further configurations will be ignored.
+ *
+ * The fields %E_CONTACT_UID and %E_CONTACT_REV are not optional,
+ * they will be stored in the summary regardless of this function's parameters.
+ * Only #EContactFields with the type #G_TYPE_STRING, #G_TYPE_BOOLEAN or
+ * #E_TYPE_CONTACT_ATTR_LIST are currently supported.
+ *
+ * If @vcard_callback is specified, then vcards will not be stored by functions
+ * such as e_book_sqlitedb_add_contact(). Instead @vcard_callback will be invoked
+ * at any time the created #EBookSqlite requires a vcard, either as a fallback
+ * for querying search expressions which cannot be satisfied with the summary
+ * fields, or when reporting results from searches.
+ *
+ * If any error occurs and %NULL is returned, then the passed @user_data will
+ * be automatically freed using the @user_data_destroy function, if specified.
+ *
+ * It is recommended to store all contact vcards in the #EBookSqlite addressbook
+ * if at all possible, however in some cases the vcards must be stored in some
+ * other storage.
+ *
+ * Returns: (transfer full): The newly created #EBookSqlite, or %NULL if opening or creating the addressbook failed.
+ *
+ * Since: 3.12
+ **/
+EBookSqlite *
+e_book_sqlite_new_full (const gchar *path,
+ ESource *source,
+ ESourceBackendSummarySetup *setup,
+ EbSqlVCardCallback vcard_callback,
+ EbSqlChangeCallback change_callback,
+ gpointer user_data,
+ GDestroyNotify user_data_destroy,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookSqlite *ebsql = NULL;
+ EContactField *fields;
+ EContactField *indexed_fields;
+ EBookIndexType *index_types = NULL;
+ gboolean had_error = FALSE;
+ GArray *summary_fields;
+ gint n_fields = 0, n_indexed_fields = 0, i;
+
+ g_return_val_if_fail (path && path[0], NULL);
+ g_return_val_if_fail (setup == NULL || E_IS_SOURCE_BACKEND_SUMMARY_SETUP (setup), NULL);
+
+ if (!setup)
+ return ebsql_new_default (
+ path,
+ source,
+ vcard_callback,
+ change_callback,
+ user_data,
+ user_data_destroy,
+ cancellable, error);
+
+ fields = e_source_backend_summary_setup_get_summary_fields (setup, &n_fields);
+ indexed_fields = e_source_backend_summary_setup_get_indexed_fields (setup, &index_types, &n_indexed_fields);
+
+ /* No specified summary fields indicates the default summary configuration should be used */
+ if (n_fields <= 0 || n_fields >= EBSQL_MAX_SUMMARY_FIELDS) {
+
+ if (n_fields)
+ g_warning (
+ "EBookSqlite refused to create addressbook with over %d summary fields",
+ EBSQL_MAX_SUMMARY_FIELDS);
+
+ ebsql = ebsql_new_default (
+ path,
+ source,
+ vcard_callback,
+ change_callback,
+ user_data,
+ user_data_destroy,
+ cancellable, error);
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+
+ return ebsql;
+ }
+
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+
+ /* Ensure the non-optional fields first */
+ summary_field_append (summary_fields, DEFAULT_FOLDER_ID, E_CONTACT_UID, error);
+ summary_field_append (summary_fields, DEFAULT_FOLDER_ID, E_CONTACT_REV, error);
+
+ for (i = 0; i < n_fields; i++) {
+ if (!summary_field_append (summary_fields, DEFAULT_FOLDER_ID, fields[i], error)) {
+ had_error = TRUE;
+ break;
+ }
+ }
+
+ if (had_error) {
+ gint n_sfields;
+ SummaryField *sfields;
+
+ /* Properly free the array */
+ n_sfields = summary_fields->len;
+ sfields = (SummaryField *) g_array_free (summary_fields, FALSE);
+ summary_fields_array_free (sfields, n_sfields);
+
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+
+ if (user_data && user_data_destroy)
+ user_data_destroy (user_data);
+
+ return NULL;
+ }
+
+ /* Add the 'indexed' flag to the SummaryField structs */
+ summary_fields_add_indexes (
+ summary_fields, indexed_fields, index_types, n_indexed_fields);
+
+ ebsql = ebsql_new_internal (
+ path, source,
+ vcard_callback, change_callback,
+ user_data, user_data_destroy,
+ (SummaryField *) summary_fields->data,
+ summary_fields->len,
+ cancellable, error);
+
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+ g_array_free (summary_fields, FALSE);
+
+ return ebsql;
+}
+
+/**
+ * e_book_sqlite_lock:
+ * @ebsql: An #EBookSqlite
+ * @lock_type: The #EbSqlLockType to acquire
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Obtains an exclusive lock on @ebsql and starts a transaction.
+ *
+ * This should be called if you need to access @ebsql multiple times while
+ * ensuring an atomic transaction. End this transaction with e_book_sqlite_unlock().
+ *
+ * If @cancellable is specified, then @ebsql will retain a reference to it until
+ * e_book_sqlite_unlock() is called. Any accesses to @ebsql with the lock held
+ * are expected to have the same @cancellable specified, or %NULL.
+ *
+ * <note><para>Aside from ensuring atomicity of transactions, this function will hold a mutex
+ * which will cause further calls to e_book_sqlite_lock() to block. If you are accessing
+ * @ebsql from multiple threads, then any interactions with @ebsql should be nested in calls
+ * to e_book_sqlite_lock() and e_book_sqlite_unlock().</para></note>
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_lock (EBookSqlite *ebsql,
+ EbSqlLockType lock_type,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->updates_lock);
+
+ /* Here, after obtaining the outer facing transaction lock, we need
+ * to assert that there is no cancellable already set */
+ if (ebsql->priv->cancel != NULL) {
+ /* This should never happen, if it does it's a bug
+ * in this code, not the calling code
+ */
+ g_warn_if_reached ();
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->updates_lock);
+ return FALSE;
+ }
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+
+ /* Here, after obtaining the regular lock, we need to assert that we are
+ * the toplevel transaction */
+ if (ebsql->priv->in_transaction != 0) {
+ g_warn_if_reached ();
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->updates_lock);
+ return FALSE;
+ }
+
+ success = ebsql_start_transaction (ebsql, lock_type, cancellable, error);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ /* If we failed to start the transaction, we don't hold the lock */
+ if (!success)
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->updates_lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_unlock:
+ * @ebsql: An #EBookSqlite
+ * @action: Which #EbSqlUnlockAction to take while unlocking
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Releases an exclusive on @ebsql and finishes a transaction previously
+ * started with e_book_sqlite_lock_updates().
+ *
+ * <note><para>If this fails, the lock on @ebsql is still released and @error will
+ * be set to indicate why the transaction or rollback failed.</para></note>
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_unlock (EBookSqlite *ebsql,
+ EbSqlUnlockAction action,
+ GError **error)
+{
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+
+ switch (action) {
+ case EBSQL_UNLOCK_NONE:
+ case EBSQL_UNLOCK_COMMIT:
+ success = ebsql_commit_transaction (ebsql, error);
+ break;
+ case EBSQL_UNLOCK_ROLLBACK:
+ success = ebsql_rollback_transaction (ebsql, error);
+ break;
+ }
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->updates_lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_ref_collator:
+ * @ebsql: An #EBookSqlite
+ *
+ * References the currently active #ECollator for @ebsql,
+ * use e_collator_unref() when finished using the returned collator.
+ *
+ * Note that the active collator will change with the active locale setting.
+ *
+ * Returns: (transfer full): A reference to the active collator.
+ *
+ * Since: 3.12
+ */
+ECollator *
+e_book_sqlite_ref_collator (EBookSqlite *ebsql)
+{
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), NULL);
+
+ return e_collator_ref (ebsql->priv->collator);
+}
+
+/**
+ * e_book_sqlite_ref_source:
+ * @ebsql: An #EBookSqlite
+ *
+ * References the #ESource to which @ebsql is paired,
+ * use g_object_unref() when finished using the source.
+ * It can be %NULL in some cases, like when running tests.
+ *
+ * Returns: (transfer full): A reference to the #ESource to which @ebsql
+ * is paired, or %NULL.
+ *
+ * Since: 3.16
+*/
+ESource *
+e_book_sqlite_ref_source (EBookSqlite *ebsql)
+{
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), NULL);
+
+ if (!ebsql->priv->source)
+ return NULL;
+
+ return g_object_ref (ebsql->priv->source);
+}
+
+/**
+ * e_book_sqlitedb_add_contact:
+ * @ebsql: An #EBookSqlite
+ * @contact: EContact to be added
+ * @extra: Extra data to store in association with this contact
+ * @replace: Whether this contact should replace another contact with the same UID.
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * This is a convenience wrapper for e_book_sqlite_add_contacts(),
+ * which is the preferred means to add or modify multiple contacts when possible.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_add_contact (EBookSqlite *ebsql,
+ EContact *contact,
+ const gchar *extra,
+ gboolean replace,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList l;
+ GSList el;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ l.data = contact;
+ l.next = NULL;
+
+ el.data = (gpointer) extra;
+ el.next = NULL;
+
+ return e_book_sqlite_add_contacts (ebsql, &l, &el, replace, cancellable, error);
+}
+
+/**
+ * e_book_sqlite_new_contacts:
+ * @ebsql: An #EBookSqlite
+ * @contacts: (element-type EContact): A list of contacts to add to @ebsql
+ * @extra: (allow-none) (element-type utf8): A list of extra data to store in association with this contact
+ * @replace: Whether this contact should replace another contact with the same UID.
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Adds or replaces contacts in @ebsql. If @replace_existing is specified then existing
+ * contacts with the same UID will be replaced, otherwise adding an existing contact
+ * will return an error.
+ *
+ * If @extra is specified, it must have an equal length as the @contacts list. Each element
+ * from the @extra list will be stored in association with it's corresponding contact
+ * in the @contacts list.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_add_contacts (EBookSqlite *ebsql,
+ GSList *contacts,
+ GSList *extra,
+ gboolean replace,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *l, *ll;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (contacts != NULL, FALSE);
+ g_return_val_if_fail (extra == NULL ||
+ g_slist_length (extra) == g_slist_length (contacts), FALSE);
+
+ EBSQL_LOCK_OR_RETURN (ebsql, cancellable, FALSE);
+
+ if (!ebsql_start_transaction (ebsql, EBSQL_LOCK_WRITE, cancellable, error)) {
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return FALSE;
+ }
+
+ for (l = contacts, ll = extra;
+ success && l != NULL;
+ l = l->next, ll = ll ? ll->next : NULL) {
+ EContact *contact = (EContact *) l->data;
+ const gchar *extra_data = NULL;
+
+ if (ll)
+ extra_data = (const gchar *) ll->data;
+
+ g_signal_emit (ebsql,
+ signals[BEFORE_INSERT_CONTACT],
+ 0,
+ ebsql->priv->db,
+ contact, extra_data,
+ replace,
+ cancellable, error,
+ &success);
+ if (!success)
+ break;
+
+ success = ebsql_insert_contact (
+ ebsql,
+ EBSQL_CHANGE_CONTACT_ADDED,
+ contact, NULL, extra_data,
+ replace, error);
+ }
+
+ if (success)
+ success = ebsql_commit_transaction (ebsql, error);
+ else
+ /* The GError is already set. */
+ ebsql_rollback_transaction (ebsql, NULL);
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_remove_contact:
+ * @ebsql: An #EBookSqlite
+ * @uid: the uid of the contact to remove
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Removes the contact indicated by @uid from @ebsql.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_remove_contact (EBookSqlite *ebsql,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList l;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ l.data = (gchar *) uid; /* Won't modify it, I promise :) */
+ l.next = NULL;
+
+ return e_book_sqlite_remove_contacts (
+ ebsql, &l, cancellable, error);
+}
+
+static gchar *
+generate_delete_stmt (const gchar *table,
+ GSList *uids)
+{
+ GString *str = g_string_new (NULL);
+ GSList *l;
+
+ ebsql_string_append_printf (str, "DELETE FROM %Q WHERE uid IN (", table);
+
+ for (l = uids; l; l = l->next) {
+ const gchar *uid = (const gchar *) l->data;
+
+ /* First uid with no comma */
+ if (l != uids)
+ g_string_append_printf (str, ", ");
+
+ ebsql_string_append_printf (str, "%Q", uid);
+ }
+
+ g_string_append_c (str, ')');
+
+ return g_string_free (str, FALSE);
+}
+
+/**
+ * e_book_sqlite_remove_contacts:
+ * @ebsql: An #EBookSqlite
+ * @uids: a #GSList of uids indicating which contacts to remove
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Removes the contacts indicated by @uids from @ebsql.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_remove_contacts (EBookSqlite *ebsql,
+ GSList *uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success = TRUE;
+ gint i;
+ gchar *stmt;
+ const gchar *contact_uid;
+ GSList *l = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uids != NULL, FALSE);
+
+ EBSQL_LOCK_OR_RETURN (ebsql, cancellable, FALSE);
+
+ if (!ebsql_start_transaction (ebsql, EBSQL_LOCK_WRITE, cancellable, error)) {
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return FALSE;
+ }
+
+ for (l = uids; success && l; l = l->next) {
+ contact_uid = (const gchar *) l->data;
+ g_signal_emit (ebsql,
+ signals[BEFORE_REMOVE_CONTACT],
+ 0,
+ ebsql->priv->db,
+ contact_uid,
+ cancellable, error,
+ &success);
+ }
+
+ /* Delete data from the auxiliary tables first */
+ for (i = 0; success && i < ebsql->priv->n_summary_fields; i++) {
+ SummaryField *field = &(ebsql->priv->summary_fields[i]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ stmt = generate_delete_stmt (field->aux_table, uids);
+ success = ebsql_exec (ebsql, stmt, NULL, NULL, NULL, error);
+ g_free (stmt);
+ }
+
+ /* Now delete the entry from the main contacts */
+ if (success) {
+ stmt = generate_delete_stmt (ebsql->priv->folderid, uids);
+ success = ebsql_exec (ebsql, stmt, NULL, NULL, NULL, error);
+ g_free (stmt);
+ }
+
+ if (success)
+ success = ebsql_commit_transaction (ebsql, error);
+ else
+ /* The GError is already set. */
+ ebsql_rollback_transaction (ebsql, NULL);
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_has_contact:
+ * @ebsql: An #EBookSqlite
+ * @uid: The uid of the contact to check for
+ * @exists: (out): Return location to store whether the contact exists.
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Checks if a contact bearing the UID indicated by @uid is stored in @ebsql.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_has_contact (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean *exists,
+ GError **error)
+{
+ gboolean local_exists = FALSE;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (exists != NULL, FALSE);
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+ success = ebsql_exec_printf (
+ ebsql,
+ "SELECT uid FROM %Q WHERE uid = %Q",
+ get_exists_cb, &local_exists, NULL, error,
+ ebsql->priv->folderid, uid);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ *exists = local_exists;
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_get_contact:
+ * @ebsql: An #EBookSqlite
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @ret_contact: (out) (transfer full): Return location to store the fetched contact
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetch the #EContact specified by @uid in @ebsql.
+ *
+ * If @meta_contact is specified, then a shallow #EContact will be created
+ * holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_get_contact (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean meta_contact,
+ EContact **ret_contact,
+ GError **error)
+{
+ gboolean success = FALSE;
+ gchar *vcard = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (ret_contact != NULL && *ret_contact == NULL, FALSE);
+
+ success = e_book_sqlite_get_vcard (
+ ebsql, uid, meta_contact, &vcard, error);
+
+ if (success && vcard) {
+ *ret_contact = e_contact_new_from_vcard_with_uid (vcard, uid);
+ g_free (vcard);
+ }
+
+ return success;
+}
+
+/**
+ * ebsql_get_contact_unlocked:
+ * @ebsql: An #EBookSqlite
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @contact: (out) (transfer full): Return location to store the fetched contact
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetch the #EContact specified by @uid in @ebsql without locking internal mutex.
+ *
+ * If @meta_contact is specified, then a shallow #EContact will be created
+ * holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.16
+ **/
+gboolean
+ebsql_get_contact_unlocked (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean meta_contact,
+ EContact **contact,
+ GError **error)
+{
+ gboolean success = FALSE;
+ gchar *vcard = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (contact != NULL && *contact == NULL, FALSE);
+
+ success = ebsql_get_vcard_unlocked (ebsql,
+ uid,
+ meta_contact,
+ &vcard,
+ error);
+
+ if (success && vcard) {
+ *contact = e_contact_new_from_vcard_with_uid (vcard, uid);
+ g_free (vcard);
+ }
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_get_vcard:
+ * @ebsql: An #EBookSqlite
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @ret_vcard: (out) (transfer full): Return location to store the fetched vcard string
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetch a vcard string for @uid in @ebsql.
+ *
+ * If @meta_contact is specified, then a shallow vcard representation will be
+ * created holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_get_vcard (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean meta_contact,
+ gchar **ret_vcard,
+ GError **error)
+{
+ gboolean success = FALSE;
+ gchar *vcard = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (ret_vcard != NULL && *ret_vcard == NULL, FALSE);
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+
+ /* Try constructing contacts from only UID/REV first if that's requested */
+ if (meta_contact) {
+ GSList *vcards = NULL;
+
+ success = ebsql_exec_printf (
+ ebsql, "SELECT summary.uid, summary.Rev FROM %Q AS summary WHERE uid = %Q",
+ collect_lean_results_cb, &vcards, NULL, error,
+ ebsql->priv->folderid, uid);
+
+ if (vcards) {
+ EbSqlSearchData *search_data = (EbSqlSearchData *) vcards->data;
+
+ vcard = search_data->vcard;
+ search_data->vcard = NULL;
+
+ g_slist_free_full (vcards, (GDestroyNotify) e_book_sqlite_search_data_free);
+ vcards = NULL;
+ }
+
+ } else {
+ success = ebsql_exec_printf (
+ ebsql, "SELECT %s FROM %Q AS summary WHERE summary.uid = %Q",
+ get_string_cb, &vcard, NULL, error,
+ EBSQL_VCARD_FRAGMENT (ebsql), ebsql->priv->folderid, uid);
+ }
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ *ret_vcard = vcard;
+
+ if (success && !vcard) {
+ EBSQL_SET_ERROR (
+ error,
+ E_BOOK_SQLITE_ERROR_CONTACT_NOT_FOUND,
+ _("Contact '%s' not found"), uid);
+ success = FALSE;
+ }
+
+ return success;
+}
+
+/**
+ * ebsql_get_vcard_unlocked:
+ * @ebsql: An #EBookSqlite
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @ret_vcard: (out) (transfer full): Return location to store the fetched vcard string
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetch a vcard string for @uid in @ebsql without locking internal mutex.
+ *
+ * If @meta_contact is specified, then a shallow vcard representation will be
+ * created holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.16
+ **/
+gboolean
+ebsql_get_vcard_unlocked (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean meta_contact,
+ gchar **ret_vcard,
+ GError **error)
+{
+ gboolean success = FALSE;
+ gchar *vcard = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (ret_vcard != NULL && *ret_vcard == NULL, FALSE);
+
+ /* Try constructing contacts from only UID/REV first if that's requested */
+ if (meta_contact) {
+ GSList *vcards = NULL;
+
+ success = ebsql_exec_printf (
+ ebsql, "SELECT summary.uid, summary.Rev FROM %Q AS summary WHERE uid = %Q",
+ collect_lean_results_cb, &vcards, NULL, error,
+ ebsql->priv->folderid, uid);
+
+ if (vcards) {
+ EbSqlSearchData *search_data = (EbSqlSearchData *) vcards->data;
+
+ vcard = search_data->vcard;
+ search_data->vcard = NULL;
+
+ g_slist_free_full (vcards, (GDestroyNotify) e_book_sqlite_search_data_free);
+ vcards = NULL;
+ }
+
+ } else {
+ success = ebsql_exec_printf (
+ ebsql, "SELECT %s FROM %Q AS summary WHERE summary.uid = %Q",
+ get_string_cb, &vcard, NULL, error,
+ EBSQL_VCARD_FRAGMENT (ebsql), ebsql->priv->folderid, uid);
+ }
+
+ *ret_vcard = vcard;
+
+ if (success && !vcard) {
+ EBSQL_SET_ERROR (error,
+ E_BOOK_SQLITE_ERROR_CONTACT_NOT_FOUND,
+ _("Contact '%s' not found"), uid);
+ success = FALSE;
+ }
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_set_contact_extra:
+ * @ebsql: An #EBookSqlite
+ * @uid: The uid of the contact to set the extra data for
+ * @extra: (allow-none): The extra data to set
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Sets or replaces the extra data associated with @uid.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_set_contact_extra (EBookSqlite *ebsql,
+ const gchar *uid,
+ const gchar *extra,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+ success = ebsql_exec_printf (
+ ebsql, "UPDATE %Q SET bdata = %Q WHERE uid = %Q",
+ NULL, NULL, NULL, error,
+ ebsql->priv->folderid, uid);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_get_contact_extra:
+ * @ebsql: An #EBookSqlite
+ * @uid: The uid of the contact to fetch the extra data for
+ * @ret_extra: (out) (transfer full): Return location to store the extra data
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetches the extra data previously set for @uid, either with
+ * e_book_sqlite_set_contact_extra() or when adding contacts.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_get_contact_extra (EBookSqlite *ebsql,
+ const gchar *uid,
+ gchar **ret_extra,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (ret_extra != NULL && *ret_extra == NULL, FALSE);
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+ success = ebsql_exec_printf (
+ ebsql, "SELECT bdata FROM %Q WHERE uid = %Q",
+ get_string_cb, ret_extra, NULL, error,
+ ebsql->priv->folderid, uid);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * ebsql_get_contact_extra_unlocked:
+ * @ebsql: An #EBookSqlite
+ * @uid: The uid of the contact to fetch the extra data for
+ * @ret_extra: (out) (transfer full): Return location to store the extra data
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetches the extra data previously set for @uid, either with
+ * e_book_sqlite_set_contact_extra() or when adding contacts,
+ * without locking internal mutex.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.16
+ **/
+gboolean
+ebsql_get_contact_extra_unlocked (EBookSqlite *ebsql,
+ const gchar *uid,
+ gchar **ret_extra,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (ret_extra != NULL && *ret_extra == NULL, FALSE);
+
+ success = ebsql_exec_printf (
+ ebsql, "SELECT bdata FROM %Q WHERE uid = %Q",
+ get_string_cb, ret_extra, NULL, error,
+ ebsql->priv->folderid, uid);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_search:
+ * @ebsql: An #EBookSqlite
+ * @sexp: (allow-none): search expression; use %NULL or an empty string to list all stored contacts.
+ * @meta_contacts: Whether entire contacts are desired, or only the metadata
+ * @ret_list: (out) (transfer full) (element-type EbSqlSearchData): Return location
+ * to store a #GSList of #EbSqlSearchData structures
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Searches @ebsql for contacts matching the search expression indicated by @sexp.
+ *
+ * When @sexp refers only to #EContactFields configured in the summary of @ebsql,
+ * the search should always be quick, when searching for other #EContactFields
+ * a fallback will be used, possibly invoking any #EbSqlVCardCallback which
+ * may have been passed to e_book_sqlite_new_full().
+ *
+ * The returned @ret_list list should be freed with g_slist_free()
+ * and all elements freed with e_book_sqlite_search_data_free().
+ *
+ * If @meta_contact is specified, then shallow vcard representations will be
+ * created holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_search (EBookSqlite *ebsql,
+ const gchar *sexp,
+ gboolean meta_contacts,
+ GSList **ret_list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (ret_list != NULL && *ret_list == NULL, FALSE);
+
+ EBSQL_LOCK_OR_RETURN (ebsql, cancellable, FALSE);
+ success = ebsql_search_query (
+ ebsql, sexp,
+ meta_contacts ?
+ SEARCH_UID_AND_REV : SEARCH_FULL,
+ ret_list,
+ cancellable,
+ error);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_search_uids:
+ * @ebsql: An #EBookSqlite
+ * @sexp: (allow-none): search expression; use %NULL or an empty string to get all stored contacts.
+ * @ret_list: (out) (transfer full): Return location to store a #GSList of contact uids
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Similar to e_book_sqlitedb_search(), but fetches only a list of contact UIDs.
+ *
+ * The returned @ret_list list should be freed with g_slist_free() and all
+ * elements freed with g_free().
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_search_uids (EBookSqlite *ebsql,
+ const gchar *sexp,
+ GSList **ret_list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (ret_list != NULL && *ret_list == NULL, FALSE);
+
+ EBSQL_LOCK_OR_RETURN (ebsql, cancellable, FALSE);
+ success = ebsql_search_query (ebsql, sexp, SEARCH_UID, ret_list, cancellable, error);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_get_key_value:
+ * @ebsql: An #EBookSqlite
+ * @key: The key to fetch a value for
+ * @value: (out) (transfer full): A return location to store the value for @key
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Fetches the value for @key and stores it in @value
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_get_key_value (EBookSqlite *ebsql,
+ const gchar *key,
+ gchar **value,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (key != NULL, FALSE);
+ g_return_val_if_fail (value != NULL && *value == NULL, FALSE);
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+ success = ebsql_exec_printf (
+ ebsql,
+ "SELECT value FROM keys WHERE folder_id = %Q AND key = %Q",
+ get_string_cb, value, NULL, error,
+ ebsql->priv->folderid, key);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_set_key_value:
+ * @ebsql: An #EBookSqlite
+ * @key: The key to fetch a value for
+ * @value: The new value for @key
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * Sets the value for @key to be @value
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_set_key_value (EBookSqlite *ebsql,
+ const gchar *key,
+ const gchar *value,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (key != NULL, FALSE);
+ g_return_val_if_fail (value != NULL, FALSE);
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+ success = ebsql_exec_printf (
+ ebsql, "INSERT or REPLACE INTO keys (key, value, folder_id) values (%Q, %Q, %Q)",
+ NULL, NULL, NULL, error,
+ key, value, ebsql->priv->folderid);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_get_key_value_int:
+ * @ebsql: An #EBookSqlite
+ * @key: The key to fetch a value for
+ * @value: (out): A return location to store the value for @key
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * A convenience function to fetch the value of @key as an integer.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_get_key_value_int (EBookSqlite *ebsql,
+ const gchar *key,
+ gint *value,
+ GError **error)
+{
+ gboolean success;
+ gchar *str_value = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (key != NULL, FALSE);
+ g_return_val_if_fail (value != NULL, FALSE);
+
+ success = e_book_sqlite_get_key_value (ebsql, key, &str_value, error);
+
+ if (success) {
+
+ if (str_value)
+ *value = g_ascii_strtoll (str_value, NULL, 10);
+ else
+ *value = 0;
+
+ g_free (str_value);
+ }
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_set_key_value_int:
+ * @ebsql: An #EBookSqlite
+ * @key: The key to fetch a value for
+ * @value: The new value for @key
+ * @error: (allow-none): A location to store any error that may have occurred.
+ *
+ * A convenience function to set the value of @key as an integer.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_book_sqlite_set_key_value_int (EBookSqlite *ebsql,
+ const gchar *key,
+ gint value,
+ GError **error)
+{
+ gboolean success;
+ gchar *str_value = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (key != NULL, FALSE);
+
+ str_value = g_strdup_printf ("%d", value);
+ success = e_book_sqlite_set_key_value (
+ ebsql, key, str_value, error);
+ g_free (str_value);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_search_data_free:
+ * @data: An #EbSqlSearchData
+ *
+ * Frees an #EbSqlSearchData
+ *
+ * Since: 3.12
+ **/
+void
+e_book_sqlite_search_data_free (EbSqlSearchData *data)
+{
+ if (data) {
+ g_free (data->uid);
+ g_free (data->vcard);
+ g_free (data->extra);
+ g_slice_free (EbSqlSearchData, data);
+ }
+}
+
+/**
+ * e_book_sqlite_set_locale:
+ * @ebsql: An #EBookSqlite
+ * @lc_collate: The new locale for the addressbook
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: A location to store any error that may have occurred
+ *
+ * Relocalizes any locale specific data in the specified
+ * new @lc_collate locale.
+ *
+ * The @lc_collate locale setting is stored and remembered on
+ * subsequent accesses of the addressbook, changing the locale
+ * will store the new locale and will modify sort keys and any
+ * locale specific data in the addressbook.
+ *
+ * As a side effect, it's possible that changing the locale
+ * will cause stored vcards to change. Notifications for
+ * these changes can be caught with the #EbSqlVCardCallback
+ * provided to e_book_sqlite_new_full().
+ *
+ * Returns: Whether the new locale was successfully set.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_book_sqlite_set_locale (EBookSqlite *ebsql,
+ const gchar *lc_collate,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+ gchar *stored_lc_collate = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+
+ EBSQL_LOCK_OR_RETURN (ebsql, cancellable, FALSE);
+
+ if (!ebsql_start_transaction (ebsql, EBSQL_LOCK_WRITE, cancellable, error)) {
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return FALSE;
+ }
+
+ success = ebsql_set_locale_internal (ebsql, lc_collate, error);
+
+ if (success)
+ success = ebsql_exec_printf (
+ ebsql, "SELECT lc_collate FROM folders WHERE folder_id = %Q",
+ get_string_cb, &stored_lc_collate, NULL, error,
+ ebsql->priv->folderid);
+
+ if (success && g_strcmp0 (stored_lc_collate, lc_collate) != 0)
+ success = ebsql_upgrade (ebsql, EBSQL_CHANGE_LOCALE_CHANGED, error);
+
+ /* If for some reason we failed, then reset the collator to use the old locale */
+ if (!success && stored_lc_collate && stored_lc_collate[0])
+ ebsql_set_locale_internal (ebsql, stored_lc_collate, NULL);
+
+ if (success)
+ success = ebsql_commit_transaction (ebsql, error);
+ else
+ /* The GError is already set. */
+ ebsql_rollback_transaction (ebsql, NULL);
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ g_free (stored_lc_collate);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_get_locale:
+ * @ebsql: An #EBookSqlite
+ * @locale_out: (out) (transfer full): The location to return the current locale
+ * @error: A location to store any error that may have occurred
+ *
+ * Fetches the current locale setting for the address-book.
+ *
+ * Upon success, @lc_collate_out will hold the returned locale setting,
+ * otherwise %FALSE will be returned and @error will be updated accordingly.
+ *
+ * Returns: Whether the locale was successfully fetched.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_book_sqlite_get_locale (EBookSqlite *ebsql,
+ gchar **locale_out,
+ GError **error)
+{
+ gboolean success;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (locale_out != NULL && *locale_out == NULL, FALSE);
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+
+ success = ebsql_exec_printf (
+ ebsql, "SELECT lc_collate FROM folders WHERE folder_id = %Q",
+ get_string_cb, locale_out, NULL, error,
+ ebsql->priv->folderid);
+
+ if (*locale_out == NULL) {
+
+ /* This can't realistically happen, if it does we
+ * should warn about it in stdout */
+ g_warning ("EBookSqlite has no active locale in the database");
+
+ *locale_out = g_strdup (ebsql->priv->locale);
+ }
+
+ if (success && !ebsql_set_locale_internal (ebsql, *locale_out, &local_error)) {
+ g_warning ("Error loading new locale: %s", local_error->message);
+ g_clear_error (&local_error);
+ }
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_cursor_new:
+ * @ebsql: An #EBookSqlite
+ * @sexp: search expression; use NULL or an empty string to get all stored contacts.
+ * @sort_fields: (array length=n_sort_fields): An array of #EContactFields as sort keys in order of priority
+ * @sort_types: (array length=n_sort_fields): An array of #EBookCursorSortTypes, one for each field in @sort_fields
+ * @n_sort_fields: The number of fields to sort results by.
+ * @error: A return location to store any error that might be reported.
+ *
+ * Creates a new #EbSqlCursor.
+ *
+ * The cursor should be freed with e_book_sqlite_cursor_free().
+ *
+ * Returns: (transfer full): A newly created #EbSqlCursor
+ *
+ * Since: 3.12
+ */
+EbSqlCursor *
+e_book_sqlite_cursor_new (EBookSqlite *ebsql,
+ const gchar *sexp,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ GError **error)
+{
+ EbSqlCursor *cursor;
+ gint i;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), NULL);
+
+ /* We don't like '\0' sexps, prefer NULL */
+ if (sexp && !sexp[0])
+ sexp = NULL;
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+
+ /* Need one sort key ... */
+ if (n_sort_fields == 0) {
+ EBSQL_SET_ERROR_LITERAL (
+ error, E_BOOK_SQLITE_ERROR_INVALID_QUERY,
+ _("At least one sort field must be specified to use an EbSqlCursor"));
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return NULL;
+ }
+
+ /* We only support string fields to sort the cursor */
+ for (i = 0; i < n_sort_fields; i++) {
+ EBSQL_NOTE (
+ CURSOR,
+ g_printerr (
+ "Building cursor to sort '%s' in '%s' order\n",
+ e_contact_field_name (sort_fields[i]),
+ sort_types[i] == E_BOOK_CURSOR_SORT_ASCENDING ?
+ "ascending" : "descending"));
+
+ if (e_contact_field_type (sort_fields[i]) != G_TYPE_STRING) {
+ EBSQL_SET_ERROR_LITERAL (
+ error, E_BOOK_SQLITE_ERROR_INVALID_QUERY,
+ _("Cannot sort by a field that is not a string type"));
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return NULL;
+ }
+ }
+
+ /* Now we need to create the cursor instance before setting up the query
+ * (not really true, but more convenient that way).
+ */
+ cursor = ebsql_cursor_new (ebsql, sexp, sort_fields, sort_types, n_sort_fields);
+
+ /* Setup the cursor's query expression which might fail */
+ if (!ebsql_cursor_setup_query (ebsql, cursor, sexp, error)) {
+ ebsql_cursor_free (cursor);
+ cursor = NULL;
+ }
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ EBSQL_NOTE (
+ CURSOR,
+ g_printerr (
+ "%s cursor with search expression '%s'\n",
+ cursor ? "Successfully created" : "Failed to create",
+ sexp));
+
+ return cursor;
+}
+
+/**
+ * e_book_sqlite_cursor_free:
+ * @ebsql: An #EBookSqlite
+ * @cursor: The #EbSqlCursor to free
+ *
+ * Frees @cursor.
+ *
+ * Since: 3.12
+ */
+void
+e_book_sqlite_cursor_free (EBookSqlite *ebsql,
+ EbSqlCursor *cursor)
+{
+ g_return_if_fail (E_IS_BOOK_SQLITE (ebsql));
+
+ ebsql_cursor_free (cursor);
+}
+
+typedef struct {
+ GSList *results;
+ gchar *alloc_vcard;
+ const gchar *last_vcard;
+
+ gboolean collect_results;
+ gint n_results;
+} CursorCollectData;
+
+static gint
+collect_results_for_cursor_cb (gpointer ref,
+ gint ncol,
+ gchar **cols,
+ gchar **names)
+{
+ CursorCollectData *data = ref;
+
+ if (data->collect_results) {
+ EbSqlSearchData *search_data;
+
+ search_data = search_data_from_results (ncol, cols, names);
+
+ data->results = g_slist_prepend (data->results, search_data);
+
+ data->last_vcard = search_data->vcard;
+ } else {
+ g_free (data->alloc_vcard);
+ data->alloc_vcard = g_strdup (cols[1]);
+
+ data->last_vcard = data->alloc_vcard;
+ }
+
+ data->n_results++;
+
+ return 0;
+}
+
+/**
+ * e_book_sqlite_cursor_step:
+ * @ebsql: An #EBookSqlite
+ * @cursor: The #EbSqlCursor to use
+ * @flags: The #EbSqlCursorStepFlags for this step
+ * @origin: The #EbSqlCursorOrigin from whence to step
+ * @count: A positive or negative amount of contacts to try and fetch
+ * @results: (out) (allow-none) (element-type EbSqlSearchData) (transfer full):
+ * A return location to store the results, or %NULL if %EBSQL_CURSOR_STEP_FETCH is not specified in %flags.
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: A return location to store any error that might be reported.
+ *
+ * Steps @cursor through it's sorted query by a maximum of @count contacts
+ * starting from @origin.
+ *
+ * If @count is negative, then the cursor will move through the list in reverse.
+ *
+ * If @cursor reaches the beginning or end of the query results, then the
+ * returned list might not contain the amount of desired contacts, or might
+ * return no results if the cursor currently points to the last contact.
+ * Reaching the end of the list is not considered an error condition. Attempts
+ * to step beyond the end of the list after having reached the end of the list
+ * will however trigger an %E_BOOK_SQLITE_ERROR_END_OF_LIST error.
+ *
+ * If %EBSQL_CURSOR_STEP_FETCH is specified in %flags, a pointer to
+ * a %NULL #GSList pointer should be provided for the @results parameter.
+ *
+ * The result list will be stored to @results and should be freed with g_slist_free()
+ * and all elements freed with e_book_sqlite_search_data_free().
+ *
+ * Returns: The number of contacts traversed if successful, otherwise -1 is
+ * returned and @error is set.
+ *
+ * Since: 3.12
+ */
+gint
+e_book_sqlite_cursor_step (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ EbSqlCursorStepFlags flags,
+ EbSqlCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GCancellable *cancellable,
+ GError **error)
+{
+ CursorCollectData data = { NULL, NULL, NULL, FALSE, 0 };
+ CursorState *state;
+ GString *query;
+ gboolean success;
+ EbSqlCursorOrigin try_position;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), -1);
+ g_return_val_if_fail (cursor != NULL, -1);
+ g_return_val_if_fail ((flags & EBSQL_CURSOR_STEP_FETCH) == 0 ||
+ (results != NULL && *results == NULL), -1);
+
+ /* Lock and check cancellable early */
+ EBSQL_LOCK_OR_RETURN (ebsql, cancellable, -1);
+
+ EBSQL_NOTE (
+ CURSOR,
+ g_printerr (
+ "Cursor requested to step by %d with origin %s will move: %s will fetch: %s\n",
+ count, ebsql_origin_str (origin),
+ (flags & EBSQL_CURSOR_STEP_MOVE) ? "yes" : "no",
+ (flags & EBSQL_CURSOR_STEP_FETCH) ? "yes" : "no"));
+
+ /* Check if this step should result in an end of list error first */
+ try_position = cursor->state.position;
+ if (origin != EBSQL_CURSOR_ORIGIN_CURRENT)
+ try_position = origin;
+
+ /* Report errors for requests to run off the end of the list */
+ if (try_position == EBSQL_CURSOR_ORIGIN_BEGIN && count < 0) {
+ EBSQL_SET_ERROR_LITERAL (
+ error, E_BOOK_SQLITE_ERROR_END_OF_LIST,
+ _("Tried to step a cursor in reverse, "
+ "but cursor is already at the beginning of the contact list"));
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return -1;
+ } else if (try_position == EBSQL_CURSOR_ORIGIN_END && count > 0) {
+ EBSQL_SET_ERROR_LITERAL (
+ error, E_BOOK_SQLITE_ERROR_END_OF_LIST,
+ _("Tried to step a cursor forwards, "
+ "but cursor is already at the end of the contact list"));
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return -1;
+ }
+
+ /* Nothing to do, silently return */
+ if (count == 0 && try_position == EBSQL_CURSOR_ORIGIN_CURRENT) {
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return 0;
+ }
+
+ /* If we're not going to modify the position, just use
+ * a copy of the current cursor state.
+ */
+ if ((flags & EBSQL_CURSOR_STEP_MOVE) != 0)
+ state = &(cursor->state);
+ else
+ state = cursor_state_copy (cursor, &(cursor->state));
+
+ /* Every query starts with the STATE_CURRENT position, first
+ * fix up the cursor state according to 'origin'
+ */
+ switch (origin) {
+ case EBSQL_CURSOR_ORIGIN_CURRENT:
+ /* Do nothing, normal operation */
+ break;
+
+ case EBSQL_CURSOR_ORIGIN_BEGIN:
+ case EBSQL_CURSOR_ORIGIN_END:
+
+ /* Prepare the state before executing the query */
+ cursor_state_clear (cursor, state, origin);
+ break;
+ }
+
+ /* If count is 0 then there is no need to run any
+ * query, however it can be useful if you just want
+ * to move the cursor to the beginning or ending of
+ * the list.
+ */
+ if (count == 0) {
+
+ /* Free the state copy if need be */
+ if ((flags & EBSQL_CURSOR_STEP_MOVE) == 0)
+ cursor_state_free (cursor, state);
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return 0;
+ }
+
+ query = g_string_new (cursor->select_vcards);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Add the cursor constraints (if any) */
+ if (state->values[0] != NULL) {
+ gchar *constraints = NULL;
+
+ if (!cursor->query)
+ g_string_append (query, " WHERE ");
+ else
+ g_string_append (query, " AND ");
+
+ constraints = ebsql_cursor_constraints (
+ ebsql, cursor, state, count < 0, FALSE);
+
+ g_string_append_c (query, '(');
+ g_string_append (query, constraints);
+ g_string_append_c (query, ')');
+
+ g_free (constraints);
+ }
+
+ /* Add the sort order */
+ g_string_append_c (query, ' ');
+ if (count > 0)
+ g_string_append (query, cursor->order);
+ else
+ g_string_append (query, cursor->reverse_order);
+
+ /* Add the limit */
+ g_string_append_printf (query, " LIMIT %d", ABS (count));
+
+ /* Specify whether we really want results or not */
+ data.collect_results = (flags & EBSQL_CURSOR_STEP_FETCH) != 0;
+
+ /* Execute the query */
+ success = ebsql_exec (
+ ebsql, query->str,
+ collect_results_for_cursor_cb, &data,
+ cancellable, error);
+
+ /* Lock was obtained above */
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ g_string_free (query, TRUE);
+
+ /* If there was no error, update the internal cursor state */
+ if (success) {
+
+ if (data.n_results < ABS (count)) {
+
+ /* We've reached the end, clear the current state */
+ if (count < 0)
+ cursor_state_clear (cursor, state, EBSQL_CURSOR_ORIGIN_BEGIN);
+ else
+ cursor_state_clear (cursor, state, EBSQL_CURSOR_ORIGIN_END);
+
+ } else if (data.last_vcard) {
+
+ /* Set the cursor state to the last result */
+ cursor_state_set_from_vcard (ebsql, cursor, state, data.last_vcard);
+ } else
+ /* Should never get here */
+ g_warn_if_reached ();
+
+ /* Assign the results to return (if any) */
+ if (results) {
+ /* Correct the order of results at the last minute */
+ *results = g_slist_reverse (data.results);
+ data.results = NULL;
+ }
+ }
+
+ /* Cleanup what was allocated by collect_results_for_cursor_cb() */
+ if (data.results)
+ g_slist_free_full (
+ data.results,
+ (GDestroyNotify) e_book_sqlite_search_data_free);
+ g_free (data.alloc_vcard);
+
+ /* Free the copy state if we were working with a copy */
+ if ((flags & EBSQL_CURSOR_STEP_MOVE) == 0)
+ cursor_state_free (cursor, state);
+
+ if (success)
+ return data.n_results;
+
+ return -1;
+}
+
+/**
+ * e_book_sqlite_cursor_set_target_alphabetic_index:
+ * @ebsql: An #EBookSqlite
+ * @cursor: The #EbSqlCursor to modify
+ * @idx: The alphabetic index
+ *
+ * Sets the @cursor position to an
+ * <link linkend="cursor-alphabet">Alphabetic Index</link>
+ * into the alphabet active in @ebsql's locale.
+ *
+ * After setting the target to an alphabetic index, for example the
+ * index for letter 'E', then further calls to e_book_sqlite_cursor_step()
+ * will return results starting with the letter 'E' (or results starting
+ * with the last result in 'D', if moving in a negative direction).
+ *
+ * The passed index must be a valid index in the active locale, knowledge
+ * on the currently active alphabet index must be obtained using #ECollator
+ * APIs.
+ *
+ * Use e_book_sqlite_ref_collator() to obtain the active collator for @ebsql.
+ *
+ * Since: 3.12
+ */
+void
+e_book_sqlite_cursor_set_target_alphabetic_index (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ gint idx)
+{
+ gint n_labels = 0;
+
+ g_return_if_fail (E_IS_BOOK_SQLITE (ebsql));
+ g_return_if_fail (cursor != NULL);
+ g_return_if_fail (idx >= 0);
+
+ e_collator_get_index_labels (
+ ebsql->priv->collator, &n_labels,
+ NULL, NULL, NULL);
+ g_return_if_fail (idx < n_labels);
+
+ cursor_state_clear (cursor, &(cursor->state), EBSQL_CURSOR_ORIGIN_CURRENT);
+ if (cursor->n_sort_fields > 0) {
+ SummaryField *field;
+ gchar *index_key;
+
+ index_key = e_collator_generate_key_for_index (ebsql->priv->collator, idx);
+ field = summary_field_get (ebsql, cursor->sort_fields[0]);
+
+ if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ cursor->state.values[0] = index_key;
+ } else {
+ cursor->state.values[0] =
+ ebsql_encode_vcard_sort_key (index_key);
+ g_free (index_key);
+ }
+ }
+}
+
+/**
+ * e_book_sqlite_cursor_set_sexp:
+ * @ebsql: An #EBookSqlite
+ * @cursor: The #EbSqlCursor
+ * @sexp: The new query expression for @cursor
+ * @error: A return location to store any error that might be reported.
+ *
+ * Modifies the current query expression for @cursor. This will not
+ * modify @cursor's state, but will change the outcome of any further
+ * calls to e_book_sqlite_cursor_calculate() or
+ * e_book_sqlite_cursor_step().
+ *
+ * Returns: %TRUE if the expression was valid and accepted by @ebsql
+ *
+ * Since: 3.12
+ */
+gboolean
+e_book_sqlite_cursor_set_sexp (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ const gchar *sexp,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (cursor != NULL, FALSE);
+
+ /* We don't like '\0' sexps, prefer NULL */
+ if (sexp && !sexp[0])
+ sexp = NULL;
+
+ EBSQL_LOCK_MUTEX (&ebsql->priv->lock);
+ success = ebsql_cursor_setup_query (ebsql, cursor, sexp, error);
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_cursor_calculate:
+ * @ebsql: An #EBookSqlite
+ * @cursor: The #EbSqlCursor
+ * @total: (out) (allow-none): A return location to store the total result set for this cursor
+ * @position: (out) (allow-none): A return location to store the total results before the cursor value
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (allow-none): A return location to store any error that might be reported.
+ *
+ * Calculates the @total amount of results for the @cursor's query expression,
+ * as well as the current @position of @cursor in the results. @position is
+ * represented as the amount of results which lead up to the current value
+ * of @cursor, if @cursor currently points to an exact contact, the position
+ * also includes the cursor contact.
+ *
+ * Returns: Whether @total and @position were successfully calculated.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_book_sqlite_cursor_calculate (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ gint *total,
+ gint *position,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success = TRUE;
+ gint local_total = 0;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), FALSE);
+ g_return_val_if_fail (cursor != NULL, FALSE);
+
+ /* If we're in a clear cursor state, then the position is 0 */
+ if (position && cursor->state.values[0] == NULL) {
+
+ if (cursor->state.position == EBSQL_CURSOR_ORIGIN_BEGIN) {
+ /* Mark the local pointer NULL, no need to calculate this anymore */
+ *position = 0;
+ position = NULL;
+ } else if (cursor->state.position == EBSQL_CURSOR_ORIGIN_END) {
+
+ /* Make sure that we look up the total so we can
+ * set the position to 'total + 1'
+ */
+ if (!total)
+ total = &local_total;
+ }
+ }
+
+ /* Early return if there is nothing to do */
+ if (!total && !position)
+ return TRUE;
+
+ EBSQL_LOCK_OR_RETURN (ebsql, cancellable, -1);
+
+ /* Start a read transaction, it's important our two queries are atomic */
+ if (!ebsql_start_transaction (ebsql, EBSQL_LOCK_READ, cancellable, error)) {
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+ return FALSE;
+ }
+
+ if (total)
+ success = cursor_count_total_locked (ebsql, cursor, total, error);
+
+ if (success && position)
+ success = cursor_count_position_locked (ebsql, cursor, position, error);
+
+ if (success)
+ success = ebsql_commit_transaction (ebsql, error);
+ else
+ /* The GError is already set. */
+ ebsql_rollback_transaction (ebsql, NULL);
+
+ EBSQL_UNLOCK_MUTEX (&ebsql->priv->lock);
+
+ /* In the case we're at the end, we just set the position
+ * to be the total + 1
+ */
+ if (success && position && total &&
+ cursor->state.position == EBSQL_CURSOR_ORIGIN_END)
+ *position = *total + 1;
+
+ return success;
+}
+
+/**
+ * e_book_sqlite_cursor_compare_contact:
+ * @ebsql: An #EBookSqlite
+ * @cursor: The #EbSqlCursor
+ * @contact: The #EContact to compare
+ * @matches_sexp: (out) (allow-none): Whether the contact matches the cursor's search expression
+ *
+ * Compares @contact with @cursor and returns whether @contact is less than, equal to, or greater
+ * than @cursor.
+ *
+ * Returns: A value that is less than, equal to, or greater than zero if @contact is found,
+ * respectively, to be less than, to match, or be greater than the current value of @cursor.
+ *
+ * Since: 3.12
+ */
+gint
+e_book_sqlite_cursor_compare_contact (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp)
+{
+ EBookSqlitePrivate *priv;
+ gint i;
+ gint comparison = 0;
+
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), -1);
+ g_return_val_if_fail (E_IS_CONTACT (contact), -1);
+ g_return_val_if_fail (cursor != NULL, -1);
+
+ priv = ebsql->priv;
+
+ if (matches_sexp) {
+ if (cursor->sexp == NULL)
+ *matches_sexp = TRUE;
+ else
+ *matches_sexp =
+ e_book_backend_sexp_match_contact (cursor->sexp, contact);
+ }
+
+ for (i = 0; i < cursor->n_sort_fields && comparison == 0; i++) {
+ SummaryField *field;
+ gchar *contact_key = NULL;
+ const gchar *cursor_key = NULL;
+ const gchar *field_value;
+ gchar *freeme = NULL;
+
+ field_value = (const gchar *) e_contact_get_const (contact, cursor->sort_fields[i]);
+ if (field_value)
+ contact_key = e_collator_generate_key (priv->collator, field_value, NULL);
+
+ field = summary_field_get (ebsql, cursor->sort_fields[i]);
+
+ if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ cursor_key = cursor->state.values[i];
+ } else {
+
+ if (cursor->state.values[i])
+ freeme = ebsql_decode_vcard_sort_key (cursor->state.values[i]);
+
+ cursor_key = freeme;
+ }
+
+ /* Empty state sorts below any contact value, which means the contact sorts above cursor */
+ if (cursor_key == NULL)
+ comparison = 1;
+ else
+ /* Check if contact sorts below, equal to, or above the cursor */
+ comparison = g_strcmp0 (contact_key, cursor_key);
+
+ g_free (contact_key);
+ g_free (freeme);
+ }
+
+ /* UID tie-breaker */
+ if (comparison == 0) {
+ const gchar *uid;
+
+ uid = (const gchar *) e_contact_get_const (contact, E_CONTACT_UID);
+
+ if (cursor->state.last_uid == NULL)
+ comparison = 1;
+ else if (uid == NULL)
+ comparison = -1;
+ else
+ comparison = strcmp (uid, cursor->state.last_uid);
+ }
+
+ return comparison;
+}
diff --git a/src/addressbook/libedata-book/e-book-sqlite.h b/src/addressbook/libedata-book/e-book-sqlite.h
new file mode 100644
index 000000000..41c9a6161
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-sqlite.h
@@ -0,0 +1,481 @@
+/*-*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* e-book-sqlitedb.h
+ *
+ * Copyright (C) 2013 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_SQLITE_H
+#define E_BOOK_SQLITE_H
+
+#include <libebook-contacts/libebook-contacts.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_SQLITE \
+ (e_book_sqlite_get_type ())
+#define E_BOOK_SQLITE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_SQLITE, EBookSqlite))
+#define E_BOOK_SQLITE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_SQLITE, EBookSqliteClass))
+#define E_IS_BOOK_SQLITE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_SQLITE))
+#define E_IS_BOOK_SQLITE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_SQLITE))
+#define E_BOOK_SQLITE_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_SQLITE, EBookSqliteClass))
+
+/**
+ * E_BOOK_SQLITE_ERROR:
+ *
+ * Error domain for #EBookSqlite operations.
+ *
+ * Since: 3.12
+ **/
+#define E_BOOK_SQLITE_ERROR (e_book_sqlite_error_quark ())
+
+/**
+ * E_BOOK_SQL_IS_POPULATED_KEY:
+ *
+ * This key can be used with e_book_sqlite_get_key_value().
+ *
+ * In the case of a migration from an older SQLite, any value which
+ * was previously stored with e_book_sqlitedb_set_is_populated()
+ * can be retrieved with this key.
+ *
+ * Since: 3.12
+ **/
+#define E_BOOK_SQL_IS_POPULATED_KEY "eds-reserved-namespace-is-populated"
+
+/**
+ * E_BOOK_SQL_SYNC_DATA_KEY:
+ *
+ * This key can be used with e_book_sqlite_get_key_value().
+ *
+ * In the case of a migration from an older SQLite, any value which
+ * was previously stored with e_book_sqlitedb_set_sync_data()
+ * can be retrieved with this key.
+ *
+ * Since: 3.12
+ **/
+#define E_BOOK_SQL_SYNC_DATA_KEY "eds-reserved-namespace-sync-data"
+
+G_BEGIN_DECLS
+
+typedef struct _EBookSqlite EBookSqlite;
+typedef struct _EBookSqliteClass EBookSqliteClass;
+typedef struct _EBookSqlitePrivate EBookSqlitePrivate;
+
+/**
+ * EbSqlChangeType:
+ * @EBSQL_CHANGE_CONTACT_ADDED: Contact was modified as a result of it's addition to the addressbook
+ * @EBSQL_CHANGE_LOCALE_CHANGED: Contact was modified as a result of a locale change
+ * @EBSQL_CHANGE_LAST: A symbolic end marker for this enumeration, will not be passed in callbacks.
+ *
+ * Indicates the type of change which occurred in a #EbSqlChangeCallback
+ *
+ * Since: 3.12
+ **/
+typedef enum {
+ EBSQL_CHANGE_CONTACT_ADDED,
+ EBSQL_CHANGE_LOCALE_CHANGED,
+ EBSQL_CHANGE_LAST
+} EbSqlChangeType;
+
+/**
+ * EbSqlChangeCallback:
+ * @change_type: The #EbSqlChangeType which occurred
+ * @uid: A contact UID
+ * @extra: The extra data associated to the contact
+ * @vcard: The vcard string for this UID
+ * @user_data: Pointer to user provided data
+ *
+ * A function which may be called in response to a change
+ * in contact data.
+ *
+ * <note><para>This user callback is called inside a lock,
+ * you must not call the #EBookSqlite API from
+ * this callback.</para></note>
+ *
+ * Since: 3.12
+ **/
+typedef void (*EbSqlChangeCallback) (EbSqlChangeType change_type,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *vcard,
+ gpointer user_data);
+
+/**
+ * EbSqlVCardCallback:
+ * @uid: A contact UID
+ * @extra: The extra data associated to the contact
+ * @user_data: User data previously passed to e_book_sqlite_new()
+ *
+ * If this callback is passed to e_book_sqlite_new(), then
+ * vcards are not stored in the SQLite and instead this callback
+ * is invoked to fetch the vcard.
+ *
+ * This callback will be called to fetch results for fully indexed
+ * and optimized queries, and it will also be called while performing
+ * fallback queries against #EContactFields which are not configured
+ * in the #ESourceBackendSummarySetup or default summary fields.
+ *
+ * <note><para>This user callback is called inside a lock,
+ * you must not call the #EBookSqlite API from
+ * this callback.</para></note>
+ *
+ * Returns: (transfer full): The appropriate vcard indicated by @uid
+ *
+ * Since: 3.12
+ **/
+typedef gchar * (*EbSqlVCardCallback) (const gchar *uid,
+ const gchar *extra,
+ gpointer user_data);
+
+/**
+ * EBookSqliteError:
+ * @E_BOOK_SQLITE_ERROR_ENGINE: An error was reported from the SQLite engine
+ * @E_BOOK_SQLITE_ERROR_CONSTRAINT: The error occurred due to an explicit constraint, this will
+ * happen when attempting to add two contacts with the same UID.
+ * @E_BOOK_SQLITE_ERROR_CONTACT_NOT_FOUND: A contact was not found by UID (this is
+ * different from a query that returns no results, which is not an error).
+ * @E_BOOK_SQLITE_ERROR_INVALID_QUERY: A query was invalid. This can happen if the
+ * search expression could not be parsed or if a phone number query contained non-phonenumber input.
+ * @E_BOOK_SQLITE_ERROR_UNSUPPORTED_QUERY: A query was not supported
+ * @E_BOOK_SQLITE_ERROR_UNSUPPORTED_FIELD: An unsupported #EContactField was specified in the summary
+ * @E_BOOK_SQLITE_ERROR_END_OF_LIST: An attempt was made to fetch results past the end of a contact list
+ * @E_BOOK_SQLITE_ERROR_LOAD: An error occured while loading or creating the database
+ *
+ * Defines the types of possible errors reported by the #EBookSqlite
+ */
+typedef enum {
+ E_BOOK_SQLITE_ERROR_ENGINE,
+ E_BOOK_SQLITE_ERROR_CONSTRAINT,
+ E_BOOK_SQLITE_ERROR_CONTACT_NOT_FOUND,
+ E_BOOK_SQLITE_ERROR_INVALID_QUERY,
+ E_BOOK_SQLITE_ERROR_UNSUPPORTED_QUERY,
+ E_BOOK_SQLITE_ERROR_UNSUPPORTED_FIELD,
+ E_BOOK_SQLITE_ERROR_END_OF_LIST,
+ E_BOOK_SQLITE_ERROR_LOAD
+} EBookSqliteError;
+
+/**
+ * EbSqlLockType:
+ * @EBSQL_LOCK_READ: Obtain a lock for reading
+ * @EBSQL_LOCK_WRITE: Obtain a lock for writing
+ *
+ * Indicates the type of lock requested in e_book_sqlite_lock()
+ */
+typedef enum {
+ EBSQL_LOCK_READ,
+ EBSQL_LOCK_WRITE
+} EbSqlLockType;
+
+/**
+ * EbSqlUnlockAction:
+ * @EBSQL_UNLOCK_NONE: Just unlock, this is appropriate for locks which were obtained with %EBSQL_LOCK_READ
+ * @EBSQL_UNLOCK_COMMIT: Commit any modifications which were made while the lock was held
+ * @EBSQL_UNLOCK_ROLLBACK: Rollback any modifications which were made while the lock was held
+ *
+ * Indicates what type of action to take while unlocking the sqlite with e_book_sqlite_unlock()
+ *
+ * In the case that some addressbook modification failed while holding an %EBSQL_LOCK_WRITE lock,
+ * then the #EBookSqlite must be unlocked with %EBSQL_UNLOCK_ROLLBACK.
+ */
+typedef enum {
+ EBSQL_UNLOCK_NONE,
+ EBSQL_UNLOCK_COMMIT,
+ EBSQL_UNLOCK_ROLLBACK
+} EbSqlUnlockAction;
+
+/**
+ * EbSqlSearchData:
+ * @uid: The %E_CONTACT_UID field of this contact
+ * @vcard: The the vcard string
+ * @extra: Any extra data associated to the vcard
+ *
+ * This structure is used to represent contacts returned
+ * by the #EBookSqlite from various functions
+ * such as e_book_sqlitedb_search().
+ *
+ * The @extra parameter will contain any data which was
+ * previously passed for this contact in e_book_sqlite_add_contact().
+ *
+ * These should be freed with e_book_sqlite_search_data_free().
+ *
+ * Since: 3.12
+ **/
+typedef struct {
+ gchar *uid;
+ gchar *vcard;
+ gchar *extra;
+} EbSqlSearchData;
+
+/**
+ * EBookSqlite:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.12
+ **/
+struct _EBookSqlite {
+ /*< private >*/
+ GObject parent;
+ EBookSqlitePrivate *priv;
+};
+
+/**
+ * EBookSqliteClass:
+ *
+ * Class structure for the #EBookSqlite class.
+ *
+ * Since: 3.12
+ */
+struct _EBookSqliteClass {
+ /*< private >*/
+ GObjectClass parent_class;
+
+ /* Signals */
+ gboolean (*before_insert_contact) (EBookSqlite *ebsql,
+ gpointer db, /* sqlite3 */
+ EContact *contact,
+ const gchar *extra,
+ gboolean replace,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (*before_remove_contact) (EBookSqlite *ebsql,
+ gpointer db, /* sqlite3 */
+ const gchar *contact_uid,
+ GCancellable *cancellable,
+ GError **error);
+};
+
+/**
+ * EbSqlCuror:
+ *
+ * An opaque cursor pointer
+ *
+ * Since: 3.12
+ */
+typedef struct _EbSqlCursor EbSqlCursor;
+
+/**
+ * EbSqlCursorOrigin:
+ * @EBSQL_CURSOR_ORIGIN_CURRENT: The current cursor position
+ * @EBSQL_CURSOR_ORIGIN_BEGIN: The beginning of the cursor results.
+ * @EBSQL_CURSOR_ORIGIN_END: The ending of the cursor results.
+ *
+ * Specifies the start position to in the list of traversed contacts
+ * in calls to e_book_sqlite_cursor_step().
+ *
+ * When an #EbSqlCuror is created, the current position implied by %EBSQL_CURSOR_ORIGIN_CURRENT
+ * is the same as %EBSQL_CURSOR_ORIGIN_BEGIN.
+ *
+ * Since: 3.12
+ */
+typedef enum {
+ EBSQL_CURSOR_ORIGIN_CURRENT = 0,
+ EBSQL_CURSOR_ORIGIN_BEGIN,
+ EBSQL_CURSOR_ORIGIN_END
+} EbSqlCursorOrigin;
+
+/**
+ * EbSqlCursorStepFlags:
+ * @EBSQL_CURSOR_STEP_MOVE: The cursor position should be modified while stepping
+ * @EBSQL_CURSOR_STEP_FETCH: Traversed contacts should be listed and returned while stepping.
+ *
+ * Defines the behaviour of e_book_sqlite_cursor_step().
+ *
+ * Since: 3.12
+ */
+typedef enum {
+ EBSQL_CURSOR_STEP_MOVE = (1 << 0),
+ EBSQL_CURSOR_STEP_FETCH = (1 << 1)
+} EbSqlCursorStepFlags;
+
+GType e_book_sqlite_get_type (void) G_GNUC_CONST;
+GQuark e_book_sqlite_error_quark (void);
+void e_book_sqlite_search_data_free (EbSqlSearchData *data);
+
+EBookSqlite * e_book_sqlite_new (const gchar *path,
+ ESource *source,
+ GCancellable *cancellable,
+ GError **error);
+EBookSqlite * e_book_sqlite_new_full (const gchar *path,
+ ESource *source,
+ ESourceBackendSummarySetup *setup,
+ EbSqlVCardCallback vcard_callback,
+ EbSqlChangeCallback change_callback,
+ gpointer user_data,
+ GDestroyNotify user_data_destroy,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_sqlite_lock (EBookSqlite *ebsql,
+ EbSqlLockType lock_type,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_sqlite_unlock (EBookSqlite *ebsql,
+ EbSqlUnlockAction action,
+ GError **error);
+gboolean e_book_sqlite_set_locale (EBookSqlite *ebsql,
+ const gchar *lc_collate,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_sqlite_get_locale (EBookSqlite *ebsql,
+ gchar **locale_out,
+ GError **error);
+
+ECollator * e_book_sqlite_ref_collator (EBookSqlite *ebsql);
+
+ESource * e_book_sqlite_ref_source (EBookSqlite *ebsql);
+
+/* Adding / Removing / Searching contacts */
+gboolean e_book_sqlite_add_contact (EBookSqlite *ebsql,
+ EContact *contact,
+ const gchar *extra,
+ gboolean replace,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_sqlite_add_contacts (EBookSqlite *ebsql,
+ GSList *contacts,
+ GSList *extra,
+ gboolean replace,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_sqlite_remove_contact (EBookSqlite *ebsql,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_sqlite_remove_contacts (EBookSqlite *ebsql,
+ GSList *uids,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_sqlite_has_contact (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean *exists,
+ GError **error);
+gboolean e_book_sqlite_get_contact (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean meta_contact,
+ EContact **ret_contact,
+ GError **error);
+gboolean ebsql_get_contact_unlocked (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean meta_contact,
+ EContact **ret_contact,
+ GError **error);
+gboolean e_book_sqlite_get_vcard (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean meta_contact,
+ gchar **ret_vcard,
+ GError **error);
+gboolean ebsql_get_vcard_unlocked (EBookSqlite *ebsql,
+ const gchar *uid,
+ gboolean meta_contact,
+ gchar **ret_vcard,
+ GError **error);
+gboolean e_book_sqlite_set_contact_extra (EBookSqlite *ebsql,
+ const gchar *uid,
+ const gchar *extra,
+ GError **error);
+gboolean e_book_sqlite_get_contact_extra (EBookSqlite *ebsql,
+ const gchar *uid,
+ gchar **ret_extra,
+ GError **error);
+gboolean ebsql_get_contact_extra_unlocked
+ (EBookSqlite *ebsql,
+ const gchar *uid,
+ gchar **ret_extra,
+ GError **error);
+gboolean e_book_sqlite_search (EBookSqlite *ebsql,
+ const gchar *sexp,
+ gboolean meta_contacts,
+ GSList **ret_list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_sqlite_search_uids (EBookSqlite *ebsql,
+ const gchar *sexp,
+ GSList **ret_list,
+ GCancellable *cancellable,
+ GError **error);
+
+/* Key / Value convenience API */
+gboolean e_book_sqlite_get_key_value (EBookSqlite *ebsql,
+ const gchar *key,
+ gchar **value,
+ GError **error);
+gboolean e_book_sqlite_set_key_value (EBookSqlite *ebsql,
+ const gchar *key,
+ const gchar *value,
+ GError **error);
+gboolean e_book_sqlite_get_key_value_int (EBookSqlite *ebsql,
+ const gchar *key,
+ gint *value,
+ GError **error);
+gboolean e_book_sqlite_set_key_value_int (EBookSqlite *ebsql,
+ const gchar *key,
+ gint value,
+ GError **error);
+
+/* Cursor API */
+EbSqlCursor * e_book_sqlite_cursor_new (EBookSqlite *ebsql,
+ const gchar *sexp,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ GError **error);
+void e_book_sqlite_cursor_free (EBookSqlite *ebsql,
+ EbSqlCursor *cursor);
+gint e_book_sqlite_cursor_step (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ EbSqlCursorStepFlags flags,
+ EbSqlCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_sqlite_cursor_set_target_alphabetic_index
+ (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ gint idx);
+gboolean e_book_sqlite_cursor_set_sexp (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ const gchar *sexp,
+ GError **error);
+gboolean e_book_sqlite_cursor_calculate (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ gint *total,
+ gint *position,
+ GCancellable *cancellable,
+ GError **error);
+gint e_book_sqlite_cursor_compare_contact
+ (EBookSqlite *ebsql,
+ EbSqlCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp);
+
+G_END_DECLS
+
+#endif /* E_BOOK_SQLITE_H */
diff --git a/src/addressbook/libedata-book/e-data-book-cursor-sqlite.c b/src/addressbook/libedata-book/e-data-book-cursor-sqlite.c
new file mode 100644
index 000000000..084f907c8
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-cursor-sqlite.c
@@ -0,0 +1,568 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+/**
+ * SECTION: e-data-book-cursor-sqlite
+ * @include: libedata-book/libedata-book.h
+ * @short_description: The SQLite cursor implementation
+ *
+ * This cursor implementation can be used with any backend which
+ * stores contacts using #EBookSqlite.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <glib/gi18n.h>
+
+#include "e-data-book-cursor-sqlite.h"
+
+#define E_DATA_BOOK_CURSOR_SQLITE_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_DATA_BOOK_CURSOR_SQLITE, EDataBookCursorSqlitePrivate))
+
+/* GObjectClass */
+static void e_data_book_cursor_sqlite_dispose (GObject *object);
+static void e_data_book_cursor_sqlite_finalize (GObject *object);
+static void e_data_book_cursor_sqlite_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec);
+
+/* EDataBookCursorClass */
+static gboolean e_data_book_cursor_sqlite_set_sexp (EDataBookCursor *cursor,
+ const gchar *sexp,
+ GError **error);
+static gint e_data_book_cursor_sqlite_step (EDataBookCursor *cursor,
+ const gchar *revision_guard,
+ EBookCursorStepFlags flags,
+ EBookCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GCancellable *cancellable,
+ GError **error);
+static gboolean e_data_book_cursor_sqlite_set_alphabetic_index (EDataBookCursor *cursor,
+ gint index,
+ const gchar *locale,
+ GError **error);
+static gboolean e_data_book_cursor_sqlite_get_position (EDataBookCursor *cursor,
+ gint *total,
+ gint *position,
+ GCancellable *cancellable,
+ GError **error);
+static gint e_data_book_cursor_sqlite_compare_contact (EDataBookCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp);
+static gboolean e_data_book_cursor_sqlite_load_locale (EDataBookCursor *cursor,
+ gchar **locale,
+ GError **error);
+
+struct _EDataBookCursorSqlitePrivate {
+ EBookSqlite *ebsql;
+ EbSqlCursor *cursor;
+ gchar *revision_key;
+};
+
+enum {
+ PROP_0,
+ PROP_EBSQL,
+ PROP_REVISION_KEY,
+ PROP_CURSOR,
+};
+
+G_DEFINE_TYPE (EDataBookCursorSqlite, e_data_book_cursor_sqlite, E_TYPE_DATA_BOOK_CURSOR);
+
+/************************************************
+ * GObjectClass *
+ ************************************************/
+static void
+e_data_book_cursor_sqlite_class_init (EDataBookCursorSqliteClass *class)
+{
+ GObjectClass *object_class;
+ EDataBookCursorClass *cursor_class;
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->dispose = e_data_book_cursor_sqlite_dispose;
+ object_class->finalize = e_data_book_cursor_sqlite_finalize;
+ object_class->set_property = e_data_book_cursor_sqlite_set_property;
+
+ cursor_class = E_DATA_BOOK_CURSOR_CLASS (class);
+ cursor_class->set_sexp = e_data_book_cursor_sqlite_set_sexp;
+ cursor_class->step = e_data_book_cursor_sqlite_step;
+ cursor_class->set_alphabetic_index = e_data_book_cursor_sqlite_set_alphabetic_index;
+ cursor_class->get_position = e_data_book_cursor_sqlite_get_position;
+ cursor_class->compare_contact = e_data_book_cursor_sqlite_compare_contact;
+ cursor_class->load_locale = e_data_book_cursor_sqlite_load_locale;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_EBSQL,
+ g_param_spec_object (
+ "ebsql", "EBookSqlite",
+ "The EBookSqlite to use for queries",
+ E_TYPE_BOOK_SQLITE,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_REVISION_KEY,
+ g_param_spec_string (
+ "revision-key", "Revision Key",
+ "The key name to fetch the revision from the sqlite backend",
+ NULL,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_CURSOR,
+ g_param_spec_pointer (
+ "cursor", "Cursor",
+ "The EbSqlCursor pointer",
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+ g_type_class_add_private (class, sizeof (EDataBookCursorSqlitePrivate));
+}
+
+static void
+e_data_book_cursor_sqlite_init (EDataBookCursorSqlite *cursor)
+{
+ cursor->priv = E_DATA_BOOK_CURSOR_SQLITE_GET_PRIVATE (cursor);
+}
+
+static void
+e_data_book_cursor_sqlite_dispose (GObject *object)
+{
+ EDataBookCursorSqlite *cursor = E_DATA_BOOK_CURSOR_SQLITE (object);
+ EDataBookCursorSqlitePrivate *priv = cursor->priv;
+
+ if (priv->ebsql != NULL) {
+
+ if (priv->cursor != NULL)
+ e_book_sqlite_cursor_free (
+ priv->ebsql, priv->cursor);
+
+ g_object_unref (priv->ebsql);
+ priv->ebsql = NULL;
+ priv->cursor = NULL;
+ }
+
+ G_OBJECT_CLASS (e_data_book_cursor_sqlite_parent_class)->dispose (object);
+}
+
+static void
+e_data_book_cursor_sqlite_finalize (GObject *object)
+{
+ EDataBookCursorSqlite *cursor = E_DATA_BOOK_CURSOR_SQLITE (object);
+ EDataBookCursorSqlitePrivate *priv = cursor->priv;
+
+ g_free (priv->revision_key);
+
+ G_OBJECT_CLASS (e_data_book_cursor_sqlite_parent_class)->finalize (object);
+}
+
+static void
+e_data_book_cursor_sqlite_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EDataBookCursorSqlite *cursor = E_DATA_BOOK_CURSOR_SQLITE (object);
+ EDataBookCursorSqlitePrivate *priv = cursor->priv;
+
+ switch (property_id) {
+ case PROP_EBSQL:
+ /* Construct-only, can only be set once */
+ priv->ebsql = g_value_dup_object (value);
+ break;
+ case PROP_REVISION_KEY:
+ /* Construct-only, can only be set once */
+ priv->revision_key = g_value_dup_string (value);
+ break;
+ case PROP_CURSOR:
+ /* Construct-only, can only be set once */
+ priv->cursor = g_value_get_pointer (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+/************************************************
+ * EDataBookCursorClass *
+ ************************************************/
+static gboolean
+e_data_book_cursor_sqlite_set_sexp (EDataBookCursor *cursor,
+ const gchar *sexp,
+ GError **error)
+{
+ EDataBookCursorSqlite *cursor_sqlite;
+ EDataBookCursorSqlitePrivate *priv;
+ GError *local_error = NULL;
+ gboolean success;
+
+ cursor_sqlite = E_DATA_BOOK_CURSOR_SQLITE (cursor);
+ priv = cursor_sqlite->priv;
+
+ success = e_book_sqlite_cursor_set_sexp (
+ priv->ebsql, priv->cursor, sexp, &local_error);
+
+ if (!success) {
+ if (g_error_matches (local_error,
+ E_BOOK_SQLITE_ERROR,
+ E_BOOK_SQLITE_ERROR_INVALID_QUERY)) {
+ g_set_error_literal (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_INVALID_QUERY,
+ local_error->message);
+ g_clear_error (&local_error);
+ } else {
+ g_propagate_error (error, local_error);
+ }
+ }
+
+ return success;
+}
+
+static gboolean
+convert_origin (EBookCursorOrigin src_origin,
+ EbSqlCursorOrigin *dest_origin,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ switch (src_origin) {
+ case E_BOOK_CURSOR_ORIGIN_CURRENT:
+ *dest_origin = EBSQL_CURSOR_ORIGIN_CURRENT;
+ break;
+ case E_BOOK_CURSOR_ORIGIN_BEGIN:
+ *dest_origin = EBSQL_CURSOR_ORIGIN_BEGIN;
+ break;
+ case E_BOOK_CURSOR_ORIGIN_END:
+ *dest_origin = EBSQL_CURSOR_ORIGIN_END;
+ break;
+ default:
+ success = FALSE;
+ g_set_error_literal (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_INVALID_ARG,
+ _("Unrecognized cursor origin"));
+ break;
+ }
+
+ return success;
+}
+
+static void
+convert_flags (EBookCursorStepFlags src_flags,
+ EbSqlCursorStepFlags *dest_flags)
+{
+ if (src_flags & E_BOOK_CURSOR_STEP_MOVE)
+ *dest_flags |= EBSQL_CURSOR_STEP_MOVE;
+
+ if (src_flags & E_BOOK_CURSOR_STEP_FETCH)
+ *dest_flags |= EBSQL_CURSOR_STEP_FETCH;
+}
+
+static gint
+e_data_book_cursor_sqlite_step (EDataBookCursor *cursor,
+ const gchar *revision_guard,
+ EBookCursorStepFlags flags,
+ EBookCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EDataBookCursorSqlite *cursor_sqlite;
+ EDataBookCursorSqlitePrivate *priv;
+ GSList *local_results = NULL, *local_converted_results = NULL, *l;
+ EbSqlCursorOrigin sqlite_origin = EBSQL_CURSOR_ORIGIN_CURRENT;
+ EbSqlCursorStepFlags sqlite_flags = 0;
+ gchar *revision = NULL;
+ gboolean success = FALSE;
+ gint n_results = -1;
+
+ cursor_sqlite = E_DATA_BOOK_CURSOR_SQLITE (cursor);
+ priv = cursor_sqlite->priv;
+
+ if (!convert_origin (origin, &sqlite_origin, error))
+ return FALSE;
+
+ convert_flags (flags, &sqlite_flags);
+
+ /* Here we check the EBookSqlite revision
+ * against the revision_guard with an atomic transaction
+ * with the sqlite.
+ *
+ * The addressbook modifications and revision changes
+ * are also atomically committed to the SQLite.
+ */
+ success = e_book_sqlite_lock (priv->ebsql, EBSQL_LOCK_READ, cancellable, error);
+
+ if (success && revision_guard)
+ success = e_book_sqlite_get_key_value (
+ priv->ebsql,
+ priv->revision_key,
+ &revision,
+ error);
+
+ if (success && revision_guard &&
+ g_strcmp0 (revision, revision_guard) != 0) {
+
+ g_set_error_literal (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_OUT_OF_SYNC,
+ _("Out of sync revision while moving cursor"));
+ success = FALSE;
+ }
+
+ if (success) {
+ GError *local_error = NULL;
+
+ n_results = e_book_sqlite_cursor_step (
+ priv->ebsql,
+ priv->cursor,
+ sqlite_flags,
+ sqlite_origin,
+ count,
+ &local_results,
+ cancellable,
+ &local_error);
+
+ if (n_results < 0) {
+
+ /* Convert the SQLite backend error to an EClient error */
+ if (g_error_matches (local_error,
+ E_BOOK_SQLITE_ERROR,
+ E_BOOK_SQLITE_ERROR_END_OF_LIST)) {
+ g_set_error_literal (
+ error, E_CLIENT_ERROR,
+ E_CLIENT_ERROR_QUERY_REFUSED,
+ local_error->message);
+ g_clear_error (&local_error);
+ } else
+ g_propagate_error (error, local_error);
+
+ success = FALSE;
+ }
+ }
+
+ if (success) {
+ success = e_book_sqlite_unlock (priv->ebsql, EBSQL_UNLOCK_NONE, error);
+
+ } else {
+ GError *local_error = NULL;
+
+ if (!e_book_sqlite_unlock (priv->ebsql, EBSQL_UNLOCK_NONE, &local_error)) {
+ g_warning (
+ "Error occurred while unlocking the SQLite: %s",
+ local_error->message);
+ g_clear_error (&local_error);
+ }
+ }
+
+ for (l = local_results; l; l = l->next) {
+ EbSqlSearchData *data = l->data;
+
+ local_converted_results =
+ g_slist_prepend (local_converted_results, data->vcard);
+ data->vcard = NULL;
+ }
+
+ g_slist_free_full (local_results, (GDestroyNotify) e_book_sqlite_search_data_free);
+
+ if (results)
+ *results = g_slist_reverse (local_converted_results);
+ else
+ g_slist_free_full (local_converted_results, (GDestroyNotify) g_free);
+
+ g_free (revision);
+
+ if (success)
+ return n_results;
+
+ return -1;
+}
+
+static gboolean
+e_data_book_cursor_sqlite_set_alphabetic_index (EDataBookCursor *cursor,
+ gint index,
+ const gchar *locale,
+ GError **error)
+{
+ EDataBookCursorSqlite *cursor_sqlite;
+ EDataBookCursorSqlitePrivate *priv;
+ gchar *current_locale = NULL;
+
+ cursor_sqlite = E_DATA_BOOK_CURSOR_SQLITE (cursor);
+ priv = cursor_sqlite->priv;
+
+ if (!e_book_sqlite_get_locale (priv->ebsql, &current_locale, error))
+ return FALSE;
+
+ /* Locale mismatch, need to report error */
+ if (g_strcmp0 (current_locale, locale) != 0) {
+ g_set_error_literal (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_OUT_OF_SYNC,
+ _("Alphabetic index was set for incorrect locale"));
+ g_free (current_locale);
+ return FALSE;
+ }
+
+ e_book_sqlite_cursor_set_target_alphabetic_index (
+ priv->ebsql,
+ priv->cursor,
+ index);
+ g_free (current_locale);
+ return TRUE;
+}
+
+static gboolean
+e_data_book_cursor_sqlite_get_position (EDataBookCursor *cursor,
+ gint *total,
+ gint *position,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EDataBookCursorSqlite *cursor_sqlite;
+ EDataBookCursorSqlitePrivate *priv;
+
+ cursor_sqlite = E_DATA_BOOK_CURSOR_SQLITE (cursor);
+ priv = cursor_sqlite->priv;
+
+ return e_book_sqlite_cursor_calculate (
+ priv->ebsql,
+ priv->cursor,
+ total, position,
+ cancellable,
+ error);
+}
+
+static gint
+e_data_book_cursor_sqlite_compare_contact (EDataBookCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp)
+{
+ EDataBookCursorSqlite *cursor_sqlite;
+ EDataBookCursorSqlitePrivate *priv;
+
+ cursor_sqlite = E_DATA_BOOK_CURSOR_SQLITE (cursor);
+ priv = cursor_sqlite->priv;
+
+ return e_book_sqlite_cursor_compare_contact (
+ priv->ebsql,
+ priv->cursor,
+ contact,
+ matches_sexp);
+}
+
+static gboolean
+e_data_book_cursor_sqlite_load_locale (EDataBookCursor *cursor,
+ gchar **locale,
+ GError **error)
+{
+ EDataBookCursorSqlite *cursor_sqlite;
+ EDataBookCursorSqlitePrivate *priv;
+
+ cursor_sqlite = E_DATA_BOOK_CURSOR_SQLITE (cursor);
+ priv = cursor_sqlite->priv;
+
+ return e_book_sqlite_get_locale (priv->ebsql, locale, error);
+}
+
+/************************************************
+ * API *
+ ************************************************/
+/**
+ * e_data_book_cursor_sqlite_new:
+ * @backend: the #EBookBackend creating this cursor
+ * @ebsql: the #EBookSqlite object to base this cursor on
+ * @revision_key: The key name to consult for the current overall contacts database revision
+ * @sort_fields: (array length=n_fields): an array of #EContactFields as sort keys in order of priority
+ * @sort_types: (array length=n_fields): an array of #EBookCursorSortTypes, one for each field in @sort_fields
+ * @n_fields: the number of fields to sort results by.
+ * @error: a return location to story any error that might be reported.
+ *
+ * Creates an #EDataBookCursor and implements all of the cursor methods
+ * using the delegate @ebsql object.
+ *
+ * This is a suitable cursor type for any backend which stores its contacts
+ * using the #EBookSqlite object.
+ *
+ * Returns: (transfer full): A newly created #EDataBookCursor, or %NULL if cursor creation failed.
+ *
+ * Since: 3.12
+ */
+EDataBookCursor *
+e_data_book_cursor_sqlite_new (EBookBackend *backend,
+ EBookSqlite *ebsql,
+ const gchar *revision_key,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error)
+{
+ EDataBookCursor *cursor = NULL;
+ EbSqlCursor *ebsql_cursor;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+ g_return_val_if_fail (E_IS_BOOK_SQLITE (ebsql), NULL);
+
+ ebsql_cursor = e_book_sqlite_cursor_new (
+ ebsql, NULL,
+ sort_fields,
+ sort_types,
+ n_fields,
+ &local_error);
+
+ if (ebsql_cursor) {
+ cursor = g_object_new (
+ E_TYPE_DATA_BOOK_CURSOR_SQLITE,
+ "backend", backend,
+ "ebsql", ebsql,
+ "revision-key", revision_key,
+ "cursor", ebsql_cursor,
+ NULL);
+
+ /* Initially created cursors should have a position & total */
+ if (!e_data_book_cursor_load_locale (E_DATA_BOOK_CURSOR (cursor),
+ NULL, NULL, error))
+ g_clear_object (&cursor);
+
+ } else if (g_error_matches (local_error,
+ E_BOOK_SQLITE_ERROR,
+ E_BOOK_SQLITE_ERROR_INVALID_QUERY)) {
+ g_set_error_literal (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_INVALID_QUERY,
+ local_error->message);
+ g_clear_error (&local_error);
+ } else {
+ g_propagate_error (error, local_error);
+ }
+
+ return cursor;
+}
diff --git a/src/addressbook/libedata-book/e-data-book-cursor-sqlite.h b/src/addressbook/libedata-book/e-data-book-cursor-sqlite.h
new file mode 100644
index 000000000..234c5964c
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-cursor-sqlite.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_DATA_BOOK_CURSOR_SQLITE_H
+#define E_DATA_BOOK_CURSOR_SQLITE_H
+
+#include <libedata-book/e-data-book-cursor.h>
+#include <libedata-book/e-book-sqlite.h>
+#include <libedata-book/e-book-backend.h>
+
+#define E_TYPE_DATA_BOOK_CURSOR_SQLITE (e_data_book_cursor_sqlite_get_type ())
+#define E_DATA_BOOK_CURSOR_SQLITE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), E_TYPE_DATA_BOOK_CURSOR_SQLITE, EDataBookCursorSqlite))
+#define E_DATA_BOOK_CURSOR_SQLITE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), E_TYPE_DATA_BOOK_CURSOR_SQLITE, EDataBookCursorSqliteClass))
+#define E_IS_DATA_BOOK_CURSOR_SQLITE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), E_TYPE_DATA_BOOK_CURSOR_SQLITE))
+#define E_IS_DATA_BOOK_CURSOR_SQLITE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), E_TYPE_DATA_BOOK_CURSOR_SQLITE))
+#define E_DATA_BOOK_CURSOR_SQLITE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), E_TYPE_DATA_BOOK_CURSOR_SQLITE, EDataBookCursorSqliteClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EDataBookCursorSqlite EDataBookCursorSqlite;
+typedef struct _EDataBookCursorSqliteClass EDataBookCursorSqliteClass;
+typedef struct _EDataBookCursorSqlitePrivate EDataBookCursorSqlitePrivate;
+
+/**
+ * EDataBookCursorSqlite:
+ *
+ * An opaque handle for the SQLite cursor instance.
+ *
+ * Since: 3.12
+ */
+struct _EDataBookCursorSqlite {
+ EDataBookCursor parent;
+ EDataBookCursorSqlitePrivate *priv;
+};
+
+/**
+ * EDataBookCursorSqliteClass:
+ *
+ * The SQLite cursor class structure.
+ *
+ * Since: 3.12
+ */
+struct _EDataBookCursorSqliteClass {
+ EDataBookCursorClass parent;
+};
+
+GType e_data_book_cursor_sqlite_get_type (void);
+EDataBookCursor *e_data_book_cursor_sqlite_new (EBookBackend *backend,
+ EBookSqlite *ebsql,
+ const gchar *revision_key,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* E_DATA_BOOK_CURSOR_SQLITE_H */
diff --git a/src/addressbook/libedata-book/e-data-book-cursor.c b/src/addressbook/libedata-book/e-data-book-cursor.c
new file mode 100644
index 000000000..7c8fb5b0f
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-cursor.c
@@ -0,0 +1,1216 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+/**
+ * SECTION: e-data-book-cursor
+ * @include: libedata-book/libedata-book.h
+ * @short_description: The abstract cursor API
+ *
+ * The #EDataBookCursor API is the high level cursor API on the
+ * addressbook server, it can respond to client requests directly
+ * when opened in direct read access mode, otherwise it will implement
+ * the org.gnome.evolution.dataserver.AddressBookCursor D-Bus interface
+ * when instantiated by the addressbook server.
+ *
+ * <note><para>EDataBookCursor is an implementation detail for backends who wish
+ * to implement cursors. If you need to use the client API to iterate over contacts
+ * stored in Evolution Data Server; you should be using #EBookClientCursor instead.
+ * </para></note>
+ *
+ * <refsect2 id="cursor-implementing">
+ * <title>Implementing Cursors</title>
+ * <para>
+ * In order for an addressbook backend to implement cursors, it must
+ * first be locale aware, persist a current locale setting and implement
+ * the #EBookBackendClass.set_locale() and #EBookBackendClass.get_locale()
+ * methods.
+ * </para>
+ * <para>
+ * The backend indicates that it supports cursors by implementing the
+ * #EBookBackendClass.create_cursor() and returning an #EDataBookCursor,
+ * any backend implementing #EBookBackendClass.create_cursor() should also
+ * implement #EBookBackendClass.delete_cursor().
+ * </para>
+ * <para>
+ * For backends which use #EBookBackendSqliteDB to store contacts,
+ * an #EDataBookCursorSqlite can be used as a cursor implementation.
+ * </para>
+ * <para>
+ * Implementing a concrete cursor class for your own addressbook
+ * backend is a matter of implementing all of the virtual methods
+ * on the #EDataBookCursorClass vtable, each virtual method has
+ * documentation describing how each of the methods should be implemented.
+ * </para>
+ * </refsect2>
+ *
+ * <refsect2 id="cursor-track-state">
+ * <title>Tracking Cursor State</title>
+ * <para>
+ * The cursor state itself is defined as an array of sort keys
+ * and an %E_CONTACT_UID value. There should be one sort key
+ * stored for each contact field which was passed to
+ * #EBookBackendClass.create_cursor().
+ * </para>
+ * <para>
+ * Initially, the cursor state is assumed to be clear and
+ * positioned naturally at the beginning so that the first
+ * calls to #EDataBookCursorClass.step() using the
+ * %E_BOOK_CURSOR_ORIGIN_CURRENT origin would respond in the
+ * same way as the %E_BOOK_CURSOR_ORIGIN_BEGIN origin would.
+ * </para>
+ * <para>
+ * Unless the %E_BOOK_CURSOR_STEP_FETCH flag is not specified
+ * in calls to #EDataBookCursorClass.step(), the cursor state
+ * should be always be set to the last contact which was traversed
+ * in every call to #EDataBookCursorClass.step(). In the case
+ * that #EDataBookCursorClass.step() was asked to step beyond the
+ * bounderies of the list, i.e. when stepping passed the end
+ * of the list of before the beginning, then the cursor state
+ * can be cleared and the implementation must track whether
+ * the cursor is at the beginning or the end of the list.
+ * </para>
+ * <para>
+ * The task of actually collecting the cursor state from a
+ * contact should be done using an #ECollator created for
+ * the locale in which your #EBookBackend is configured for.
+ * </para>
+ * <example>
+ * <title>Collecting sort keys for a given contact</title>
+ * <programlisting><![CDATA[
+ * static void
+ * update_state_from_contact (EBookBackendSmth *smth,
+ * EBookBackendSmthCursor *cursor,
+ * EContact *contact)
+ * {
+ * gint i;
+ *
+ * clear_state (smth, cursor);
+ *
+ * // For each sort key the cursor was created for
+ * for (i = 0; i < cursor->n_sort_fields; i++) {
+ *
+ * // Using an ECollator created for the locale
+ * // set on your EBookBackend...
+ * const gchar *string = e_contact_get_const (contact, cursor->sort_fields[i]);
+ *
+ * // Generate a sort key for each value
+ * if (string)
+ * cursor->state->values[i] =
+ * e_collator_generate_key (smth->collator,
+ * string, NULL);
+ * else
+ * cursor->state->values[i] = g_strdup ("");
+ * }
+ *
+ * state->last_uid = e_contact_get (contact, E_CONTACT_UID);
+ * }
+ * ]]></programlisting>
+ * </example>
+ * <para>
+ * Using the strings collected above for a given contact,
+ * two contacts can easily be compared for equality in
+ * a locale sensitive way, using strcmp() directly on
+ * the generated sort keys.
+ * </para>
+ * <para>
+ * Calls to #EDataBookCursorClass.step() with the
+ * %E_BOOK_CURSOR_ORIGIN_BEGIN or %E_BOOK_CURSOR_ORIGIN_END reset
+ * the cursor state before fetching results from either the
+ * beginning or ending of the result list respectively.
+ * </para>
+ * </refsect2>
+ *
+ * <refsect2 id="cursor-localized-sorting">
+ * <title>Implementing Localized Sorting</title>
+ * <para>
+ * To implement localized sorting in an addressbook backend, an #ECollator
+ * can be used. The #ECollator provides all the functionality needed
+ * to respond to the cursor methods.
+ * </para>
+ * <para>
+ * When storing contacts in your backend, sort keys should be generated
+ * for any fields which might be used as sort key parameters for a cursor,
+ * keys for these fields should be generated with e_collator_generate_key()
+ * using an #ECollator created for the locale in which your addressbook is
+ * currently configured (undesired fields may be rejected at cursor creation
+ * time with an %E_CLIENT_ERROR_INVALID_QUERY error).
+ * </para>
+ * <para>
+ * The generated sort keys can then be used with strcmp() in order to
+ * compare results with the currently stored cursor state. These comparisons
+ * should compare contact fields in order of precedence in the array of
+ * sort fields which the cursor was configured with. If two contacts match
+ * exactly, then the %E_CONTACT_UID value is used to determine which
+ * contact sorts above or below the other.
+ * </para>
+ * <para>
+ * When your sort keys are generated using #ECollator, you can easily
+ * use e_collator_generate_key_for_index() to implement
+ * #EDataBookCursorClass.set_alphabetic_index() and set the cursor
+ * position before (below) a given letter in the active alphabet. The key
+ * generated for an alphabetic index is guaranteed to sort below any word
+ * starting with the given letter, and above any word starting with the
+ * preceeding letter.
+ * </para>
+ * </refsect2>
+ *
+ * <refsect2 id="cursor-dra">
+ * <title>Direct Read Access</title>
+ * <para>
+ * In order to support cursors in backends which support Direct Read Access
+ * mode, the underlying addressbook data must be written atomically along with each
+ * new revision attribute. The cursor mechanics rely on this atomicity in order
+ * to avoid any race conditions and ensure that data read back from the addressbook
+ * is current and up to date.
+ * </para>
+ * </refsect2>
+ *
+ * <refsect2 id="cursor-backends">
+ * <title>Backend Tasks</title>
+ * <para>
+ * Backends have ownership of the cursors which they create
+ * and have some responsibility when supporting cursors.
+ * </para>
+ * <para>
+ * As mentioned above, in Direct Read Access mode (if supported), all
+ * revision writes and addressbook modifications must be committed
+ * atomically.
+ * </para>
+ * <para>
+ * Beyond that, it is the responsibility of the backend to call
+ * e_data_book_cursor_contact_added() and e_data_book_cursor_contact_removed()
+ * whenever the addressbook is modified. When a contact is modified
+ * but not added or removed, then e_data_book_cursor_contact_removed()
+ * should be called with the old existing contact and then
+ * e_data_book_cursor_contact_added() should be called with
+ * the new contact. This will automatically update the cursor
+ * total and position status.
+ * </para>
+ * <para>
+ * Note that if it's too much trouble to load the existing
+ * contact data when a contact is modified, then
+ * e_data_book_cursor_recalculate() can be called instead. This
+ * will use the #EDataBookCursorClass.get_position() method
+ * recalculate current cursor position from scratch.
+ * </para>
+ * </refsect2>
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <glib/gi18n.h>
+
+#include "e-data-book-cursor.h"
+#include "e-book-backend.h"
+
+/* Private D-Bus class. */
+#include <e-dbus-address-book-cursor.h>
+
+#define E_DATA_BOOK_CURSOR_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_DATA_BOOK_CURSOR, EDataBookCursorPrivate))
+
+/* GObjectClass */
+static void e_data_book_cursor_constructed (GObject *object);
+static void e_data_book_cursor_dispose (GObject *object);
+static void e_data_book_cursor_finalize (GObject *object);
+static void e_data_book_cursor_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec);
+static void e_data_book_cursor_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec);
+
+/* Private Functions */
+static void data_book_cursor_set_values (EDataBookCursor *cursor,
+ gint total,
+ gint position);
+static gint data_book_cursor_compare_contact (EDataBookCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp);
+static void calculate_step_position (EDataBookCursor *cursor,
+ EBookCursorOrigin origin,
+ gint count,
+ gint results);
+
+/* D-Bus callbacks */
+static gint data_book_cursor_handle_step (EDBusAddressBookCursor *dbus_object,
+ GDBusMethodInvocation *invocation,
+ const gchar *revision,
+ EBookCursorStepFlags flags,
+ EBookCursorOrigin origin,
+ gint count,
+ EDataBookCursor *cursor);
+static gboolean data_book_cursor_handle_set_alphabetic_index (EDBusAddressBookCursor *dbus_object,
+ GDBusMethodInvocation *invocation,
+ gint index,
+ const gchar *locale,
+ EDataBookCursor *cursor);
+static gboolean data_book_cursor_handle_set_query (EDBusAddressBookCursor *dbus_object,
+ GDBusMethodInvocation *invocation,
+ const gchar *query,
+ EDataBookCursor *cursor);
+static gboolean data_book_cursor_handle_dispose (EDBusAddressBookCursor *dbus_object,
+ GDBusMethodInvocation *invocation,
+ EDataBookCursor *cursor);
+
+struct _EDataBookCursorPrivate {
+ EDBusAddressBookCursor *dbus_object;
+ EBookBackend *backend;
+
+ gchar *locale;
+ gint total;
+ gint position;
+};
+
+enum {
+ PROP_0,
+ PROP_BACKEND,
+ PROP_TOTAL,
+ PROP_POSITION,
+};
+
+G_DEFINE_ABSTRACT_TYPE (
+ EDataBookCursor,
+ e_data_book_cursor,
+ G_TYPE_OBJECT);
+
+/************************************************
+ * GObjectClass *
+ ************************************************/
+static void
+e_data_book_cursor_class_init (EDataBookCursorClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->constructed = e_data_book_cursor_constructed;
+ object_class->finalize = e_data_book_cursor_finalize;
+ object_class->dispose = e_data_book_cursor_dispose;
+ object_class->get_property = e_data_book_cursor_get_property;
+ object_class->set_property = e_data_book_cursor_set_property;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_BACKEND,
+ g_param_spec_object (
+ "backend",
+ "Backend",
+ "The backend which created this cursor",
+ E_TYPE_BOOK_BACKEND,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_TOTAL,
+ g_param_spec_int (
+ "total", "Total",
+ "The total results for this cursor",
+ 0, G_MAXINT, 0,
+ G_PARAM_READABLE));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_POSITION,
+ g_param_spec_int (
+ "position", "Position",
+ "The current position of this cursor",
+ 0, G_MAXINT, 0,
+ G_PARAM_READABLE));
+
+ g_type_class_add_private (class, sizeof (EDataBookCursorPrivate));
+}
+
+static void
+e_data_book_cursor_init (EDataBookCursor *cursor)
+{
+ cursor->priv = E_DATA_BOOK_CURSOR_GET_PRIVATE (cursor);
+}
+
+static void
+e_data_book_cursor_constructed (GObject *object)
+{
+ EDataBookCursor *cursor = E_DATA_BOOK_CURSOR (object);
+ GError *error = NULL;
+
+ /* Get the initial cursor values */
+ if (!e_data_book_cursor_recalculate (cursor, NULL, &error)) {
+ g_warning (
+ "Failed to calculate initial cursor position: %s",
+ error->message);
+ g_clear_error (&error);
+ }
+
+ G_OBJECT_CLASS (e_data_book_cursor_parent_class)->constructed (object);
+}
+
+static void
+e_data_book_cursor_finalize (GObject *object)
+{
+ EDataBookCursor *cursor = E_DATA_BOOK_CURSOR (object);
+ EDataBookCursorPrivate *priv = cursor->priv;
+
+ g_free (priv->locale);
+
+ G_OBJECT_CLASS (e_data_book_cursor_parent_class)->finalize (object);
+}
+
+static void
+e_data_book_cursor_dispose (GObject *object)
+{
+ EDataBookCursor *cursor = E_DATA_BOOK_CURSOR (object);
+ EDataBookCursorPrivate *priv = cursor->priv;
+
+ g_clear_object (&(priv->dbus_object));
+ g_clear_object (&(priv->backend));
+
+ G_OBJECT_CLASS (e_data_book_cursor_parent_class)->dispose (object);
+}
+
+static void
+e_data_book_cursor_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ EDataBookCursor *cursor = E_DATA_BOOK_CURSOR (object);
+ EDataBookCursorPrivate *priv = cursor->priv;
+
+ switch (property_id) {
+ case PROP_BACKEND:
+ g_value_set_object (value, priv->backend);
+ break;
+ case PROP_TOTAL:
+ g_value_set_int (value, priv->total);
+ break;
+ case PROP_POSITION:
+ g_value_set_int (value, priv->position);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+e_data_book_cursor_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EDataBookCursor *cursor = E_DATA_BOOK_CURSOR (object);
+ EDataBookCursorPrivate *priv = cursor->priv;
+
+ switch (property_id) {
+ case PROP_BACKEND:
+ /* Construct-only, can only be set once */
+ priv->backend = g_value_dup_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+/************************************************
+ * Private Functions *
+ ************************************************/
+static void
+data_book_cursor_set_values (EDataBookCursor *cursor,
+ gint total,
+ gint position)
+{
+ EDataBookCursorPrivate *priv;
+ gboolean changed = FALSE;
+
+ g_return_if_fail (E_IS_DATA_BOOK_CURSOR (cursor));
+
+ priv = cursor->priv;
+
+ g_object_freeze_notify (G_OBJECT (cursor));
+
+ if (priv->total != total) {
+ priv->total = total;
+ g_object_notify (G_OBJECT (cursor), "total");
+ changed = TRUE;
+ }
+
+ if (priv->position != position) {
+ priv->position = position;
+ g_object_notify (G_OBJECT (cursor), "position");
+ changed = TRUE;
+ }
+
+ g_object_thaw_notify (G_OBJECT (cursor));
+
+ if (changed && priv->dbus_object) {
+ e_dbus_address_book_cursor_set_total (priv->dbus_object, priv->total);
+ e_dbus_address_book_cursor_set_position (priv->dbus_object, priv->position);
+ }
+}
+
+static gint
+data_book_cursor_compare_contact (EDataBookCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp)
+{
+ gint result;
+
+ if (!E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->compare_contact) {
+ g_critical (
+ "EDataBookCursor.compare_contact() unimplemented on type '%s'",
+ G_OBJECT_TYPE_NAME (cursor));
+ return 0;
+ }
+
+ g_object_ref (cursor);
+ result = (* E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->compare_contact) (cursor,
+ contact,
+ matches_sexp);
+ g_object_unref (cursor);
+
+ return result;
+}
+
+static void
+calculate_step_position (EDataBookCursor *cursor,
+ EBookCursorOrigin origin,
+ gint count,
+ gint results)
+{
+ EDataBookCursorPrivate *priv = cursor->priv;
+ gint new_position;
+ gint offset = results;
+
+ g_return_if_fail (origin == E_BOOK_CURSOR_ORIGIN_CURRENT ||
+ origin == E_BOOK_CURSOR_ORIGIN_BEGIN ||
+ origin == E_BOOK_CURSOR_ORIGIN_END);
+
+ /* If we didnt get as many contacts as asked for, it indicates that
+ * we've reached the end of the list (or beginning)... in this case
+ * we add 1 to the offset
+ * so that we land on the 0 position or the total + 1 position
+ * respectively.
+ */
+ if (offset < ABS (count)) {
+ offset += 1;
+ }
+
+ /* Convert our 'number of results' into a signed 'offset'
+ * to add to the cursor position.
+ */
+ if (count < 0)
+ offset = -offset;
+
+ /* Don't assert the boundaries of values here, we
+ * did that in e_data_book_cursor_step() already.
+ */
+ switch (origin) {
+ case E_BOOK_CURSOR_ORIGIN_CURRENT:
+ new_position = priv->position + offset;
+ break;
+
+ case E_BOOK_CURSOR_ORIGIN_BEGIN:
+ new_position = offset;
+ break;
+
+ case E_BOOK_CURSOR_ORIGIN_END:
+ new_position = (priv->total + 1) + offset;
+ break;
+ }
+
+ new_position = CLAMP (new_position, 0, priv->total + 1);
+ data_book_cursor_set_values (cursor, priv->total, new_position);
+}
+
+/************************************************
+ * D-Bus Callbacks *
+ ************************************************/
+static gboolean
+data_book_cursor_handle_step (EDBusAddressBookCursor *dbus_object,
+ GDBusMethodInvocation *invocation,
+ const gchar *revision,
+ EBookCursorStepFlags flags,
+ EBookCursorOrigin origin,
+ gint count,
+ EDataBookCursor *cursor)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ gint n_results;
+
+ n_results = e_data_book_cursor_step (
+ cursor, revision, flags, origin,
+ count, &results, NULL, &error);
+
+ if (n_results < 0) {
+ g_dbus_method_invocation_return_gerror (invocation, error);
+ g_clear_error (&error);
+ } else {
+ gchar **strv = NULL;
+ const gchar * const empty_str[] = { NULL };
+
+ if (results) {
+ GSList *l;
+ gint i = 0;
+
+ strv = g_new0 (gchar *, g_slist_length (results) + 1);
+
+ for (l = results; l; l = l->next) {
+ gchar *vcard = l->data;
+
+ strv[i++] = e_util_utf8_make_valid (vcard);
+ }
+
+ g_slist_free_full (results, g_free);
+ }
+
+ e_dbus_address_book_cursor_complete_step (
+ dbus_object,
+ invocation,
+ n_results,
+ strv ?
+ (const gchar *const *) strv :
+ empty_str,
+ cursor->priv->total,
+ cursor->priv->position);
+
+ g_strfreev (strv);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+data_book_cursor_handle_set_alphabetic_index (EDBusAddressBookCursor *dbus_object,
+ GDBusMethodInvocation *invocation,
+ gint index,
+ const gchar *locale,
+ EDataBookCursor *cursor)
+{
+ GError *error = NULL;
+
+ if (!e_data_book_cursor_set_alphabetic_index (cursor,
+ index,
+ locale,
+ NULL,
+ &error)) {
+ g_dbus_method_invocation_return_gerror (invocation, error);
+ g_clear_error (&error);
+ } else {
+ e_dbus_address_book_cursor_complete_set_alphabetic_index (
+ dbus_object,
+ invocation,
+ cursor->priv->total,
+ cursor->priv->position);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+data_book_cursor_handle_set_query (EDBusAddressBookCursor *dbus_object,
+ GDBusMethodInvocation *invocation,
+ const gchar *query,
+ EDataBookCursor *cursor)
+{
+ GError *error = NULL;
+
+ if (!e_data_book_cursor_set_sexp (cursor, query, NULL, &error)) {
+ g_dbus_method_invocation_return_gerror (invocation, error);
+ g_clear_error (&error);
+ } else {
+ e_dbus_address_book_cursor_complete_set_query (
+ dbus_object,
+ invocation,
+ cursor->priv->total,
+ cursor->priv->position);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+data_book_cursor_handle_dispose (EDBusAddressBookCursor *dbus_object,
+ GDBusMethodInvocation *invocation,
+ EDataBookCursor *cursor)
+{
+ EDataBookCursorPrivate *priv = cursor->priv;
+ GError *error = NULL;
+
+ /* The backend will release the cursor, just make sure that
+ * we survive long enough to complete this method call
+ */
+ g_object_ref (cursor);
+
+ /* This should never really happen, but if it does, there is no
+ * we cannot expect the client to recover well from an error at
+ * dispose time, so let's just log the warning.
+ */
+ if (!e_book_backend_delete_cursor (priv->backend, cursor, &error)) {
+ g_warning ("Error trying to delete cursor: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ e_dbus_address_book_cursor_complete_dispose (dbus_object, invocation);
+
+ g_object_unref (cursor);
+
+ return TRUE;
+}
+
+/************************************************
+ * API *
+ ************************************************/
+
+/**
+ * e_data_book_cursor_get_backend:
+ * @cursor: an #EDataBookCursor
+ *
+ * Gets the backend which created and owns @cursor.
+ *
+ * Returns: (transfer none): The #EBookBackend owning @cursor.
+ *
+ * Since: 3.12
+ */
+struct _EBookBackend *
+e_data_book_cursor_get_backend (EDataBookCursor *cursor)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR (cursor), NULL);
+
+ return cursor->priv->backend;
+}
+
+/**
+ * e_data_book_cursor_get_total:
+ * @cursor: an #EDataBookCursor
+ *
+ * Fetch the total number of contacts which match @cursor's query expression.
+ *
+ * Returns: the total contacts for @cursor
+ *
+ * Since: 3.12
+ */
+gint
+e_data_book_cursor_get_total (EDataBookCursor *cursor)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR (cursor), -1);
+
+ return cursor->priv->total;
+}
+
+/**
+ * e_data_book_cursor_get_position:
+ * @cursor: an #EDataBookCursor
+ *
+ * Fetch the current position of @cursor in its result list.
+ *
+ * Returns: the current position of @cursor
+ *
+ * Since: 3.12
+ */
+gint
+e_data_book_cursor_get_position (EDataBookCursor *cursor)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR (cursor), -1);
+
+ return cursor->priv->position;
+}
+
+/**
+ * e_data_book_cursor_set_sexp:
+ * @cursor: an #EDataBookCursor
+ * @sexp: (allow-none): the search expression to set
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Sets the search expression for the cursor
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_data_book_cursor_set_sexp (EDataBookCursor *cursor,
+ const gchar *sexp,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GError *local_error = NULL;
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR (cursor), FALSE);
+
+ g_object_ref (cursor);
+
+ if (E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->set_sexp) {
+ success = (* E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->set_sexp) (cursor,
+ sexp,
+ error);
+
+ } else {
+ g_set_error_literal (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ _("Cursor does not support setting the search expression"));
+ }
+
+ /* We already set the new search expression,
+ * we can't fail anymore so just fire a warning
+ */
+ if (success &&
+ !e_data_book_cursor_recalculate (cursor, cancellable, &local_error)) {
+ g_warning (
+ "Failed to recalculate the cursor value "
+ "after setting the search expression: %s",
+ local_error->message);
+ g_clear_error (&local_error);
+ }
+
+ g_object_unref (cursor);
+
+ return success;
+}
+
+/**
+ * e_data_book_cursor_step:
+ * @cursor: an #EDataBookCursor
+ * @revision_guard: The expected current addressbook revision, or %NULL
+ * @flags: The #EBookCursorStepFlags for this step
+ * @origin: The #EBookCursorOrigin from whence to step
+ * @count: a positive or negative amount of contacts to try and fetch
+ * @results: (out) (allow-none) (element-type utf8) (transfer full):
+ * A return location to store the results, or %NULL if %E_BOOK_CURSOR_STEP_FETCH is not specified in %flags
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Steps @cursor through it's sorted query by a maximum of @count contacts
+ * starting from @origin.
+ *
+ * If @count is negative, then the cursor will move through the list in reverse.
+ *
+ * If @cursor reaches the beginning or end of the query results, then the
+ * returned list might not contain the amount of desired contacts, or might
+ * return no results if the cursor currently points to the last contact.
+ * Reaching the end of the list is not considered an error condition. Attempts
+ * to step beyond the end of the list after having reached the end of the list
+ * will however trigger an %E_CLIENT_ERROR_QUERY_REFUSED error.
+ *
+ * If %E_BOOK_CURSOR_STEP_FETCH is specified in %flags, a pointer to
+ * a %NULL #GSList pointer should be provided for the @results parameter.
+ *
+ * The result list will be stored to @results and should be freed with g_slist_free()
+ * and all elements freed with g_free().
+ *
+ * If a @revision_guard is specified, the cursor implementation will issue an
+ * %E_CLIENT_ERROR_OUT_OF_SYNC error if the @revision_guard does not match
+ * the current addressbook revision.
+ *
+ * An explanation of how stepping is expected to behave can be found
+ * in the <link linkend="cursor-iteration">user facing reference documentation</link>.
+ *
+ * Returns: The number of contacts traversed if successful, otherwise -1 is
+ * returned and @error is set.
+ *
+ * Since: 3.12
+ */
+gint
+e_data_book_cursor_step (EDataBookCursor *cursor,
+ const gchar *revision_guard,
+ EBookCursorStepFlags flags,
+ EBookCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gint retval;
+
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR (cursor), FALSE);
+ g_return_val_if_fail ((flags & E_BOOK_CURSOR_STEP_FETCH) == 0 ||
+ (results != NULL && *results == NULL), -1);
+
+ if (!E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->step) {
+ g_set_error_literal (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ _("Cursor does not support step"));
+ return FALSE;
+ }
+
+ g_object_ref (cursor);
+ retval = (* E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->step) (cursor,
+ revision_guard,
+ flags,
+ origin,
+ count,
+ results,
+ cancellable,
+ error);
+ g_object_unref (cursor);
+
+ if (retval >= 0 && (flags & E_BOOK_CURSOR_STEP_MOVE) != 0) {
+
+ calculate_step_position (cursor, origin, count, retval);
+ }
+
+ return retval;
+}
+
+/**
+ * e_data_book_cursor_set_alphabetic_index:
+ * @cursor: an #EDataBookCursor
+ * @index: the alphabetic index
+ * @locale: the locale in which @index is expected to be a valid alphabetic index
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Sets the @cursor position to an
+ * <link linkend="cursor-alphabet">Alphabetic Index</link>
+ * into the alphabet active in the @locale of the addressbook.
+ *
+ * After setting the target to an alphabetic index, for example the
+ * index for letter 'E', then further calls to e_data_book_cursor_step()
+ * will return results starting with the letter 'E' (or results starting
+ * with the last result in 'D', if moving in a negative direction).
+ *
+ * The passed index must be a valid index in @locale, if by some chance
+ * the addressbook backend has changed into a new locale after this
+ * call has been issued, an %E_CLIENT_ERROR_OUT_OF_SYNC error will be
+ * issued indicating that there was a locale mismatch.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_data_book_cursor_set_alphabetic_index (EDataBookCursor *cursor,
+ gint index,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GError *local_error = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR (cursor), FALSE);
+
+ g_object_ref (cursor);
+
+ if (E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->set_alphabetic_index) {
+ success = (* E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->set_alphabetic_index) (cursor,
+ index,
+ locale,
+ error);
+
+ /* We already set the new cursor value, we can't fail anymore so just fire a warning */
+ if (!e_data_book_cursor_recalculate (cursor, cancellable, &local_error)) {
+ g_warning (
+ "Failed to recalculate the cursor value "
+ "after setting the alphabetic index: %s",
+ local_error->message);
+ g_clear_error (&local_error);
+ }
+
+ } else {
+ g_set_error_literal (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_NOT_SUPPORTED,
+ _("Cursor does not support alphabetic indexes"));
+ success = FALSE;
+ }
+
+ g_object_unref (cursor);
+
+ return success;
+}
+
+/**
+ * e_data_book_cursor_recalculate:
+ * @cursor: an #EDataBookCursor
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Recalculates the cursor's total and position, this is meant
+ * for cursor created in Direct Read Access mode to synchronously
+ * recalculate the position and total values when the addressbook
+ * revision has changed.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_data_book_cursor_recalculate (EDataBookCursor *cursor,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gint total = 0;
+ gint position = 0;
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR (cursor), FALSE);
+
+ /* Bad programming error */
+ if (!E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->get_position) {
+ g_critical (
+ "EDataBookCursor.get_position() unimplemented on type '%s'",
+ G_OBJECT_TYPE_NAME (cursor));
+
+ return FALSE;
+ }
+
+ g_object_ref (cursor);
+ success = (* E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->get_position) (cursor,
+ &total,
+ &position,
+ cancellable,
+ error);
+ g_object_unref (cursor);
+
+ if (success)
+ data_book_cursor_set_values (cursor, total, position);
+
+ return success;
+}
+
+/**
+ * e_data_book_cursor_load_locale:
+ * @cursor: an #EDataBookCursor
+ * @locale: (out) (allow-none): return location for the locale
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Load the current locale setting from the cursor's underlying database.
+ *
+ * Addressbook backends implementing cursors should call this function on all active
+ * cursor when the locale setting changes.
+ *
+ * This will implicitly reset @cursor's state and position.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_data_book_cursor_load_locale (EDataBookCursor *cursor,
+ gchar **locale,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EDataBookCursorPrivate *priv;
+ gboolean success;
+ gchar *local_locale = NULL;
+
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR (cursor), FALSE);
+
+ priv = cursor->priv;
+
+ if (!E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->load_locale) {
+ g_critical (
+ "EDataBookCursor.load_locale() unimplemented on type '%s'",
+ G_OBJECT_TYPE_NAME (cursor));
+ return FALSE;
+ }
+
+ g_object_ref (cursor);
+ success = (* E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->load_locale) (cursor, &local_locale, error);
+ g_object_unref (cursor);
+
+ /* Changed ! Reset the thing */
+ if (g_strcmp0 (priv->locale, local_locale) != 0) {
+ GError *local_error = NULL;
+
+ g_free (priv->locale);
+ priv->locale = g_strdup (local_locale);
+
+ if (e_data_book_cursor_step (cursor, NULL,
+ E_BOOK_CURSOR_STEP_MOVE,
+ E_BOOK_CURSOR_ORIGIN_BEGIN,
+ 0, NULL, cancellable, &local_error) < 0) {
+ g_warning (
+ "Error resetting cursor position after locale change: %s",
+ local_error->message);
+ g_clear_error (&local_error);
+ } else if (!e_data_book_cursor_recalculate (E_DATA_BOOK_CURSOR (cursor),
+ cancellable, &local_error)) {
+ g_warning (
+ "Error recalculating cursor position after locale change: %s",
+ local_error->message);
+ g_clear_error (&local_error);
+ }
+ }
+
+ if (locale)
+ *locale = local_locale;
+ else
+ g_free (local_locale);
+
+ return success;
+}
+
+/**
+ * e_data_book_cursor_contact_added:
+ * @cursor: an #EDataBookCursor
+ * @contact: the #EContact which was added to the addressbook
+ *
+ * Should be called by addressbook backends whenever a contact
+ * is added.
+ *
+ * Since: 3.12
+ */
+void
+e_data_book_cursor_contact_added (EDataBookCursor *cursor,
+ EContact *contact)
+{
+ EDataBookCursorPrivate *priv;
+ gint comparison = 0;
+ gboolean matches_sexp = FALSE;
+ gint new_total, new_position;
+
+ g_return_if_fail (E_IS_DATA_BOOK_CURSOR (cursor));
+ g_return_if_fail (E_IS_CONTACT (contact));
+
+ priv = cursor->priv;
+
+ comparison = data_book_cursor_compare_contact (cursor, contact, &matches_sexp);
+
+ /* The added contact doesn't match the cursor search expression, no need
+ * to change the position & total values
+ */
+ if (!matches_sexp)
+ return;
+
+ new_total = priv->total;
+ new_position = priv->position;
+
+ /* One new contact */
+ new_total++;
+
+ /* New contact was added at cursor position or before cursor position */
+ if (comparison <= 0)
+ new_position++;
+
+ /* Notify total & position change */
+ data_book_cursor_set_values (cursor, new_total, new_position);
+}
+
+/**
+ * e_data_book_cursor_contact_removed:
+ * @cursor: an #EDataBookCursor
+ * @contact: the #EContact which was removed from the addressbook
+ *
+ * Should be called by addressbook backends whenever a contact
+ * is removed.
+ *
+ * Since: 3.12
+ */
+void
+e_data_book_cursor_contact_removed (EDataBookCursor *cursor,
+ EContact *contact)
+{
+ EDataBookCursorPrivate *priv;
+ gint comparison = 0;
+ gboolean matches_sexp = FALSE;
+ gint new_total, new_position;
+
+ g_return_if_fail (E_IS_DATA_BOOK_CURSOR (cursor));
+ g_return_if_fail (E_IS_CONTACT (contact));
+
+ priv = cursor->priv;
+
+ comparison = data_book_cursor_compare_contact (cursor, contact, &matches_sexp);
+
+ /* The removed contact did not match the cursor search expression, no need
+ * to change the position & total values
+ */
+ if (!matches_sexp)
+ return;
+
+ new_total = priv->total;
+ new_position = priv->position;
+
+ /* One less contact */
+ new_total--;
+
+ /* Removed contact was the exact cursor position or before cursor position */
+ if (comparison <= 0)
+ new_position--;
+
+ /* Notify total & position change */
+ data_book_cursor_set_values (cursor, new_total, new_position);
+}
+
+/**
+ * e_data_book_cursor_register_gdbus_object:
+ * @cursor: an #EDataBookCursor
+ * @connection: the #GDBusConnection to register with
+ * @object_path: the object path to place the direct access configuration data
+ * @error: (out) (allow-none): a location to store any error which might occur while registering
+ *
+ * Places @cursor on the @connection at @object_path
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_data_book_cursor_register_gdbus_object (EDataBookCursor *cursor,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error)
+{
+ EDataBookCursorPrivate *priv;
+
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR (cursor), FALSE);
+ g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), FALSE);
+ g_return_val_if_fail (object_path != NULL, FALSE);
+
+ priv = cursor->priv;
+
+ if (!priv->dbus_object) {
+ priv->dbus_object = e_dbus_address_book_cursor_skeleton_new ();
+
+ g_signal_connect (
+ priv->dbus_object, "handle-step",
+ G_CALLBACK (data_book_cursor_handle_step), cursor);
+ g_signal_connect (
+ priv->dbus_object, "handle-set-alphabetic-index",
+ G_CALLBACK (data_book_cursor_handle_set_alphabetic_index), cursor);
+ g_signal_connect (
+ priv->dbus_object, "handle-set-query",
+ G_CALLBACK (data_book_cursor_handle_set_query), cursor);
+ g_signal_connect (
+ priv->dbus_object, "handle-dispose",
+ G_CALLBACK (data_book_cursor_handle_dispose), cursor);
+
+ /* Set initial total / position */
+ e_dbus_address_book_cursor_set_total (priv->dbus_object, priv->total);
+ e_dbus_address_book_cursor_set_position (priv->dbus_object, priv->position);
+ }
+
+ return g_dbus_interface_skeleton_export (
+ G_DBUS_INTERFACE_SKELETON (priv->dbus_object),
+ connection, object_path, error);
+}
diff --git a/src/addressbook/libedata-book/e-data-book-cursor.h b/src/addressbook/libedata-book/e-data-book-cursor.h
new file mode 100644
index 000000000..9813d0da0
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-cursor.h
@@ -0,0 +1,318 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_DATA_BOOK_CURSOR_H
+#define E_DATA_BOOK_CURSOR_H
+
+#include <gio/gio.h>
+#include <libebook-contacts/libebook-contacts.h>
+
+#define E_TYPE_DATA_BOOK_CURSOR (e_data_book_cursor_get_type ())
+#define E_DATA_BOOK_CURSOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), E_TYPE_DATA_BOOK_CURSOR, EDataBookCursor))
+#define E_DATA_BOOK_CURSOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), E_TYPE_DATA_BOOK_CURSOR, EDataBookCursorClass))
+#define E_IS_DATA_BOOK_CURSOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), E_TYPE_DATA_BOOK_CURSOR))
+#define E_IS_DATA_BOOK_CURSOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), E_TYPE_DATA_BOOK_CURSOR))
+#define E_DATA_BOOK_CURSOR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), E_TYPE_DATA_BOOK_CURSOR, EDataBookCursorClass))
+
+G_BEGIN_DECLS
+
+struct _EBookBackend;
+
+typedef struct _EDataBookCursor EDataBookCursor;
+typedef struct _EDataBookCursorClass EDataBookCursorClass;
+typedef struct _EDataBookCursorPrivate EDataBookCursorPrivate;
+
+/*
+ * The following virtual methods have typedefs in order to provide richer
+ * documentation about how to implement the EDataBookCursorClass.
+ */
+
+/**
+ * EDataBookCursorSetSexpFunc:
+ * @cursor: an #EDataBookCursor
+ * @sexp: (allow-none): the search expression to set, or %NULL for unfiltered results
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Method type for #EDataBookCursorClass.set_sexp()
+ *
+ * A cursor implementation must implement this in order to modify the search
+ * expression for @cursor. After this is called, the position and total will
+ * be recalculated.
+ *
+ * If the cursor implementation is unable to deal with the #EContactFields
+ * referred to in @sexp, then an %E_CLIENT_ERROR_INVALID_QUERY error should
+ * be set to indicate this.
+ *
+ * Returns: %TRUE on Success, otherwise %FALSE is returned if any error occurred
+ * and @error is set to reflect the error which occurred.
+ *
+ * Since: 3.12
+ */
+typedef gboolean (*EDataBookCursorSetSexpFunc) (EDataBookCursor *cursor,
+ const gchar *sexp,
+ GError **error);
+
+/**
+ * EDataBookCursorStepFunc:
+ * @cursor: an #EDataBookCursor
+ * @revision_guard: (allow-none): The expected current addressbook revision, or %NULL
+ * @flags: The #EBookCursorStepFlags for this step
+ * @origin: The #EBookCursorOrigin from whence to step
+ * @count: a positive or negative amount of contacts to try and fetch
+ * @results: (out) (allow-none) (element-type utf8) (transfer full):
+ * A return location to store the results, or %NULL if %E_BOOK_CURSOR_STEP_FETCH is not specified in %flags
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Method type for #EDataBookCursorClass.step()
+ *
+ * As all cursor methods may be called either by the addressbook service or
+ * directly by a client in Direct Read Access mode, it is important that the
+ * operation be an atomic transaction with the underlying database.
+ *
+ * The @revision_guard, if specified, will be set to the %CLIENT_BACKEND_PROPERTY_REVISION
+ * value at the time which the given client issued the call to move the cursor.
+ * If the @revision_guard provided by the client does not match the stored addressbook
+ * revision, then an %E_CLIENT_ERROR_OUT_OF_SYNC error should be set to indicate
+ * that the revision was out of sync while attempting to move the cursor.
+ *
+ * <note><para>If the addressbook backend supports direct read access, then the
+ * revision comparison and reading of the data store must be coupled into a
+ * single atomic operation (the data read back from the store must be the correct
+ * data for the given addressbook revision).</para></note>
+ *
+ * See e_data_book_cursor_step() for more details on the expected behaviour of this method.
+ *
+ * Returns: The number of contacts traversed if successfull, otherwise -1 is
+ * returned and @error is set.
+ *
+ * Since: 3.12
+ */
+typedef gint (*EDataBookCursorStepFunc) (EDataBookCursor *cursor,
+ const gchar *revision_guard,
+ EBookCursorStepFlags flags,
+ EBookCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * EDataBookCursorSetAlphabetIndexFunc:
+ * @cursor: an #EDataBookCursor
+ * @index: the alphabetic index
+ * @locale: the locale in which @index is expected to be a valid alphabetic index
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Method type for #EDataBookCursorClass.set_alphabetic_index()
+ *
+ * Sets the cursor state to point to an
+ * <link linkend="cursor-alphabet">index into the active alphabet</link>.
+ *
+ * The implementing class must check that @locale matches the current
+ * locale setting of the underlying database and report an %E_CLIENT_ERROR_OUT_OF_SYNC
+ * error in the case that the locales do not match.
+ *
+ * Returns: %TRUE on Success, otherwise %FALSE is returned if any error occurred
+ * and @error is set to reflect the error which occurred.
+ *
+ * Since: 3.12
+ */
+typedef gboolean (*EDataBookCursorSetAlphabetIndexFunc) (EDataBookCursor *cursor,
+ gint index,
+ const gchar *locale,
+ GError **error);
+
+/**
+ * EDataBookCursorGetPositionFunc:
+ * @cursor: an #EDataBookCursor
+ * @total: (out): The total number of contacts matching @cursor's query expression
+ * @position: (out): The current position of @cursor in it's result list
+ * @cancellable: (allow-none): A #GCancellable
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Method type for #EDataBookCursorClass.get_position()
+ *
+ * Cursor implementations must implement this to count the total results
+ * matching @cursor's query expression and to calculate the amount of contacts
+ * leading up to the current cursor state (cursor inclusive).
+ *
+ * A cursor position is defined as an integer which is inclusive of the
+ * current contact to which it points (if the cursor points to an exact
+ * contact). A position of %0 indicates that the cursor is situated in
+ * a position that is before and after the entire result set. The cursor
+ * position should be %0 at creation time, and should start again from
+ * the symbolic %0 position whenever %E_BOOK_CURSOR_ORIGIN_BEGIN is
+ * specified in the #EDataBookCursorClass.step() method (or whenever
+ * moving the cursor beyond the end of the result set).
+ *
+ * If the cursor is positioned beyond the end of the list, then
+ * the position should be the total amount of contacts available
+ * in the list (as returned through the @total argument) plus one.
+ *
+ * This method is called by e_data_book_cursor_recalculate() and in some
+ * other cases where @cursor's current position and total must be
+ * recalculated from scratch.
+ *
+ * Returns: %TRUE on Success, otherwise %FALSE is returned if any error occurred
+ * and @error is set to reflect the error which occurred.
+ *
+ * Since: 3.12
+ */
+typedef gboolean (*EDataBookCursorGetPositionFunc) (EDataBookCursor *cursor,
+ gint *total,
+ gint *position,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * EDataBookCursorCompareContactFunc:
+ * @cursor: an #EDataBookCursor
+ * @contact: the #EContact to compare with @cursor
+ * @matches_sexp: (out) (allow-none): return location to set whether @contact matched @cursor's search expression
+ *
+ * Method type for #EDataBookCursorClass.compare_contact()
+ *
+ * Cursor implementations must implement this in order to compare a
+ * contact with the current cursor state.
+ *
+ * This is called when the addressbook backends notify active cursors
+ * that the addressbook has been modified with e_data_book_cursor_contact_added() and
+ * e_data_book_cursor_contact_removed().
+ *
+ * Returns: A value that is less than, equal to, or greater than zero if @contact is found,
+ * respectively, to be less than, to match, or be greater than the current value of @cursor.
+ *
+ * Since: 3.12
+ */
+typedef gint (*EDataBookCursorCompareContactFunc) (EDataBookCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp);
+
+/**
+ * EDataBookCursorLoadLocaleFunc:
+ * @cursor: an #EDataBookCursor
+ * @locale: (out) (transfer full): return location to store the newly loaded locale
+ * @error: (out) (allow-none): return location for a #GError, or %NULL
+ *
+ * Method type for #EDataBookCursorClass.load_locale()
+ *
+ * Fetches the locale setting from @cursor's addressbook
+ *
+ * If the locale setting has changed, the cursor must reload any
+ * internal locale specific data and ensure that comparisons of
+ * sort keys will function properly in the new locale.
+ *
+ * Upon locale changes, the implementation need not worry about
+ * updating it's current cursor state, the cursor state will be
+ * reset automatically for you.
+ *
+ * Returns: %TRUE on Success, otherwise %FALSE is returned if any error occurred
+ * and @error is set to reflect the error which occurred.
+ *
+ * Since: 3.12
+ */
+typedef gboolean (*EDataBookCursorLoadLocaleFunc) (EDataBookCursor *cursor,
+ gchar **locale,
+ GError **error);
+
+/**
+ * EDataBookCursor:
+ *
+ * An opaque handle for an addressbook cursor
+ *
+ * Since: 3.12
+ */
+struct _EDataBookCursor {
+ GObject parent;
+
+ EDataBookCursorPrivate *priv;
+};
+
+/**
+ * EDataBookCursorClass:
+ * @set_sexp: The #EDataBookCursorSetSexpFunc delegate to set the search expression
+ * @step: The #EDataBookCursorStepFunc delegate to navigate the cursor
+ * @set_alphabetic_index: The #EDataBookCursorSetAlphabetIndexFunc delegate to set the alphabetic position
+ * @get_position: The #EDataBookCursorGetPositionFunc delegate to calculate the current total and position values
+ * @compare_contact: The #EDataBookCursorCompareContactFunc delegate to compare an #EContact with the the cursor position
+ * @load_locale: The #EDataBookCursorLoadLocaleFunc delegate used to reload the locale setting
+ *
+ * Methods to implement on an #EDataBookCursor concrete class.
+ *
+ * Since: 3.12
+ */
+struct _EDataBookCursorClass {
+ /*< private >*/
+ GObjectClass parent;
+
+ /*< public >*/
+ EDataBookCursorSetSexpFunc set_sexp;
+ EDataBookCursorStepFunc step;
+ EDataBookCursorSetAlphabetIndexFunc set_alphabetic_index;
+ EDataBookCursorGetPositionFunc get_position;
+ EDataBookCursorCompareContactFunc compare_contact;
+ EDataBookCursorLoadLocaleFunc load_locale;
+};
+
+GType e_data_book_cursor_get_type (void);
+
+struct _EBookBackend *e_data_book_cursor_get_backend (EDataBookCursor *cursor);
+gint e_data_book_cursor_get_total (EDataBookCursor *cursor);
+gint e_data_book_cursor_get_position (EDataBookCursor *cursor);
+gboolean e_data_book_cursor_set_sexp (EDataBookCursor *cursor,
+ const gchar *sexp,
+ GCancellable *cancellable,
+ GError **error);
+gint e_data_book_cursor_step (EDataBookCursor *cursor,
+ const gchar *revision_guard,
+ EBookCursorStepFlags flags,
+ EBookCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_data_book_cursor_set_alphabetic_index (EDataBookCursor *cursor,
+ gint index,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_data_book_cursor_recalculate (EDataBookCursor *cursor,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_data_book_cursor_load_locale (EDataBookCursor *cursor,
+ gchar **locale,
+ GCancellable *cancellable,
+ GError **error);
+void e_data_book_cursor_contact_added (EDataBookCursor *cursor,
+ EContact *contact);
+void e_data_book_cursor_contact_removed (EDataBookCursor *cursor,
+ EContact *contact);
+gboolean e_data_book_cursor_register_gdbus_object (EDataBookCursor *cursor,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* E_DATA_BOOK_CURSOR_H */
diff --git a/src/addressbook/libedata-book/e-data-book-direct.c b/src/addressbook/libedata-book/e-data-book-direct.c
new file mode 100644
index 000000000..776fb4fd3
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-direct.c
@@ -0,0 +1,144 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2012 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+/**
+ * SECTION: e-data-book-direct
+ * @include: libedata-book/libedata-book.h
+ * @short_description: An interface for implementing Direct Read Access
+ *
+ * This class should be created by an #EBookBackendClass.get_direct_book()
+ * implementation of a backend which supports direct read access.
+ *
+ * This will only be asked of the backend when instantiated on the server
+ * side. If the server side instance of an #EBookBackend does return
+ * an #EDataBookDirect, then a client side instance of the same backend
+ * will be created and #EBookBackendClass.configure_direct() will be
+ * called on the corresponding client side instance.
+ **/
+#include "evolution-data-server-config.h"
+
+#include <string.h>
+
+#include <e-dbus-direct-book.h>
+#include "e-data-book-direct.h"
+
+#define E_DATA_BOOK_DIRECT_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_DATA_BOOK_DIRECT, EDataBookDirectPrivate))
+
+G_DEFINE_TYPE (EDataBookDirect, e_data_book_direct, G_TYPE_OBJECT);
+#define THRESHOLD_ITEMS 32 /* how many items can be hold in a cache, before propagated to UI */
+#define THRESHOLD_SECONDS 2 /* how long to wait until notifications are propagated to UI; in seconds */
+
+struct _EDataBookDirectPrivate {
+ EDBusDirectBook *gdbus_object;
+};
+
+/* GObjectClass */
+static void
+e_data_book_direct_dispose (GObject *object)
+{
+ EDataBookDirect *direct = E_DATA_BOOK_DIRECT (object);
+
+ if (direct->priv->gdbus_object) {
+ g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (direct->priv->gdbus_object));
+ g_object_unref (direct->priv->gdbus_object);
+ direct->priv->gdbus_object = NULL;
+ }
+
+ G_OBJECT_CLASS (e_data_book_direct_parent_class)->dispose (object);
+}
+
+static void
+e_data_book_direct_init (EDataBookDirect *direct)
+{
+ direct->priv = E_DATA_BOOK_DIRECT_GET_PRIVATE (direct);
+ direct->priv->gdbus_object = e_dbus_direct_book_skeleton_new ();
+}
+
+static void
+e_data_book_direct_class_init (EDataBookDirectClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ g_type_class_add_private (class, sizeof (EDataBookDirectPrivate));
+
+ object_class->dispose = e_data_book_direct_dispose;
+}
+
+/**
+ * e_data_book_direct_new:
+ * @backend_path: Full path to the installed backend shared library
+ * @backend_factory_name: Type name of the EBookBackendFactory implemented by the library
+ * @config: A backend specific configuration string
+ *
+ * Creates a #EDataBookDirect to report configuration data needed for direct
+ * read access.
+ *
+ * This is returned by e_book_backend_get_direct_book() for backends
+ * which support direct read access mode.
+ *
+ * Returns: (transfer full): A newly created #EDataBookDirect
+ *
+ * Since: 3.8
+ */
+EDataBookDirect *
+e_data_book_direct_new (const gchar *backend_path,
+ const gchar *backend_factory_name,
+ const gchar *config)
+{
+ EDataBookDirect *direct;
+
+ g_return_val_if_fail (backend_path && backend_path[0], NULL);
+ g_return_val_if_fail (backend_factory_name && backend_factory_name[0], NULL);
+
+ direct = g_object_new (E_TYPE_DATA_BOOK_DIRECT, NULL);
+
+ e_dbus_direct_book_set_backend_path (direct->priv->gdbus_object, backend_path);
+ e_dbus_direct_book_set_backend_name (direct->priv->gdbus_object, backend_factory_name);
+ e_dbus_direct_book_set_backend_config (direct->priv->gdbus_object, config);
+
+ return direct;
+}
+
+/**
+ * e_data_book_direct_register_gdbus_object:
+ * @direct: An #EDataBookDirect
+ * @connection: The #GDBusConnection to register with
+ * @object_path: The object path to place the direct access configuration data
+ * @error: A location to store any error which might occur while registering
+ *
+ * Places @direct on the @connection at @object_path
+ *
+ * Since: 3.8
+ **/
+gboolean
+e_data_book_direct_register_gdbus_object (EDataBookDirect *direct,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_DIRECT (direct), FALSE);
+ g_return_val_if_fail (connection != NULL, FALSE);
+ g_return_val_if_fail (object_path != NULL, 0);
+
+ return g_dbus_interface_skeleton_export (
+ G_DBUS_INTERFACE_SKELETON (direct->priv->gdbus_object),
+ connection, object_path, error);
+}
diff --git a/src/addressbook/libedata-book/e-data-book-direct.h b/src/addressbook/libedata-book/e-data-book-direct.h
new file mode 100644
index 000000000..d5b373d46
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-direct.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2012 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb@openismus.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_DATA_BOOK_DIRECT_H
+#define E_DATA_BOOK_DIRECT_H
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define E_TYPE_DATA_BOOK_DIRECT (e_data_book_direct_get_type ())
+#define E_DATA_BOOK_DIRECT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), E_TYPE_DATA_BOOK_DIRECT, EDataBookDirect))
+#define E_DATA_BOOK_DIRECT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), E_TYPE_DATA_BOOK_DIRECT, EDataBookDirectClass))
+#define E_IS_DATA_BOOK_DIRECT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), E_TYPE_DATA_BOOK_DIRECT))
+#define E_IS_DATA_BOOK_DIRECT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), E_TYPE_DATA_BOOK_DIRECT))
+#define E_DATA_BOOK_DIRECT_GET_CLASS(k) (G_TYPE_INSTANCE_GET_CLASS ((obj), E_TYPE_DATA_BOOK_DIRECT, EDataBookDirect))
+
+typedef struct _EDataBookDirect EDataBookDirect;
+typedef struct _EDataBookDirectClass EDataBookDirectClass;
+typedef struct _EDataBookDirectPrivate EDataBookDirectPrivate;
+
+struct _EDataBookDirect {
+ GObject parent;
+ EDataBookDirectPrivate *priv;
+};
+
+struct _EDataBookDirectClass {
+ GObjectClass parent;
+};
+
+GType e_data_book_direct_get_type (void);
+EDataBookDirect * e_data_book_direct_new (const gchar *backend_path,
+ const gchar *backend_factory_name,
+ const gchar *config);
+
+gboolean e_data_book_direct_register_gdbus_object (EDataBookDirect *direct,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* E_DATA_BOOK_DIRECT_H */
diff --git a/src/addressbook/libedata-book/e-data-book-factory.c b/src/addressbook/libedata-book/e-data-book-factory.c
new file mode 100644
index 000000000..ba1806834
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-factory.c
@@ -0,0 +1,200 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2006 OpenedHand Ltd
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Ross Burton <ross@linux.intel.com>
+ */
+
+/**
+ * SECTION: e-data-book-factory
+ * @include: libedata-book/libedata-book.h
+ * @short_description: The main addressbook server object
+ *
+ * This class handles incomming D-Bus connections and creates
+ * the #EDataBook layer for server side addressbooks to communicate
+ * with client side #EBookClient objects.
+ **/
+#include "evolution-data-server-config.h"
+
+#include <locale.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <glib/gi18n.h>
+
+/* Private D-Bus classes. */
+#include <e-dbus-address-book-factory.h>
+
+#include "e-book-backend.h"
+#include "e-book-backend-factory.h"
+#include "e-data-book.h"
+#include "e-data-book-factory.h"
+
+#define d(x)
+
+#define E_DATA_BOOK_FACTORY_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_DATA_BOOK_FACTORY, EDataBookFactoryPrivate))
+
+struct _EDataBookFactoryPrivate {
+ EDBusAddressBookFactory *dbus_factory;
+};
+
+/* Forward Declarations */
+static void e_data_book_factory_initable_init
+ (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (
+ EDataBookFactory,
+ e_data_book_factory,
+ E_TYPE_DATA_FACTORY,
+ G_IMPLEMENT_INTERFACE (
+ G_TYPE_INITABLE,
+ e_data_book_factory_initable_init))
+
+static GDBusInterfaceSkeleton *
+data_book_factory_get_dbus_interface_skeleton (EDBusServer *server)
+{
+ EDataBookFactory *factory;
+
+ factory = E_DATA_BOOK_FACTORY (server);
+
+ return G_DBUS_INTERFACE_SKELETON (factory->priv->dbus_factory);
+}
+
+static const gchar *
+data_book_get_factory_name (EBackendFactory *backend_factory)
+{
+ EBookBackendFactoryClass *class;
+
+ class = E_BOOK_BACKEND_FACTORY_GET_CLASS (E_BOOK_BACKEND_FACTORY (backend_factory));
+
+ return class->factory_name;
+}
+
+static void
+data_book_complete_open (EDataFactory *data_factory,
+ GDBusMethodInvocation *invocation,
+ const gchar *object_path,
+ const gchar *bus_name,
+ const gchar *extension_name)
+{
+ EDataBookFactory *data_book_factory = E_DATA_BOOK_FACTORY (data_factory);
+
+ e_dbus_address_book_factory_complete_open_address_book (
+ data_book_factory->priv->dbus_factory, invocation, object_path, bus_name);
+}
+
+static gchar *overwrite_subprocess_book_path = NULL;
+
+static gboolean
+data_book_factory_handle_open_address_book_cb (EDBusAddressBookFactory *iface,
+ GDBusMethodInvocation *invocation,
+ const gchar *uid,
+ EDataBookFactory *factory)
+{
+ EDataFactory *data_factory = E_DATA_FACTORY (factory);
+
+ e_data_factory_spawn_subprocess_backend (
+ data_factory, invocation, uid, E_SOURCE_EXTENSION_ADDRESS_BOOK,
+ overwrite_subprocess_book_path ? overwrite_subprocess_book_path : SUBPROCESS_BOOK_BACKEND_PATH);
+
+ return TRUE;
+}
+
+static void
+data_book_factory_dispose (GObject *object)
+{
+ EDataBookFactory *factory;
+ EDataBookFactoryPrivate *priv;
+
+ factory = E_DATA_BOOK_FACTORY (object);
+ priv = factory->priv;
+
+ g_clear_object (&priv->dbus_factory);
+
+ /* Chain up to parent's dispose() method. */
+ G_OBJECT_CLASS (e_data_book_factory_parent_class)->dispose (object);
+}
+
+static void
+e_data_book_factory_class_init (EDataBookFactoryClass *class)
+{
+ GObjectClass *object_class;
+ EDBusServerClass *dbus_server_class;
+ EDataFactoryClass *data_factory_class;
+ const gchar *modules_directory = BACKENDDIR;
+ const gchar *modules_directory_env;
+ const gchar *subprocess_book_path_env;
+
+ modules_directory_env = g_getenv (EDS_ADDRESS_BOOK_MODULES);
+ if (modules_directory_env &&
+ g_file_test (modules_directory_env, G_FILE_TEST_IS_DIR))
+ modules_directory = g_strdup (modules_directory_env);
+
+ subprocess_book_path_env = g_getenv (EDS_SUBPROCESS_BOOK_PATH);
+ if (subprocess_book_path_env &&
+ g_file_test (subprocess_book_path_env, G_FILE_TEST_IS_EXECUTABLE))
+ overwrite_subprocess_book_path = g_strdup (subprocess_book_path_env);
+
+ g_type_class_add_private (class, sizeof (EDataBookFactoryPrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->dispose = data_book_factory_dispose;
+
+ dbus_server_class = E_DBUS_SERVER_CLASS (class);
+ dbus_server_class->bus_name = ADDRESS_BOOK_DBUS_SERVICE_NAME;
+ dbus_server_class->module_directory = modules_directory;
+
+ data_factory_class = E_DATA_FACTORY_CLASS (class);
+ data_factory_class->backend_factory_type = E_TYPE_BOOK_BACKEND_FACTORY;
+ data_factory_class->factory_object_path = "/org/gnome/evolution/dataserver/AddressBookFactory";
+ data_factory_class->subprocess_object_path_prefix = "/org/gnome/evolution/dataserver/Subprocess/Backend/AddressBook";
+ data_factory_class->subprocess_bus_name_prefix = "org.gnome.evolution.dataserver.Subprocess.Backend.AddressBook";
+ data_factory_class->get_dbus_interface_skeleton = data_book_factory_get_dbus_interface_skeleton;
+ data_factory_class->get_factory_name = data_book_get_factory_name;
+ data_factory_class->complete_open = data_book_complete_open;
+}
+
+static void
+e_data_book_factory_initable_init (GInitableIface *iface)
+{
+}
+
+static void
+e_data_book_factory_init (EDataBookFactory *factory)
+{
+ factory->priv = E_DATA_BOOK_FACTORY_GET_PRIVATE (factory);
+
+ factory->priv->dbus_factory =
+ e_dbus_address_book_factory_skeleton_new ();
+
+ g_signal_connect (
+ factory->priv->dbus_factory, "handle-open-address-book",
+ G_CALLBACK (data_book_factory_handle_open_address_book_cb),
+ factory);
+}
+
+EDBusServer *
+e_data_book_factory_new (GCancellable *cancellable,
+ GError **error)
+{
+ return g_initable_new (
+ E_TYPE_DATA_BOOK_FACTORY,
+ cancellable, error,
+ "reload-supported", TRUE, NULL);
+}
diff --git a/src/addressbook/libedata-book/e-data-book-factory.h b/src/addressbook/libedata-book/e-data-book-factory.h
new file mode 100644
index 000000000..40a2988f0
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-factory.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2006 OpenedHand Ltd
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_DATA_BOOK_FACTORY_H
+#define E_DATA_BOOK_FACTORY_H
+
+#include <libebackend/libebackend.h>
+
+/* Standard GObject macros */
+#define E_TYPE_DATA_BOOK_FACTORY \
+ (e_data_book_factory_get_type ())
+#define E_DATA_BOOK_FACTORY(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_DATA_BOOK_FACTORY, EDataBookFactory))
+#define E_DATA_BOOK_FACTORY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_DATA_BOOK_FACTORY, EDataBookFactoryClass))
+#define E_IS_DATA_BOOK_FACTORY(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_DATA_BOOK_FACTORY))
+#define E_IS_DATA_BOOK_FACTORY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_DATA_BOOK_FACTORY))
+#define E_DATA_BOOK_FACTORY_GET_CLASS(cls) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_DATA_BOOK_FACTORY, EDataBookFactoryClass))
+
+/**
+ * EDS_ADDRESS_BOOK_MODULES:
+ *
+ * This environment variable configures where the address book
+ * factory loads it's backend modules from.
+ */
+#define EDS_ADDRESS_BOOK_MODULES "EDS_ADDRESS_BOOK_MODULES"
+
+/**
+ * EDS_SUBPROCESS_BOOK_PATH:
+ *
+ * This environment variable configures where the address book
+ * factory subprocess is located in.
+ */
+#define EDS_SUBPROCESS_BOOK_PATH "EDS_SUBPROCESS_BOOK_PATH"
+
+G_BEGIN_DECLS
+
+typedef struct _EDataBookFactory EDataBookFactory;
+typedef struct _EDataBookFactoryClass EDataBookFactoryClass;
+typedef struct _EDataBookFactoryPrivate EDataBookFactoryPrivate;
+
+struct _EDataBookFactory {
+ EDataFactory parent;
+ EDataBookFactoryPrivate *priv;
+};
+
+struct _EDataBookFactoryClass {
+ EDataFactoryClass parent_class;
+};
+
+GType e_data_book_factory_get_type (void) G_GNUC_CONST;
+EDBusServer * e_data_book_factory_new (GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* E_DATA_BOOK_FACTORY_H */
diff --git a/src/addressbook/libedata-book/e-data-book-view.c b/src/addressbook/libedata-book/e-data-book-view.c
new file mode 100644
index 000000000..3f70c4b07
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-view.c
@@ -0,0 +1,1148 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2006 OpenedHand Ltd
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Ross Burton <ross@linux.intel.com>
+ */
+
+/**
+ * SECTION: e-data-book-view
+ * @include: libedata-book/libedata-book.h
+ * @short_description: A server side object for issuing view notifications
+ *
+ * This class communicates with #EBookClientViews over the bus.
+ *
+ * Addressbook backends can automatically own a number of views requested
+ * by the client, this API can be used by the backend to issue notifications
+ * which will be delivered to the #EBookClientView
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <string.h>
+
+#include "e-data-book-view.h"
+
+#include "e-data-book.h"
+#include "e-book-backend.h"
+
+#include "e-gdbus-book-view.h"
+
+#define E_DATA_BOOK_VIEW_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_DATA_BOOK_VIEW, EDataBookViewPrivate))
+
+/* how many items can be hold in a cache, before propagated to UI */
+#define THRESHOLD_ITEMS 32
+
+/* how long to wait until notifications are propagated to UI; in seconds */
+#define THRESHOLD_SECONDS 2
+
+struct _EDataBookViewPrivate {
+ GDBusConnection *connection;
+ EGdbusBookView *gdbus_object;
+ gchar *object_path;
+
+ EBookBackend *backend;
+
+ EBookBackendSExp *sexp;
+ EBookClientViewFlags flags;
+
+ gboolean running;
+ gboolean complete;
+ GMutex pending_mutex;
+
+ GArray *adds;
+ GArray *changes;
+ GArray *removes;
+
+ GHashTable *ids;
+
+ guint flush_id;
+
+ /* which fields is listener interested in */
+ GHashTable *fields_of_interest;
+ gboolean send_uids_only;
+};
+
+enum {
+ PROP_0,
+ PROP_BACKEND,
+ PROP_CONNECTION,
+ PROP_OBJECT_PATH,
+ PROP_SEXP
+};
+
+/* Forward Declarations */
+static void e_data_book_view_initable_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (
+ EDataBookView,
+ e_data_book_view,
+ G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (
+ G_TYPE_INITABLE,
+ e_data_book_view_initable_init))
+
+static guint
+str_ic_hash (gconstpointer key)
+{
+ guint32 hash = 5381;
+ const gchar *str = key;
+ gint ii;
+
+ if (str == NULL)
+ return hash;
+
+ for (ii = 0; str[ii] != '\0'; ii++)
+ hash = hash * 33 + g_ascii_tolower (str[ii]);
+
+ return hash;
+}
+
+static gboolean
+str_ic_equal (gconstpointer a,
+ gconstpointer b)
+{
+ const gchar *stra = a;
+ const gchar *strb = b;
+ gint ii;
+
+ if (stra == NULL && strb == NULL)
+ return TRUE;
+
+ if (stra == NULL || strb == NULL)
+ return FALSE;
+
+ for (ii = 0; stra[ii] != '\0' && strb[ii] != '\0'; ii++) {
+ if (g_ascii_tolower (stra[ii]) != g_ascii_tolower (strb[ii]))
+ return FALSE;
+ }
+
+ return stra[ii] == strb[ii];
+}
+
+static void
+reset_array (GArray *array)
+{
+ gint i = 0;
+ gchar *tmp = NULL;
+
+ /* Free stored strings */
+ for (i = 0; i < array->len; i++) {
+ tmp = g_array_index (array, gchar *, i);
+ g_free (tmp);
+ }
+
+ /* Force the array size to 0 */
+ g_array_set_size (array, 0);
+}
+
+static void
+send_pending_adds (EDataBookView *view)
+{
+ if (view->priv->adds->len == 0)
+ return;
+
+ e_gdbus_book_view_emit_objects_added (
+ view->priv->gdbus_object,
+ (const gchar * const *) view->priv->adds->data);
+ reset_array (view->priv->adds);
+}
+
+static void
+send_pending_changes (EDataBookView *view)
+{
+ if (view->priv->changes->len == 0)
+ return;
+
+ e_gdbus_book_view_emit_objects_modified (
+ view->priv->gdbus_object,
+ (const gchar * const *) view->priv->changes->data);
+ reset_array (view->priv->changes);
+}
+
+static void
+send_pending_removes (EDataBookView *view)
+{
+ if (view->priv->removes->len == 0)
+ return;
+
+ e_gdbus_book_view_emit_objects_removed (
+ view->priv->gdbus_object,
+ (const gchar * const *) view->priv->removes->data);
+ reset_array (view->priv->removes);
+}
+
+static gboolean
+pending_flush_timeout_cb (gpointer data)
+{
+ EDataBookView *view = data;
+
+ g_mutex_lock (&view->priv->pending_mutex);
+
+ view->priv->flush_id = 0;
+
+ send_pending_adds (view);
+ send_pending_changes (view);
+ send_pending_removes (view);
+
+ g_mutex_unlock (&view->priv->pending_mutex);
+
+ return FALSE;
+}
+
+static void
+ensure_pending_flush_timeout (EDataBookView *view)
+{
+ if (view->priv->flush_id > 0)
+ return;
+
+ view->priv->flush_id = e_named_timeout_add_seconds (
+ THRESHOLD_SECONDS, pending_flush_timeout_cb, view);
+}
+
+static gpointer
+bookview_start_thread (gpointer data)
+{
+ EDataBookView *view = data;
+
+ if (view->priv->running)
+ e_book_backend_start_view (view->priv->backend, view);
+ g_object_unref (view);
+
+ return NULL;
+}
+
+static gboolean
+impl_DataBookView_start (EGdbusBookView *object,
+ GDBusMethodInvocation *invocation,
+ EDataBookView *view)
+{
+ GThread *thread;
+
+ view->priv->running = TRUE;
+ view->priv->complete = FALSE;
+
+ thread = g_thread_new (
+ NULL, bookview_start_thread, g_object_ref (view));
+ g_thread_unref (thread);
+
+ e_gdbus_book_view_complete_start (object, invocation, NULL);
+
+ return TRUE;
+}
+
+static gpointer
+bookview_stop_thread (gpointer data)
+{
+ EDataBookView *view = data;
+
+ if (!view->priv->running)
+ e_book_backend_stop_view (view->priv->backend, view);
+ g_object_unref (view);
+
+ return NULL;
+}
+
+static gboolean
+impl_DataBookView_stop (EGdbusBookView *object,
+ GDBusMethodInvocation *invocation,
+ EDataBookView *view)
+{
+ GThread *thread;
+
+ view->priv->running = FALSE;
+ view->priv->complete = FALSE;
+
+ thread = g_thread_new (
+ NULL, bookview_stop_thread, g_object_ref (view));
+ g_thread_unref (thread);
+
+ e_gdbus_book_view_complete_stop (object, invocation, NULL);
+
+ return TRUE;
+}
+
+static gboolean
+impl_DataBookView_setFlags (EGdbusBookView *object,
+ GDBusMethodInvocation *invocation,
+ EBookClientViewFlags flags,
+ EDataBookView *view)
+{
+ view->priv->flags = flags;
+
+ e_gdbus_book_view_complete_set_flags (object, invocation, NULL);
+
+ return TRUE;
+}
+
+static gboolean
+impl_DataBookView_dispose (EGdbusBookView *object,
+ GDBusMethodInvocation *invocation,
+ EDataBookView *view)
+{
+ e_gdbus_book_view_complete_dispose (object, invocation, NULL);
+
+ e_book_backend_stop_view (view->priv->backend, view);
+ view->priv->running = FALSE;
+ e_book_backend_remove_view (view->priv->backend, view);
+
+ return TRUE;
+}
+
+static gboolean
+impl_DataBookView_set_fields_of_interest (EGdbusBookView *object,
+ GDBusMethodInvocation *invocation,
+ const gchar * const *in_fields_of_interest,
+ EDataBookView *view)
+{
+ gint ii;
+
+ g_return_val_if_fail (in_fields_of_interest != NULL, TRUE);
+
+ if (view->priv->fields_of_interest != NULL) {
+ g_hash_table_destroy (view->priv->fields_of_interest);
+ view->priv->fields_of_interest = NULL;
+ }
+
+ view->priv->send_uids_only = FALSE;
+
+ for (ii = 0; in_fields_of_interest[ii]; ii++) {
+ const gchar *field = in_fields_of_interest[ii];
+
+ if (!*field)
+ continue;
+
+ if (strcmp (field, "x-evolution-uids-only") == 0) {
+ view->priv->send_uids_only = TRUE;
+ continue;
+ }
+
+ if (view->priv->fields_of_interest == NULL)
+ view->priv->fields_of_interest =
+ g_hash_table_new_full (
+ (GHashFunc) str_ic_hash,
+ (GEqualFunc) str_ic_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) NULL);
+
+ g_hash_table_insert (
+ view->priv->fields_of_interest,
+ g_strdup (field), GINT_TO_POINTER (1));
+ }
+
+ e_gdbus_book_view_complete_set_fields_of_interest (
+ object, invocation, NULL);
+
+ return TRUE;
+}
+
+static void
+data_book_view_set_backend (EDataBookView *view,
+ EBookBackend *backend)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+ g_return_if_fail (view->priv->backend == NULL);
+
+ view->priv->backend = g_object_ref (backend);
+}
+
+static void
+data_book_view_set_connection (EDataBookView *view,
+ GDBusConnection *connection)
+{
+ g_return_if_fail (G_IS_DBUS_CONNECTION (connection));
+ g_return_if_fail (view->priv->connection == NULL);
+
+ view->priv->connection = g_object_ref (connection);
+}
+
+static void
+data_book_view_set_object_path (EDataBookView *view,
+ const gchar *object_path)
+{
+ g_return_if_fail (object_path != NULL);
+ g_return_if_fail (view->priv->object_path == NULL);
+
+ view->priv->object_path = g_strdup (object_path);
+}
+
+static void
+data_book_view_set_sexp (EDataBookView *view,
+ EBookBackendSExp *sexp)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND_SEXP (sexp));
+ g_return_if_fail (view->priv->sexp == NULL);
+
+ view->priv->sexp = g_object_ref (sexp);
+}
+
+static void
+data_book_view_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_BACKEND:
+ data_book_view_set_backend (
+ E_DATA_BOOK_VIEW (object),
+ g_value_get_object (value));
+ return;
+
+ case PROP_CONNECTION:
+ data_book_view_set_connection (
+ E_DATA_BOOK_VIEW (object),
+ g_value_get_object (value));
+ return;
+
+ case PROP_OBJECT_PATH:
+ data_book_view_set_object_path (
+ E_DATA_BOOK_VIEW (object),
+ g_value_get_string (value));
+ return;
+
+ case PROP_SEXP:
+ data_book_view_set_sexp (
+ E_DATA_BOOK_VIEW (object),
+ g_value_get_object (value));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+data_book_view_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_BACKEND:
+ g_value_set_object (
+ value,
+ e_data_book_view_get_backend (
+ E_DATA_BOOK_VIEW (object)));
+ return;
+
+ case PROP_CONNECTION:
+ g_value_set_object (
+ value,
+ e_data_book_view_get_connection (
+ E_DATA_BOOK_VIEW (object)));
+ return;
+
+ case PROP_OBJECT_PATH:
+ g_value_set_string (
+ value,
+ e_data_book_view_get_object_path (
+ E_DATA_BOOK_VIEW (object)));
+ return;
+
+ case PROP_SEXP:
+ g_value_set_object (
+ value,
+ e_data_book_view_get_sexp (
+ E_DATA_BOOK_VIEW (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+data_book_view_dispose (GObject *object)
+{
+ EDataBookViewPrivate *priv;
+
+ priv = E_DATA_BOOK_VIEW_GET_PRIVATE (object);
+
+ g_clear_object (&priv->connection);
+ g_clear_object (&priv->gdbus_object);
+ g_clear_object (&priv->backend);
+ g_clear_object (&priv->sexp);
+
+ g_mutex_lock (&priv->pending_mutex);
+
+ if (priv->flush_id > 0) {
+ g_source_remove (priv->flush_id);
+ priv->flush_id = 0;
+ }
+
+ g_mutex_unlock (&priv->pending_mutex);
+
+ /* Chain up to parent's dispose() method. */
+ G_OBJECT_CLASS (e_data_book_view_parent_class)->dispose (object);
+}
+
+static void
+data_book_view_finalize (GObject *object)
+{
+ EDataBookViewPrivate *priv;
+
+ priv = E_DATA_BOOK_VIEW_GET_PRIVATE (object);
+
+ g_free (priv->object_path);
+
+ reset_array (priv->adds);
+ reset_array (priv->changes);
+ reset_array (priv->removes);
+ g_array_free (priv->adds, TRUE);
+ g_array_free (priv->changes, TRUE);
+ g_array_free (priv->removes, TRUE);
+
+ if (priv->fields_of_interest)
+ g_hash_table_destroy (priv->fields_of_interest);
+
+ g_mutex_clear (&priv->pending_mutex);
+
+ g_hash_table_destroy (priv->ids);
+
+ /* Chain up to parent's finalize() method. */
+ G_OBJECT_CLASS (e_data_book_view_parent_class)->finalize (object);
+}
+
+static gboolean
+data_book_view_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EDataBookView *view;
+
+ view = E_DATA_BOOK_VIEW (initable);
+
+ return e_gdbus_book_view_register_object (
+ view->priv->gdbus_object,
+ view->priv->connection,
+ view->priv->object_path,
+ error);
+}
+
+static void
+e_data_book_view_class_init (EDataBookViewClass *class)
+{
+ GObjectClass *object_class;
+
+ g_type_class_add_private (class, sizeof (EDataBookViewPrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->set_property = data_book_view_set_property;
+ object_class->get_property = data_book_view_get_property;
+ object_class->dispose = data_book_view_dispose;
+ object_class->finalize = data_book_view_finalize;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_BACKEND,
+ g_param_spec_object (
+ "backend",
+ "Backend",
+ "The backend being monitored",
+ E_TYPE_BOOK_BACKEND,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_CONNECTION,
+ g_param_spec_object (
+ "connection",
+ "Connection",
+ "The GDBusConnection on which "
+ "to export the view interface",
+ G_TYPE_DBUS_CONNECTION,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_OBJECT_PATH,
+ g_param_spec_string (
+ "object-path",
+ "Object Path",
+ "The object path at which to "
+ "export the view interface",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_SEXP,
+ g_param_spec_object (
+ "sexp",
+ "S-Expression",
+ "The query expression for this view",
+ E_TYPE_BOOK_BACKEND_SEXP,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+e_data_book_view_initable_init (GInitableIface *iface)
+{
+ iface->init = data_book_view_initable_init;
+}
+
+static void
+e_data_book_view_init (EDataBookView *view)
+{
+ view->priv = E_DATA_BOOK_VIEW_GET_PRIVATE (view);
+
+ view->priv->flags = E_BOOK_CLIENT_VIEW_FLAGS_NOTIFY_INITIAL;
+
+ view->priv->gdbus_object = e_gdbus_book_view_stub_new ();
+ g_signal_connect (
+ view->priv->gdbus_object, "handle-start",
+ G_CALLBACK (impl_DataBookView_start), view);
+ g_signal_connect (
+ view->priv->gdbus_object, "handle-stop",
+ G_CALLBACK (impl_DataBookView_stop), view);
+ g_signal_connect (
+ view->priv->gdbus_object, "handle-set-flags",
+ G_CALLBACK (impl_DataBookView_setFlags), view);
+ g_signal_connect (
+ view->priv->gdbus_object, "handle-dispose",
+ G_CALLBACK (impl_DataBookView_dispose), view);
+ g_signal_connect (
+ view->priv->gdbus_object, "handle-set-fields-of-interest",
+ G_CALLBACK (impl_DataBookView_set_fields_of_interest), view);
+
+ view->priv->fields_of_interest = NULL;
+ view->priv->running = FALSE;
+ view->priv->complete = FALSE;
+ g_mutex_init (&view->priv->pending_mutex);
+
+ /* THRESHOLD_ITEMS * 2 because we store UID and vcard */
+ view->priv->adds = g_array_sized_new (
+ TRUE, TRUE, sizeof (gchar *), THRESHOLD_ITEMS * 2);
+ view->priv->changes = g_array_sized_new (
+ TRUE, TRUE, sizeof (gchar *), THRESHOLD_ITEMS * 2);
+ view->priv->removes = g_array_sized_new (
+ TRUE, TRUE, sizeof (gchar *), THRESHOLD_ITEMS);
+
+ view->priv->ids = g_hash_table_new_full (
+ (GHashFunc) g_str_hash,
+ (GEqualFunc) g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) NULL);
+
+ view->priv->flush_id = 0;
+}
+
+/**
+ * e_data_book_view_new:
+ * @backend: an #EBookBackend
+ * @sexp: an #EBookBackendSExp
+ * @connection: a #GDBusConnection
+ * @object_path: an object path for the view
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EDataBookView and exports its D-Bus interface on
+ * @connection at @object_path. If an error occurs while exporting,
+ * the function sets @error and returns %NULL.
+ *
+ * Returns: an #EDataBookView
+ */
+EDataBookView *
+e_data_book_view_new (EBookBackend *backend,
+ EBookBackendSExp *sexp,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SEXP (sexp), NULL);
+ g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
+ g_return_val_if_fail (object_path != NULL, NULL);
+
+ return g_initable_new (
+ E_TYPE_DATA_BOOK_VIEW, NULL, error,
+ "backend", backend,
+ "connection", connection,
+ "object-path", object_path,
+ "sexp", sexp, NULL);
+}
+
+/**
+ * e_data_book_view_get_backend:
+ * @view: an #EDataBookView
+ *
+ * Gets the backend that @view is querying.
+ *
+ * Returns: The associated #EBookBackend.
+ **/
+EBookBackend *
+e_data_book_view_get_backend (EDataBookView *view)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), NULL);
+
+ return view->priv->backend;
+}
+
+/**
+ * e_data_book_view_get_sexp:
+ * @view: an #EDataBookView
+ *
+ * Gets the s-expression used for matching contacts to @view.
+ *
+ * Returns: The #EBookBackendSExp used.
+ *
+ * Since: 3.8
+ **/
+EBookBackendSExp *
+e_data_book_view_get_sexp (EDataBookView *view)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), NULL);
+
+ return view->priv->sexp;
+}
+
+/**
+ * e_data_book_view_get_connection:
+ * @view: an #EDataBookView
+ *
+ * Returns the #GDBusConnection on which the AddressBookView D-Bus
+ * interface is exported.
+ *
+ * Returns: the #GDBusConnection
+ *
+ * Since: 3.8
+ **/
+GDBusConnection *
+e_data_book_view_get_connection (EDataBookView *view)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), NULL);
+
+ return view->priv->connection;
+}
+
+/**
+ * e_data_book_view_get_object_path:
+ * @view: an #EDataBookView
+ *
+ * Returns the object path at which the AddressBookView D-Bus interface
+ * is exported.
+ *
+ * Returns: the object path
+ *
+ * Since: 3.8
+ **/
+const gchar *
+e_data_book_view_get_object_path (EDataBookView *view)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), NULL);
+
+ return view->priv->object_path;
+}
+
+/**
+ * e_data_book_view_get_flags:
+ * @view: an #EDataBookView
+ *
+ * Gets the #EBookClientViewFlags that control the behaviour of @view.
+ *
+ * Returns: the flags for @view.
+ *
+ * Since: 3.4
+ **/
+EBookClientViewFlags
+e_data_book_view_get_flags (EDataBookView *view)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), 0);
+
+ return view->priv->flags;
+}
+
+/*
+ * Queue @vcard to be sent as a change notification.
+ */
+static void
+notify_change (EDataBookView *view,
+ const gchar *id,
+ const gchar *vcard)
+{
+ gchar *utf8_vcard, *utf8_id;
+
+ send_pending_adds (view);
+ send_pending_removes (view);
+
+ if (view->priv->changes->len == THRESHOLD_ITEMS * 2) {
+ send_pending_changes (view);
+ }
+
+ if (view->priv->send_uids_only == FALSE) {
+ utf8_vcard = e_util_utf8_make_valid (vcard);
+ g_array_append_val (view->priv->changes, utf8_vcard);
+ }
+
+ utf8_id = e_util_utf8_make_valid (id);
+ g_array_append_val (view->priv->changes, utf8_id);
+
+ ensure_pending_flush_timeout (view);
+}
+
+/*
+ * Queue @id to be sent as a change notification.
+ */
+static void
+notify_remove (EDataBookView *view,
+ const gchar *id)
+{
+ gchar *valid_id;
+
+ send_pending_adds (view);
+ send_pending_changes (view);
+
+ if (view->priv->removes->len == THRESHOLD_ITEMS) {
+ send_pending_removes (view);
+ }
+
+ valid_id = e_util_utf8_make_valid (id);
+ g_array_append_val (view->priv->removes, valid_id);
+ g_hash_table_remove (view->priv->ids, valid_id);
+
+ ensure_pending_flush_timeout (view);
+}
+
+/*
+ * Queue @id and @vcard to be sent as a change notification.
+ */
+static void
+notify_add (EDataBookView *view,
+ const gchar *id,
+ const gchar *vcard)
+{
+ EBookClientViewFlags flags;
+ gchar *utf8_vcard, *utf8_id;
+
+ send_pending_changes (view);
+ send_pending_removes (view);
+
+ utf8_id = e_util_utf8_make_valid (id);
+
+ /* Do not send contact add notifications during initial stage */
+ flags = e_data_book_view_get_flags (view);
+ if (view->priv->complete || (flags & E_BOOK_CLIENT_VIEW_FLAGS_NOTIFY_INITIAL) != 0) {
+ gchar *utf8_id_copy = g_strdup (utf8_id);
+
+ if (view->priv->adds->len == THRESHOLD_ITEMS) {
+ send_pending_adds (view);
+ }
+
+ if (view->priv->send_uids_only == FALSE) {
+ utf8_vcard = e_util_utf8_make_valid (vcard);
+ g_array_append_val (view->priv->adds, utf8_vcard);
+ }
+
+ g_array_append_val (view->priv->adds, utf8_id_copy);
+
+ ensure_pending_flush_timeout (view);
+ }
+
+ g_hash_table_insert (view->priv->ids, utf8_id, GUINT_TO_POINTER (1));
+}
+
+static gboolean
+id_is_in_view (EDataBookView *view,
+ const gchar *id)
+{
+ gchar *valid_id;
+ gboolean res;
+
+ g_return_val_if_fail (view != NULL, FALSE);
+ g_return_val_if_fail (id != NULL, FALSE);
+
+ valid_id = e_util_utf8_make_valid (id);
+ res = g_hash_table_lookup (view->priv->ids, valid_id) != NULL;
+ g_free (valid_id);
+
+ return res;
+}
+
+/**
+ * e_data_book_view_notify_update:
+ * @view: an #EDataBookView
+ * @contact: an #EContact
+ *
+ * Notify listeners that @contact has changed. This can
+ * trigger an add, change or removal event depending on
+ * whether the change causes the contact to start matching,
+ * no longer match, or stay matching the query specified
+ * by @view.
+ **/
+void
+e_data_book_view_notify_update (EDataBookView *view,
+ const EContact *contact)
+{
+ gboolean currently_in_view, want_in_view;
+ const gchar *id;
+ gchar *vcard;
+
+ g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+ g_return_if_fail (E_IS_CONTACT (contact));
+
+ if (!view->priv->running)
+ return;
+
+ g_mutex_lock (&view->priv->pending_mutex);
+
+ id = e_contact_get_const ((EContact *) contact, E_CONTACT_UID);
+
+ currently_in_view = id_is_in_view (view, id);
+ want_in_view = e_book_backend_sexp_match_contact (
+ view->priv->sexp, (EContact *) contact);
+
+ if (want_in_view) {
+ vcard = e_vcard_to_string (
+ E_VCARD (contact),
+ EVC_FORMAT_VCARD_30);
+
+ if (currently_in_view)
+ notify_change (view, id, vcard);
+ else
+ notify_add (view, id, vcard);
+
+ g_free (vcard);
+ } else {
+ if (currently_in_view)
+ notify_remove (view, id);
+ /* else nothing; we're removing a card that wasn't there */
+ }
+
+ g_mutex_unlock (&view->priv->pending_mutex);
+}
+
+/**
+ * e_data_book_view_notify_update_vcard:
+ * @view: an #EDataBookView
+ * @vcard: a plain vCard
+ *
+ * Notify listeners that @vcard has changed. This can
+ * trigger an add, change or removal event depending on
+ * whether the change causes the contact to start matching,
+ * no longer match, or stay matching the query specified
+ * by @view. This method should be preferred over
+ * e_data_book_view_notify_update() when the native
+ * representation of a contact is a vCard.
+ **/
+void
+e_data_book_view_notify_update_vcard (EDataBookView *view,
+ const gchar *id,
+ const gchar *vcard)
+{
+ gboolean currently_in_view, want_in_view;
+ EContact *contact;
+
+ g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+ g_return_if_fail (id != NULL);
+ g_return_if_fail (vcard != NULL);
+
+ if (!view->priv->running)
+ return;
+
+ g_mutex_lock (&view->priv->pending_mutex);
+
+ contact = e_contact_new_from_vcard_with_uid (vcard, id);
+ currently_in_view = id_is_in_view (view, id);
+ want_in_view = e_book_backend_sexp_match_contact (
+ view->priv->sexp, contact);
+
+ if (want_in_view) {
+ if (currently_in_view)
+ notify_change (view, id, vcard);
+ else
+ notify_add (view, id, vcard);
+ } else {
+ if (currently_in_view)
+ notify_remove (view, id);
+ }
+
+ /* Do this last so that id is still valid when notify_ is called */
+ g_object_unref (contact);
+
+ g_mutex_unlock (&view->priv->pending_mutex);
+}
+
+/**
+ * e_data_book_view_notify_update_prefiltered_vcard:
+ * @view: an #EDataBookView
+ * @id: the UID of this contact
+ * @vcard: a plain vCard
+ *
+ * Notify listeners that @vcard has changed. This can
+ * trigger an add, change or removal event depending on
+ * whether the change causes the contact to start matching,
+ * no longer match, or stay matching the query specified
+ * by @view. This method should be preferred over
+ * e_data_book_view_notify_update() when the native
+ * representation of a contact is a vCard.
+ *
+ * The important difference between this method and
+ * e_data_book_view_notify_update() and
+ * e_data_book_view_notify_update_vcard() is
+ * that it doesn't match the contact against the book view query to see if it
+ * should be included, it assumes that this has been done and the contact is
+ * known to exist in the view.
+ **/
+void
+e_data_book_view_notify_update_prefiltered_vcard (EDataBookView *view,
+ const gchar *id,
+ const gchar *vcard)
+{
+ gboolean currently_in_view;
+
+ g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+ g_return_if_fail (id != NULL);
+ g_return_if_fail (vcard != NULL);
+
+ if (!view->priv->running)
+ return;
+
+ g_mutex_lock (&view->priv->pending_mutex);
+
+ currently_in_view = id_is_in_view (view, id);
+
+ if (currently_in_view)
+ notify_change (view, id, vcard);
+ else
+ notify_add (view, id, vcard);
+
+ g_mutex_unlock (&view->priv->pending_mutex);
+}
+
+/**
+ * e_data_book_view_notify_remove:
+ * @view: an #EDataBookView
+ * @id: a unique contact ID
+ *
+ * Notify listeners that a contact specified by @id
+ * was removed from @view.
+ **/
+void
+e_data_book_view_notify_remove (EDataBookView *view,
+ const gchar *id)
+{
+ g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+ g_return_if_fail (id != NULL);
+
+ if (!view->priv->running)
+ return;
+
+ g_mutex_lock (&view->priv->pending_mutex);
+
+ if (id_is_in_view (view, id))
+ notify_remove (view, id);
+
+ g_mutex_unlock (&view->priv->pending_mutex);
+}
+
+/**
+ * e_data_book_view_notify_complete:
+ * @view: an #EDataBookView
+ * @error: the error of the query, if any
+ *
+ * Notifies listeners that all pending updates on @view
+ * have been sent. The listener's information should now be
+ * in sync with the backend's.
+ **/
+void
+e_data_book_view_notify_complete (EDataBookView *view,
+ const GError *error)
+{
+ gchar **strv_error;
+
+ g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+
+ if (!view->priv->running)
+ return;
+
+ /* View is complete */
+ view->priv->complete = TRUE;
+
+ g_mutex_lock (&view->priv->pending_mutex);
+
+ send_pending_adds (view);
+ send_pending_changes (view);
+ send_pending_removes (view);
+
+ g_mutex_unlock (&view->priv->pending_mutex);
+
+ strv_error = e_gdbus_templates_encode_error (error);
+ e_gdbus_book_view_emit_complete (
+ view->priv->gdbus_object,
+ (const gchar * const *) strv_error);
+ g_strfreev (strv_error);
+}
+
+/**
+ * e_data_book_view_notify_progress:
+ * @view: an #EDataBookView
+ * @percent: percent done; use -1 when not available
+ * @message: a text message
+ *
+ * Provides listeners with a human-readable text describing the
+ * current backend operation. This can be used for progress
+ * reporting.
+ *
+ * Since: 3.2
+ **/
+void
+e_data_book_view_notify_progress (EDataBookView *view,
+ guint percent,
+ const gchar *message)
+{
+ gchar *gdbus_message = NULL;
+
+ g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+
+ if (!view->priv->running)
+ return;
+
+ e_gdbus_book_view_emit_progress (
+ view->priv->gdbus_object, percent,
+ e_util_ensure_gdbus_string (message, &gdbus_message));
+
+ g_free (gdbus_message);
+}
+
+/**
+ * e_data_book_view_get_fields_of_interest:
+ * @view: an #EDataBookView
+ *
+ * Returns: Hash table of field names which the listener is interested in.
+ * Backends can return fully populated objects, but the listener advertised
+ * that it will use only these. Returns %NULL for all available fields.
+ *
+ * Note: The data pointer in the hash table has no special meaning, it's
+ * only GINT_TO_POINTER(1) for easier checking. Also, field names are
+ * compared case insensitively.
+ **/
+GHashTable *
+e_data_book_view_get_fields_of_interest (EDataBookView *view)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), NULL);
+
+ return view->priv->fields_of_interest;
+}
+
diff --git a/src/addressbook/libedata-book/e-data-book-view.h b/src/addressbook/libedata-book/e-data-book-view.h
new file mode 100644
index 000000000..de24ca717
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-view.h
@@ -0,0 +1,113 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2006 OpenedHand Ltd
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Nat Friedman <nat@ximian.com>
+ * Ross Burton <ross@linux.intel.com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_DATA_BOOK_VIEW_H
+#define E_DATA_BOOK_VIEW_H
+
+#include <libebook-contacts/libebook-contacts.h>
+
+#include <libedata-book/e-book-backend-sexp.h>
+
+/* Standard GObject macros */
+#define E_TYPE_DATA_BOOK_VIEW \
+ (e_data_book_view_get_type ())
+#define E_DATA_BOOK_VIEW(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_DATA_BOOK_VIEW, EDataBookView))
+#define E_DATA_BOOK_VIEW_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_DATA_BOOK_VIEW, EDataBookViewClass))
+#define E_IS_DATA_BOOK_VIEW(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_DATA_BOOK_VIEW))
+#define E_IS_DATA_BOOK_VIEW_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_DATA_BOOK_VIEW))
+#define E_DATA_BOOK_VIEW_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_DATA_BOOK_VIEW, EDataBookViewClass))
+
+G_BEGIN_DECLS
+
+struct _EBookBackend;
+
+typedef struct _EDataBookView EDataBookView;
+typedef struct _EDataBookViewClass EDataBookViewClass;
+typedef struct _EDataBookViewPrivate EDataBookViewPrivate;
+
+struct _EDataBookView {
+ GObject parent;
+ EDataBookViewPrivate *priv;
+};
+
+struct _EDataBookViewClass {
+ GObjectClass parent;
+};
+
+GType e_data_book_view_get_type (void) G_GNUC_CONST;
+EDataBookView * e_data_book_view_new (struct _EBookBackend *backend,
+ EBookBackendSExp *sexp,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error);
+struct _EBookBackend *
+ e_data_book_view_get_backend (EDataBookView *view);
+GDBusConnection *
+ e_data_book_view_get_connection (EDataBookView *view);
+const gchar * e_data_book_view_get_object_path
+ (EDataBookView *view);
+EBookBackendSExp *
+ e_data_book_view_get_sexp (EDataBookView *view);
+EBookClientViewFlags
+ e_data_book_view_get_flags (EDataBookView *view);
+void e_data_book_view_notify_update (EDataBookView *view,
+ const EContact *contact);
+
+void e_data_book_view_notify_update_vcard
+ (EDataBookView *view,
+ const gchar *id,
+ const gchar *vcard);
+void e_data_book_view_notify_update_prefiltered_vcard
+ (EDataBookView *view,
+ const gchar *id,
+ const gchar *vcard);
+
+void e_data_book_view_notify_remove (EDataBookView *view,
+ const gchar *id);
+void e_data_book_view_notify_complete
+ (EDataBookView *view,
+ const GError *error);
+void e_data_book_view_notify_progress
+ (EDataBookView *view,
+ guint percent,
+ const gchar *message);
+
+GHashTable * e_data_book_view_get_fields_of_interest
+ (EDataBookView *view);
+
+G_END_DECLS
+
+#endif /* E_DATA_BOOK_VIEW_H */
diff --git a/src/addressbook/libedata-book/e-data-book-view.xml b/src/addressbook/libedata-book/e-data-book-view.xml
new file mode 100644
index 000000000..1fa1960a9
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-view.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE node SYSTEM "dbus.dtd">
+
+<!--
+ Author: Ross Burton <ross@linux.intel.com>
+ Copyright (C) 2005 Opened Hand Ltd
+ Copyright (C) 2009 Intel Corporation
+-->
+<node>
+
+ <interface name="org.gnome.evolution.dataserver.AddressBookView">
+ <annotation name="org.freedesktop.DBus.GLib.CSymbol" value="EDataBookView"/>
+
+ <method name="start">
+ <annotation name="org.freedesktop.DBus.GLib.CSymbol" value="impl_BookView_start"/>
+ </method>
+
+ <method name="stop">
+ <annotation name="org.freedesktop.DBus.GLib.CSymbol" value="impl_BookView_stop"/>
+ </method>
+
+ <method name="dispose">
+ <annotation name="org.freedesktop.DBus.GLib.CSymbol" value="impl_BookView_dispose"/>
+ </method>
+
+ <signal name="ContactsAdded">
+ <arg name="vcards" type="as"/>
+ </signal>
+ <signal name="ContactsChanged">
+ <arg name="vcards" type="as"/>
+ </signal>
+ <signal name="ContactsRemoved">
+ <arg name="ids" type="as"/>
+ </signal>
+ <signal name="StatusMessage">
+ <arg name="message" type="s"/>
+ </signal>
+ <signal name="Complete">
+ <arg name="status" type="u"/>
+ <arg name="message" type="s"/>
+ </signal>
+ </interface>
+
+</node>
diff --git a/src/addressbook/libedata-book/e-data-book.c b/src/addressbook/libedata-book/e-data-book.c
new file mode 100644
index 000000000..58f2ed9a9
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book.c
@@ -0,0 +1,2375 @@
+/*
+ * e-data-book.c
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * SECTION: e-data-book
+ * @include: libedata-book/libedata-book.h
+ * @short_description: Server side D-Bus layer to communicate with addressbooks
+ *
+ * This class communicates with #EBookClients over the bus and accesses
+ * an #EBookBackend to satisfy client requests.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <locale.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+/* Private D-Bus classes. */
+#include <e-dbus-address-book.h>
+
+#include <libebook-contacts/libebook-contacts.h>
+
+#include "e-data-book-factory.h"
+#include "e-data-book.h"
+#include "e-data-book-view.h"
+#include "e-book-backend.h"
+#include "e-book-backend-sexp.h"
+#include "e-book-backend-factory.h"
+
+#define E_DATA_BOOK_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_DATA_BOOK, EDataBookPrivate))
+
+typedef struct _AsyncContext AsyncContext;
+
+struct _EDataBookPrivate {
+ GDBusConnection *connection;
+ EDBusAddressBook *dbus_interface;
+ EModule *direct_module;
+ EDataBookDirect *direct_book;
+
+ GWeakRef backend;
+ gchar *object_path;
+
+ GMutex sender_lock;
+ GHashTable *sender_table;
+};
+
+struct _AsyncContext {
+ EDataBook *data_book;
+ EDBusAddressBook *dbus_interface;
+ GDBusMethodInvocation *invocation;
+ GCancellable *cancellable;
+ guint watcher_id;
+};
+
+enum {
+ PROP_0,
+ PROP_BACKEND,
+ PROP_CONNECTION,
+ PROP_OBJECT_PATH
+};
+
+/* Forward Declarations */
+static void e_data_book_initable_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (
+ EDataBook,
+ e_data_book,
+ G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (
+ G_TYPE_INITABLE,
+ e_data_book_initable_init))
+
+static void
+sender_vanished_cb (GDBusConnection *connection,
+ const gchar *sender,
+ GCancellable *cancellable)
+{
+ g_cancellable_cancel (cancellable);
+}
+
+static void
+sender_table_insert (EDataBook *data_book,
+ const gchar *sender,
+ GCancellable *cancellable)
+{
+ GHashTable *sender_table;
+ GPtrArray *array;
+
+ g_return_if_fail (sender != NULL);
+
+ g_mutex_lock (&data_book->priv->sender_lock);
+
+ sender_table = data_book->priv->sender_table;
+ array = g_hash_table_lookup (sender_table, sender);
+
+ if (array == NULL) {
+ array = g_ptr_array_new_with_free_func (
+ (GDestroyNotify) g_object_unref);
+ g_hash_table_insert (
+ sender_table, g_strdup (sender), array);
+ }
+
+ g_ptr_array_add (array, g_object_ref (cancellable));
+
+ g_mutex_unlock (&data_book->priv->sender_lock);
+}
+
+static gboolean
+sender_table_remove (EDataBook *data_book,
+ const gchar *sender,
+ GCancellable *cancellable)
+{
+ GHashTable *sender_table;
+ GPtrArray *array;
+ gboolean removed = FALSE;
+
+ g_return_val_if_fail (sender != NULL, FALSE);
+
+ g_mutex_lock (&data_book->priv->sender_lock);
+
+ sender_table = data_book->priv->sender_table;
+ array = g_hash_table_lookup (sender_table, sender);
+
+ if (array != NULL) {
+ removed = g_ptr_array_remove_fast (array, cancellable);
+
+ if (array->len == 0)
+ g_hash_table_remove (sender_table, sender);
+ }
+
+ g_mutex_unlock (&data_book->priv->sender_lock);
+
+ return removed;
+}
+
+static AsyncContext *
+async_context_new (EDataBook *data_book,
+ GDBusMethodInvocation *invocation)
+{
+ AsyncContext *async_context;
+ EDBusAddressBook *dbus_interface;
+
+ dbus_interface = data_book->priv->dbus_interface;
+
+ async_context = g_slice_new0 (AsyncContext);
+ async_context->data_book = g_object_ref (data_book);
+ async_context->dbus_interface = g_object_ref (dbus_interface);
+ async_context->invocation = g_object_ref (invocation);
+ async_context->cancellable = g_cancellable_new ();
+
+ async_context->watcher_id = g_bus_watch_name_on_connection (
+ g_dbus_method_invocation_get_connection (invocation),
+ g_dbus_method_invocation_get_sender (invocation),
+ G_BUS_NAME_WATCHER_FLAGS_NONE,
+ (GBusNameAppearedCallback) NULL,
+ (GBusNameVanishedCallback) sender_vanished_cb,
+ g_object_ref (async_context->cancellable),
+ (GDestroyNotify) g_object_unref);
+
+ sender_table_insert (
+ async_context->data_book,
+ g_dbus_method_invocation_get_sender (invocation),
+ async_context->cancellable);
+
+ return async_context;
+}
+
+static void
+async_context_free (AsyncContext *async_context)
+{
+ sender_table_remove (
+ async_context->data_book,
+ g_dbus_method_invocation_get_sender (
+ async_context->invocation),
+ async_context->cancellable);
+
+ g_clear_object (&async_context->data_book);
+ g_clear_object (&async_context->dbus_interface);
+ g_clear_object (&async_context->invocation);
+ g_clear_object (&async_context->cancellable);
+
+ if (async_context->watcher_id > 0)
+ g_bus_unwatch_name (async_context->watcher_id);
+
+ g_slice_free (AsyncContext, async_context);
+}
+
+static gchar *
+construct_bookview_path (void)
+{
+ static volatile gint counter = 1;
+
+ g_atomic_int_inc (&counter);
+
+ return g_strdup_printf (
+ "/org/gnome/evolution/dataserver/AddressBookView/%d/%d",
+ getpid (), counter);
+}
+
+static gchar *
+construct_bookcursor_path (void)
+{
+ static volatile gint counter = 1;
+
+ g_atomic_int_inc (&counter);
+
+ return g_strdup_printf (
+ "/org/gnome/evolution/dataserver/AddressBookCursor/%d/%d",
+ getpid (), counter);
+}
+
+static void
+data_book_convert_to_client_error (GError *error)
+{
+ g_return_if_fail (error != NULL);
+
+ /* Data-Factory returns common error for unknown/broken ESource-s */
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+ error->domain = E_BOOK_CLIENT_ERROR;
+ error->code = E_BOOK_CLIENT_ERROR_NO_SUCH_BOOK;
+
+ return;
+ }
+
+ if (error->domain != E_DATA_BOOK_ERROR)
+ return;
+
+ switch (error->code) {
+ case E_DATA_BOOK_STATUS_REPOSITORY_OFFLINE:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_REPOSITORY_OFFLINE;
+ break;
+
+ case E_DATA_BOOK_STATUS_PERMISSION_DENIED:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_PERMISSION_DENIED;
+ break;
+
+ case E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND:
+ error->domain = E_BOOK_CLIENT_ERROR;
+ error->code = E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND;
+ break;
+
+ case E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS:
+ error->domain = E_BOOK_CLIENT_ERROR;
+ error->code = E_BOOK_CLIENT_ERROR_CONTACT_ID_ALREADY_EXISTS;
+ break;
+
+ case E_DATA_BOOK_STATUS_AUTHENTICATION_FAILED:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_AUTHENTICATION_FAILED;
+ break;
+
+ case E_DATA_BOOK_STATUS_UNSUPPORTED_AUTHENTICATION_METHOD:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_UNSUPPORTED_AUTHENTICATION_METHOD;
+ break;
+
+ case E_DATA_BOOK_STATUS_TLS_NOT_AVAILABLE:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_TLS_NOT_AVAILABLE;
+ break;
+
+ case E_DATA_BOOK_STATUS_NO_SUCH_BOOK:
+ error->domain = E_BOOK_CLIENT_ERROR;
+ error->code = E_BOOK_CLIENT_ERROR_NO_SUCH_BOOK;
+ break;
+
+ case E_DATA_BOOK_STATUS_BOOK_REMOVED:
+ error->domain = E_BOOK_CLIENT_ERROR;
+ error->code = E_BOOK_CLIENT_ERROR_NO_SUCH_SOURCE;
+ break;
+
+ case E_DATA_BOOK_STATUS_OFFLINE_UNAVAILABLE:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_OFFLINE_UNAVAILABLE;
+ break;
+
+ case E_DATA_BOOK_STATUS_SEARCH_SIZE_LIMIT_EXCEEDED:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_SEARCH_SIZE_LIMIT_EXCEEDED;
+ break;
+
+ case E_DATA_BOOK_STATUS_SEARCH_TIME_LIMIT_EXCEEDED:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_SEARCH_TIME_LIMIT_EXCEEDED;
+ break;
+
+ case E_DATA_BOOK_STATUS_INVALID_QUERY:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_INVALID_QUERY;
+ break;
+
+ case E_DATA_BOOK_STATUS_QUERY_REFUSED:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_QUERY_REFUSED;
+ break;
+
+ case E_DATA_BOOK_STATUS_COULD_NOT_CANCEL:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_COULD_NOT_CANCEL;
+ break;
+
+ case E_DATA_BOOK_STATUS_NO_SPACE:
+ error->domain = E_BOOK_CLIENT_ERROR;
+ error->code = E_BOOK_CLIENT_ERROR_NO_SPACE;
+ break;
+
+ case E_DATA_BOOK_STATUS_INVALID_ARG:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_INVALID_ARG;
+ break;
+
+ case E_DATA_BOOK_STATUS_NOT_SUPPORTED:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_NOT_SUPPORTED;
+ break;
+
+ case E_DATA_BOOK_STATUS_NOT_OPENED:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_NOT_OPENED;
+ break;
+
+ case E_DATA_BOOK_STATUS_OUT_OF_SYNC:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_OUT_OF_SYNC;
+ break;
+
+ case E_DATA_BOOK_STATUS_UNSUPPORTED_FIELD:
+ case E_DATA_BOOK_STATUS_OTHER_ERROR:
+ case E_DATA_BOOK_STATUS_INVALID_SERVER_VERSION:
+ error->domain = E_CLIENT_ERROR;
+ error->code = E_CLIENT_ERROR_OTHER_ERROR;
+ break;
+
+ default:
+ g_warn_if_reached ();
+ }
+}
+
+/**
+ * e_data_book_status_to_string:
+ * @status: an #EDataBookStatus
+ *
+ * Get localized human readable description of the given status code.
+ *
+ * Returns: Localized human readable description of the given status code
+ *
+ * Since: 2.32
+ **/
+const gchar *
+e_data_book_status_to_string (EDataBookStatus status)
+{
+ gint i;
+ static struct _statuses {
+ EDataBookStatus status;
+ const gchar *msg;
+ } statuses[] = {
+ { E_DATA_BOOK_STATUS_SUCCESS, N_("Success") },
+ { E_DATA_BOOK_STATUS_BUSY, N_("Backend is busy") },
+ { E_DATA_BOOK_STATUS_REPOSITORY_OFFLINE, N_("Repository offline") },
+ { E_DATA_BOOK_STATUS_PERMISSION_DENIED, N_("Permission denied") },
+ { E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, N_("Contact not found") },
+ { E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS, N_("Contact ID already exists") },
+ { E_DATA_BOOK_STATUS_AUTHENTICATION_FAILED, N_("Authentication Failed") },
+ { E_DATA_BOOK_STATUS_AUTHENTICATION_REQUIRED, N_("Authentication Required") },
+ { E_DATA_BOOK_STATUS_UNSUPPORTED_FIELD, N_("Unsupported field") },
+ { E_DATA_BOOK_STATUS_UNSUPPORTED_AUTHENTICATION_METHOD, N_("Unsupported authentication method") },
+ { E_DATA_BOOK_STATUS_TLS_NOT_AVAILABLE, N_("TLS not available") },
+ { E_DATA_BOOK_STATUS_NO_SUCH_BOOK, N_("Address book does not exist") },
+ { E_DATA_BOOK_STATUS_BOOK_REMOVED, N_("Book removed") },
+ { E_DATA_BOOK_STATUS_OFFLINE_UNAVAILABLE, N_("Not available in offline mode") },
+ { E_DATA_BOOK_STATUS_SEARCH_SIZE_LIMIT_EXCEEDED, N_("Search size limit exceeded") },
+ { E_DATA_BOOK_STATUS_SEARCH_TIME_LIMIT_EXCEEDED, N_("Search time limit exceeded") },
+ { E_DATA_BOOK_STATUS_INVALID_QUERY, N_("Invalid query") },
+ { E_DATA_BOOK_STATUS_QUERY_REFUSED, N_("Query refused") },
+ { E_DATA_BOOK_STATUS_COULD_NOT_CANCEL, N_("Could not cancel") },
+ /* { E_DATA_BOOK_STATUS_OTHER_ERROR, N_("Other error") }, */
+ { E_DATA_BOOK_STATUS_INVALID_SERVER_VERSION, N_("Invalid server version") },
+ { E_DATA_BOOK_STATUS_NO_SPACE, N_("No space") },
+ { E_DATA_BOOK_STATUS_INVALID_ARG, N_("Invalid argument") },
+ /* Translators: The string for NOT_SUPPORTED error */
+ { E_DATA_BOOK_STATUS_NOT_SUPPORTED, N_("Not supported") },
+ { E_DATA_BOOK_STATUS_NOT_OPENED, N_("Backend is not opened yet") },
+ { E_DATA_BOOK_STATUS_OUT_OF_SYNC, N_("Object is out of sync") }
+ };
+
+ for (i = 0; i < G_N_ELEMENTS (statuses); i++) {
+ if (statuses[i].status == status)
+ return _(statuses[i].msg);
+ }
+
+ return _("Other error");
+}
+
+/* Create the EDataBook error quark */
+GQuark
+e_data_book_error_quark (void)
+{
+ #define ERR_PREFIX "org.gnome.evolution.dataserver.AddressBook."
+
+ static const GDBusErrorEntry entries[] = {
+ { E_DATA_BOOK_STATUS_SUCCESS, ERR_PREFIX "Success" },
+ { E_DATA_BOOK_STATUS_BUSY, ERR_PREFIX "Busy" },
+ { E_DATA_BOOK_STATUS_REPOSITORY_OFFLINE, ERR_PREFIX "RepositoryOffline" },
+ { E_DATA_BOOK_STATUS_PERMISSION_DENIED, ERR_PREFIX "PermissionDenied" },
+ { E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, ERR_PREFIX "ContactNotFound" },
+ { E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS, ERR_PREFIX "ContactIDAlreadyExists" },
+ { E_DATA_BOOK_STATUS_AUTHENTICATION_FAILED, ERR_PREFIX "AuthenticationFailed" },
+ { E_DATA_BOOK_STATUS_AUTHENTICATION_REQUIRED, ERR_PREFIX "AuthenticationRequired" },
+ { E_DATA_BOOK_STATUS_UNSUPPORTED_FIELD, ERR_PREFIX "UnsupportedField" },
+ { E_DATA_BOOK_STATUS_UNSUPPORTED_AUTHENTICATION_METHOD, ERR_PREFIX "UnsupportedAuthenticationMethod" },
+ { E_DATA_BOOK_STATUS_TLS_NOT_AVAILABLE, ERR_PREFIX "TLSNotAvailable" },
+ { E_DATA_BOOK_STATUS_NO_SUCH_BOOK, ERR_PREFIX "NoSuchBook" },
+ { E_DATA_BOOK_STATUS_BOOK_REMOVED, ERR_PREFIX "BookRemoved" },
+ { E_DATA_BOOK_STATUS_OFFLINE_UNAVAILABLE, ERR_PREFIX "OfflineUnavailable" },
+ { E_DATA_BOOK_STATUS_SEARCH_SIZE_LIMIT_EXCEEDED, ERR_PREFIX "SearchSizeLimitExceeded" },
+ { E_DATA_BOOK_STATUS_SEARCH_TIME_LIMIT_EXCEEDED, ERR_PREFIX "SearchTimeLimitExceeded" },
+ { E_DATA_BOOK_STATUS_INVALID_QUERY, ERR_PREFIX "InvalidQuery" },
+ { E_DATA_BOOK_STATUS_QUERY_REFUSED, ERR_PREFIX "QueryRefused" },
+ { E_DATA_BOOK_STATUS_COULD_NOT_CANCEL, ERR_PREFIX "CouldNotCancel" },
+ { E_DATA_BOOK_STATUS_OTHER_ERROR, ERR_PREFIX "OtherError" },
+ { E_DATA_BOOK_STATUS_INVALID_SERVER_VERSION, ERR_PREFIX "InvalidServerVersion" },
+ { E_DATA_BOOK_STATUS_NO_SPACE, ERR_PREFIX "NoSpace" },
+ { E_DATA_BOOK_STATUS_INVALID_ARG, ERR_PREFIX "InvalidArg" },
+ { E_DATA_BOOK_STATUS_NOT_SUPPORTED, ERR_PREFIX "NotSupported" },
+ { E_DATA_BOOK_STATUS_NOT_OPENED, ERR_PREFIX "NotOpened" },
+ { E_DATA_BOOK_STATUS_OUT_OF_SYNC, ERR_PREFIX "OutOfSync" }
+ };
+
+ #undef ERR_PREFIX
+
+ static volatile gsize quark_volatile = 0;
+
+ g_dbus_error_register_error_domain ("e-data-book-error", &quark_volatile, entries, G_N_ELEMENTS (entries));
+
+ return (GQuark) quark_volatile;
+}
+
+/**
+ * e_data_book_create_error:
+ * @status: #EDataBookStatus code
+ * @custom_msg: Custom message to use for the error. When NULL,
+ * then uses a default message based on the @status code.
+ *
+ * Returns: NULL, when the @status is E_DATA_BOOK_STATUS_SUCCESS,
+ * or a newly allocated GError, which should be freed
+ * with g_error_free() call.
+ *
+ * Since: 2.32
+ **/
+GError *
+e_data_book_create_error (EDataBookStatus status,
+ const gchar *custom_msg)
+{
+ if (status == E_DATA_BOOK_STATUS_SUCCESS)
+ return NULL;
+
+ return g_error_new_literal (E_DATA_BOOK_ERROR, status, custom_msg ? custom_msg : e_data_book_status_to_string (status));
+}
+
+/**
+ * e_data_book_create_error_fmt:
+ * @status: an #EDataBookStatus
+ * @custom_msg_fmt: Custom message to use for the error. When NULL,
+ * then uses a default message based on the @status code.
+ * @...: arguments for the @custom_msg_fmt
+ *
+ * Similar as e_data_book_create_error(), only here, instead of custom_msg,
+ * is used a printf() format to create a custom_msg for the error.
+ *
+ * Returns: (transfer full): a new #GError populated with the values
+ * from the parameters.
+ *
+ * Since: 2.32
+ **/
+GError *
+e_data_book_create_error_fmt (EDataBookStatus status,
+ const gchar *custom_msg_fmt,
+ ...)
+{
+ GError *error;
+ gchar *custom_msg;
+ va_list ap;
+
+ if (!custom_msg_fmt)
+ return e_data_book_create_error (status, NULL);
+
+ va_start (ap, custom_msg_fmt);
+ custom_msg = g_strdup_vprintf (custom_msg_fmt, ap);
+ va_end (ap);
+
+ error = e_data_book_create_error (status, custom_msg);
+
+ g_free (custom_msg);
+
+ return error;
+}
+
+/**
+ * e_data_book_string_slist_to_comma_string:
+ * @strings: (element-type gchar *): a list of gchar *
+ *
+ * Takes a list of strings and converts it to a comma-separated string of
+ * values; free returned pointer with g_free()
+ *
+ * Returns: (transfer full): comma-separated newly allocated text of @strings
+ *
+ * Since: 3.2
+ **/
+gchar *
+e_data_book_string_slist_to_comma_string (const GSList *strings)
+{
+ GString *tmp;
+ gchar *res;
+ const GSList *l;
+
+ tmp = g_string_new ("");
+ for (l = strings; l != NULL; l = l->next) {
+ const gchar *str = l->data;
+
+ if (!str)
+ continue;
+
+ if (strchr (str, ',')) {
+ g_warning ("%s: String cannot contain comma; skipping value '%s'\n", G_STRFUNC, str);
+ continue;
+ }
+
+ if (tmp->len)
+ g_string_append_c (tmp, ',');
+ g_string_append (tmp, str);
+ }
+
+ res = e_util_utf8_make_valid (tmp->str);
+
+ g_string_free (tmp, TRUE);
+
+ return res;
+}
+
+static GPtrArray *
+data_book_encode_properties (EDBusAddressBook *dbus_interface)
+{
+ GPtrArray *properties_array;
+
+ g_warn_if_fail (E_DBUS_IS_ADDRESS_BOOK (dbus_interface));
+
+ properties_array = g_ptr_array_new_with_free_func (g_free);
+
+ if (dbus_interface) {
+ GParamSpec **properties;
+ guint ii, n_properties = 0;
+
+ properties = g_object_class_list_properties (G_OBJECT_GET_CLASS (dbus_interface), &n_properties);
+
+ for (ii = 0; ii < n_properties; ii++) {
+ gboolean can_process =
+ g_type_is_a (properties[ii]->value_type, G_TYPE_BOOLEAN) ||
+ g_type_is_a (properties[ii]->value_type, G_TYPE_STRING) ||
+ g_type_is_a (properties[ii]->value_type, G_TYPE_STRV) ||
+ g_type_is_a (properties[ii]->value_type, G_TYPE_UCHAR) ||
+ g_type_is_a (properties[ii]->value_type, G_TYPE_INT) ||
+ g_type_is_a (properties[ii]->value_type, G_TYPE_UINT) ||
+ g_type_is_a (properties[ii]->value_type, G_TYPE_INT64) ||
+ g_type_is_a (properties[ii]->value_type, G_TYPE_UINT64) ||
+ g_type_is_a (properties[ii]->value_type, G_TYPE_DOUBLE);
+
+ if (can_process) {
+ GValue value = G_VALUE_INIT;
+ GVariant *stored = NULL;
+
+ g_value_init (&value, properties[ii]->value_type);
+ g_object_get_property ((GObject *) dbus_interface, properties[ii]->name, &value);
+
+ #define WORKOUT(gvl, gvr) \
+ if (g_type_is_a (properties[ii]->value_type, G_TYPE_ ## gvl)) \
+ stored = g_dbus_gvalue_to_gvariant (&value, G_VARIANT_TYPE_ ## gvr);
+
+ WORKOUT (BOOLEAN, BOOLEAN);
+ WORKOUT (STRING, STRING);
+ WORKOUT (STRV, STRING_ARRAY);
+ WORKOUT (UCHAR, BYTE);
+ WORKOUT (INT, INT32);
+ WORKOUT (UINT, UINT32);
+ WORKOUT (INT64, INT64);
+ WORKOUT (UINT64, UINT64);
+ WORKOUT (DOUBLE, DOUBLE);
+
+ #undef WORKOUT
+
+ g_value_unset (&value);
+
+ if (stored) {
+ g_ptr_array_add (properties_array, g_strdup (properties[ii]->name));
+ g_ptr_array_add (properties_array, g_variant_print (stored, TRUE));
+
+ g_variant_unref (stored);
+ }
+ }
+ }
+
+ g_free (properties);
+ }
+
+ g_ptr_array_add (properties_array, NULL);
+
+ return properties_array;
+}
+
+static gboolean
+data_book_handle_retrieve_properties_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ EDataBook *data_book)
+{
+ GPtrArray *properties_array;
+
+ properties_array = data_book_encode_properties (dbus_interface);
+
+ e_dbus_address_book_complete_retrieve_properties (
+ dbus_interface,
+ invocation,
+ (const gchar * const *) properties_array->pdata);
+
+ g_ptr_array_free (properties_array, TRUE);
+
+ return TRUE;
+}
+
+static void
+data_book_complete_open_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncContext *async_context = user_data;
+ GError *error = NULL;
+
+ e_book_backend_open_finish (
+ E_BOOK_BACKEND (source_object), result, &error);
+
+ if (error == NULL) {
+ GPtrArray *properties_array;
+
+ properties_array = data_book_encode_properties (async_context->dbus_interface);
+
+ e_dbus_address_book_complete_open (
+ async_context->dbus_interface,
+ async_context->invocation,
+ (const gchar * const *) properties_array->pdata);
+
+ g_ptr_array_free (properties_array, TRUE);
+ } else {
+ data_book_convert_to_client_error (error);
+ g_dbus_method_invocation_take_error (
+ async_context->invocation, error);
+ }
+
+ async_context_free (async_context);
+}
+
+static gboolean
+data_book_handle_open_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ AsyncContext *async_context;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ async_context = async_context_new (data_book, invocation);
+
+ e_book_backend_open (
+ backend,
+ async_context->cancellable,
+ data_book_complete_open_cb,
+ async_context);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+static void
+data_book_complete_refresh_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncContext *async_context = user_data;
+ GError *error = NULL;
+
+ e_book_backend_refresh_finish (
+ E_BOOK_BACKEND (source_object), result, &error);
+
+ if (error == NULL) {
+ e_dbus_address_book_complete_refresh (
+ async_context->dbus_interface,
+ async_context->invocation);
+ } else {
+ data_book_convert_to_client_error (error);
+ g_dbus_method_invocation_take_error (
+ async_context->invocation, error);
+ }
+
+ async_context_free (async_context);
+}
+
+static gboolean
+data_book_handle_refresh_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ AsyncContext *async_context;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ async_context = async_context_new (data_book, invocation);
+
+ e_book_backend_refresh (
+ backend,
+ async_context->cancellable,
+ data_book_complete_refresh_cb,
+ async_context);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+static void
+data_book_complete_get_contact_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncContext *async_context = user_data;
+ EContact *contact;
+ GError *error = NULL;
+
+ contact = e_book_backend_get_contact_finish (
+ E_BOOK_BACKEND (source_object), result, &error);
+
+ /* Sanity check. */
+ g_return_if_fail (
+ ((contact != NULL) && (error == NULL)) ||
+ ((contact == NULL) && (error != NULL)));
+
+ if (contact != NULL) {
+ gchar *vcard;
+ gchar *utf8_vcard;
+
+ vcard = e_vcard_to_string (
+ E_VCARD (contact),
+ EVC_FORMAT_VCARD_30);
+ utf8_vcard = e_util_utf8_make_valid (vcard);
+ e_dbus_address_book_complete_get_contact (
+ async_context->dbus_interface,
+ async_context->invocation,
+ utf8_vcard);
+ g_free (utf8_vcard);
+ g_free (vcard);
+
+ g_object_unref (contact);
+ } else {
+ data_book_convert_to_client_error (error);
+ g_dbus_method_invocation_take_error (
+ async_context->invocation, error);
+ }
+
+ async_context_free (async_context);
+}
+
+static gboolean
+data_book_handle_get_contact_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ const gchar *in_uid,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ AsyncContext *async_context;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ async_context = async_context_new (data_book, invocation);
+
+ e_book_backend_get_contact (
+ backend, in_uid,
+ async_context->cancellable,
+ data_book_complete_get_contact_cb,
+ async_context);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+static void
+data_book_complete_get_contact_list_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncContext *async_context = user_data;
+ GQueue queue = G_QUEUE_INIT;
+ GError *error = NULL;
+
+ e_book_backend_get_contact_list_finish (
+ E_BOOK_BACKEND (source_object), result, &queue, &error);
+
+ if (error == NULL) {
+ gchar **strv;
+ gint ii = 0;
+
+ strv = g_new0 (gchar *, queue.length + 1);
+
+ while (!g_queue_is_empty (&queue)) {
+ EContact *contact;
+ gchar *vcard;
+
+ contact = g_queue_pop_head (&queue);
+
+ vcard = e_vcard_to_string (
+ E_VCARD (contact),
+ EVC_FORMAT_VCARD_30);
+ strv[ii++] = e_util_utf8_make_valid (vcard);
+ g_free (vcard);
+
+ g_object_unref (contact);
+ }
+
+ e_dbus_address_book_complete_get_contact_list (
+ async_context->dbus_interface,
+ async_context->invocation,
+ (const gchar * const *) strv);
+
+ g_strfreev (strv);
+ } else {
+ data_book_convert_to_client_error (error);
+ g_dbus_method_invocation_take_error (
+ async_context->invocation, error);
+ }
+
+ async_context_free (async_context);
+}
+
+static gboolean
+data_book_handle_get_contact_list_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ const gchar *in_query,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ AsyncContext *async_context;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ async_context = async_context_new (data_book, invocation);
+
+ e_book_backend_get_contact_list (
+ backend, in_query,
+ async_context->cancellable,
+ data_book_complete_get_contact_list_cb,
+ async_context);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+static void
+data_book_complete_get_contact_list_uids_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncContext *async_context = user_data;
+ GQueue queue = G_QUEUE_INIT;
+ GError *error = NULL;
+
+ e_book_backend_get_contact_list_uids_finish (
+ E_BOOK_BACKEND (source_object), result, &queue, &error);
+
+ if (error == NULL) {
+ gchar **strv;
+ gint ii = 0;
+
+ strv = g_new0 (gchar *, queue.length + 1);
+
+ while (!g_queue_is_empty (&queue)) {
+ gchar *uid = g_queue_pop_head (&queue);
+ strv[ii++] = e_util_utf8_make_valid (uid);
+ g_free (uid);
+ }
+
+ e_dbus_address_book_complete_get_contact_list_uids (
+ async_context->dbus_interface,
+ async_context->invocation,
+ (const gchar * const *) strv);
+
+ g_strfreev (strv);
+ } else {
+ data_book_convert_to_client_error (error);
+ g_dbus_method_invocation_take_error (
+ async_context->invocation, error);
+ }
+
+ async_context_free (async_context);
+}
+
+static gboolean
+data_book_handle_get_contact_list_uids_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ const gchar *in_query,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ AsyncContext *async_context;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ async_context = async_context_new (data_book, invocation);
+
+ e_book_backend_get_contact_list_uids (
+ backend, in_query,
+ async_context->cancellable,
+ data_book_complete_get_contact_list_uids_cb,
+ async_context);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+static void
+data_book_complete_create_contacts_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncContext *async_context = user_data;
+ GQueue queue = G_QUEUE_INIT;
+ GError *error = NULL;
+
+ e_book_backend_create_contacts_finish (
+ E_BOOK_BACKEND (source_object), result, &queue, &error);
+
+ if (error == NULL) {
+ gchar **strv;
+ gint ii = 0;
+
+ strv = g_new0 (gchar *, queue.length + 1);
+
+ while (!g_queue_is_empty (&queue)) {
+ EContact *contact;
+ const gchar *uid;
+
+ contact = g_queue_pop_head (&queue);
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ strv[ii++] = e_util_utf8_make_valid (uid);
+ g_object_unref (contact);
+ }
+
+ e_dbus_address_book_complete_create_contacts (
+ async_context->dbus_interface,
+ async_context->invocation,
+ (const gchar * const *) strv);
+
+ g_strfreev (strv);
+ } else {
+ data_book_convert_to_client_error (error);
+ g_dbus_method_invocation_take_error (
+ async_context->invocation, error);
+ }
+
+ async_context_free (async_context);
+}
+
+static gboolean
+data_book_handle_create_contacts_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ const gchar * const *in_vcards,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ AsyncContext *async_context;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ async_context = async_context_new (data_book, invocation);
+
+ e_book_backend_create_contacts (
+ backend, in_vcards,
+ async_context->cancellable,
+ data_book_complete_create_contacts_cb,
+ async_context);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+static void
+data_book_complete_modify_contacts_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncContext *async_context = user_data;
+ GError *error = NULL;
+
+ e_book_backend_modify_contacts_finish (
+ E_BOOK_BACKEND (source_object), result, &error);
+
+ if (error == NULL) {
+ e_dbus_address_book_complete_modify_contacts (
+ async_context->dbus_interface,
+ async_context->invocation);
+ } else {
+ data_book_convert_to_client_error (error);
+ g_dbus_method_invocation_take_error (
+ async_context->invocation, error);
+ }
+
+ async_context_free (async_context);
+}
+
+static gboolean
+data_book_handle_modify_contacts_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ const gchar * const *in_vcards,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ AsyncContext *async_context;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ async_context = async_context_new (data_book, invocation);
+
+ e_book_backend_modify_contacts (
+ backend, in_vcards,
+ async_context->cancellable,
+ data_book_complete_modify_contacts_cb,
+ async_context);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+static void
+data_book_complete_remove_contacts_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncContext *async_context = user_data;
+ GError *error = NULL;
+
+ e_book_backend_remove_contacts_finish (
+ E_BOOK_BACKEND (source_object), result, &error);
+
+ if (error == NULL) {
+ e_dbus_address_book_complete_remove_contacts (
+ async_context->dbus_interface,
+ async_context->invocation);
+ } else {
+ data_book_convert_to_client_error (error);
+ g_dbus_method_invocation_take_error (
+ async_context->invocation, error);
+ }
+
+ async_context_free (async_context);
+}
+
+static gboolean
+data_book_handle_remove_contacts_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ const gchar * const *in_uids,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ AsyncContext *async_context;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ async_context = async_context_new (data_book, invocation);
+
+ e_book_backend_remove_contacts (
+ backend, in_uids,
+ async_context->cancellable,
+ data_book_complete_remove_contacts_cb,
+ async_context);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+static gboolean
+data_book_handle_get_view_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ const gchar *in_query,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ EDataBookView *view;
+ EBookBackendSExp *sexp;
+ GDBusConnection *connection;
+ gchar *object_path;
+ GError *error = NULL;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ sexp = e_book_backend_sexp_new (in_query);
+ if (sexp == NULL) {
+ g_dbus_method_invocation_return_error_literal (
+ invocation,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_INVALID_QUERY,
+ _("Invalid query"));
+ g_object_unref (backend);
+ return TRUE;
+ }
+
+ object_path = construct_bookview_path ();
+ connection = g_dbus_method_invocation_get_connection (invocation);
+
+ view = e_data_book_view_new (
+ backend, sexp, connection, object_path, &error);
+
+ g_object_unref (sexp);
+
+ /* Sanity check. */
+ g_return_val_if_fail (
+ ((view != NULL) && (error == NULL)) ||
+ ((view == NULL) && (error != NULL)), FALSE);
+
+ if (view != NULL) {
+ e_dbus_address_book_complete_get_view (
+ dbus_interface, invocation, object_path);
+ e_book_backend_add_view (backend, view);
+ g_object_unref (view);
+ } else {
+ data_book_convert_to_client_error (error);
+ g_prefix_error (&error, "%s", _("Invalid query: "));
+ g_dbus_method_invocation_take_error (invocation, error);
+ }
+
+ g_free (object_path);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+static gboolean
+data_book_interpret_sort_keys (const gchar * const *in_sort_keys,
+ const gchar * const *in_sort_types,
+ EContactField **out_sort_keys,
+ EBookCursorSortType **out_sort_types,
+ gint *n_fields,
+ GError **error)
+{
+ gint i, key_count = 0, type_count = 0;
+ EContactField *sort_keys;
+ EBookCursorSortType *sort_types;
+ gboolean success = TRUE;
+
+ if (!in_sort_keys || !in_sort_types) {
+ g_set_error (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_INVALID_ARG,
+ "Missing sort keys while trying to create a Cursor");
+ return FALSE;
+ }
+
+ for (i = 0; in_sort_keys[i] != NULL; i++)
+ key_count++;
+ for (i = 0; in_sort_types[i] != NULL; i++)
+ type_count++;
+
+ if (key_count != type_count) {
+ g_set_error (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_INVALID_ARG,
+ "Must specify the same amount of sort keys as sort types while creating a Cursor");
+ return FALSE;
+ }
+
+ sort_keys = g_new0 (EContactField, key_count);
+ sort_types = g_new0 (EBookCursorSortType, type_count);
+
+ for (i = 0; success && i < key_count; i++) {
+
+ sort_keys[i] = e_contact_field_id (in_sort_keys[i]);
+
+ if (sort_keys[i] == 0) {
+ g_set_error (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_INVALID_ARG,
+ "Invalid sort key '%s' specified when creating a Cursor",
+ in_sort_keys[i]);
+ success = FALSE;
+ }
+ }
+
+ for (i = 0; success && i < type_count; i++) {
+ gint enum_value = 0;
+
+ if (!e_enum_from_string (E_TYPE_BOOK_CURSOR_SORT_TYPE,
+ in_sort_types[i],
+ &enum_value)) {
+ g_set_error (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_INVALID_ARG,
+ "Invalid sort type '%s' specified when creating a Cursor",
+ in_sort_types[i]);
+ success = FALSE;
+ }
+
+ sort_types[i] = enum_value;
+ }
+
+ if (!success) {
+ g_free (sort_keys);
+ g_free (sort_types);
+ } else {
+ *out_sort_keys = sort_keys;
+ *out_sort_types = sort_types;
+ *n_fields = key_count;
+ }
+
+ return success;
+}
+
+static gboolean
+data_book_handle_get_cursor_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ const gchar *in_query,
+ const gchar * const *in_sort_keys,
+ const gchar * const *in_sort_types,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ EDataBookCursor *cursor;
+ GDBusConnection *connection;
+ EContactField *sort_keys = NULL;
+ EBookCursorSortType *sort_types = NULL;
+ gint n_fields = 0;
+ gchar *object_path;
+ GError *error = NULL;
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ /*
+ * Interpret arguments
+ */
+ if (!data_book_interpret_sort_keys (in_sort_keys,
+ in_sort_types,
+ &sort_keys,
+ &sort_types,
+ &n_fields,
+ &error)) {
+ g_dbus_method_invocation_take_error (invocation, error);
+ g_object_unref (backend);
+ return TRUE;
+ }
+
+ /*
+ * Create cursor
+ */
+ cursor = e_book_backend_create_cursor (
+ backend, sort_keys, sort_types, n_fields, &error);
+ g_free (sort_keys);
+ g_free (sort_types);
+
+ if (!cursor) {
+ g_dbus_method_invocation_take_error (invocation, error);
+ g_object_unref (backend);
+ return TRUE;
+ }
+
+ /*
+ * Set the query, if any (no query is allowed)
+ */
+ if (!e_data_book_cursor_set_sexp (cursor, in_query, NULL, &error)) {
+
+ e_book_backend_delete_cursor (backend, cursor, NULL);
+ g_dbus_method_invocation_take_error (invocation, error);
+ g_object_unref (backend);
+ return TRUE;
+ }
+
+ object_path = construct_bookcursor_path ();
+ connection = g_dbus_method_invocation_get_connection (invocation);
+
+ /*
+ * Now export the object on the connection
+ */
+ if (!e_data_book_cursor_register_gdbus_object (cursor, connection, object_path, &error)) {
+ e_book_backend_delete_cursor (backend, cursor, NULL);
+ g_dbus_method_invocation_take_error (invocation, error);
+ g_object_unref (backend);
+ g_free (object_path);
+ return TRUE;
+ }
+
+ /*
+ * All is good in the hood, complete the method call
+ */
+ e_dbus_address_book_complete_get_cursor (
+ dbus_interface, invocation, object_path);
+ g_free (object_path);
+ g_object_unref (backend);
+ return TRUE;
+}
+
+static void
+data_book_source_unset_last_credentials_required_arguments_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GError *local_error = NULL;
+
+ g_return_if_fail (E_IS_SOURCE (source_object));
+
+ e_source_unset_last_credentials_required_arguments_finish (E_SOURCE (source_object), result, &local_error);
+
+ if (local_error)
+ g_debug ("%s: Call failed: %s", G_STRFUNC, local_error->message);
+
+ g_clear_error (&local_error);
+}
+
+static gboolean
+data_book_handle_close_cb (EDBusAddressBook *dbus_interface,
+ GDBusMethodInvocation *invocation,
+ EDataBook *data_book)
+{
+ EBookBackend *backend;
+ ESource *source;
+ const gchar *sender;
+
+ /* G_DBUS_MESSAGE_FLAGS_NO_REPLY_EXPECTED should be set on
+ * the GDBusMessage, but we complete the invocation anyway
+ * and let the D-Bus machinery suppress the reply. */
+ e_dbus_address_book_complete_close (dbus_interface, invocation);
+
+ backend = e_data_book_ref_backend (data_book);
+ g_return_val_if_fail (backend != NULL, FALSE);
+
+ source = e_backend_get_source (E_BACKEND (backend));
+ e_source_unset_last_credentials_required_arguments (source, NULL,
+ data_book_source_unset_last_credentials_required_arguments_cb, NULL);
+
+ sender = g_dbus_method_invocation_get_sender (invocation);
+ g_signal_emit_by_name (backend, "closed", sender);
+
+ g_object_unref (backend);
+
+ return TRUE;
+}
+
+/**
+ * e_data_book_respond_open:
+ * @book: An #EDataBook
+ * @opid: An operation ID
+ * @error: Operation error, if any, automatically freed if passed it
+ *
+ * Notifies listeners of the completion of the open method call.
+ **/
+void
+e_data_book_respond_open (EDataBook *book,
+ guint opid,
+ GError *error)
+{
+ EBookBackend *backend;
+ GSimpleAsyncResult *simple;
+
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+
+ backend = e_data_book_ref_backend (book);
+ g_return_if_fail (backend != NULL);
+
+ simple = e_book_backend_prepare_for_completion (backend, opid, NULL);
+ g_return_if_fail (simple != NULL);
+
+ /* Translators: This is prefix to a detailed error message */
+ g_prefix_error (&error, "%s", _("Cannot open book: "));
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+
+ g_simple_async_result_complete_in_idle (simple);
+
+ g_object_unref (simple);
+ g_object_unref (backend);
+}
+
+/**
+ * e_data_book_respond_refresh:
+ * @book: An #EDataBook
+ * @opid: An operation ID
+ * @error: Operation error, if any, automatically freed if passed it
+ *
+ * Notifies listeners of the completion of the refresh method call.
+ *
+ * Since: 3.2
+ */
+void
+e_data_book_respond_refresh (EDataBook *book,
+ guint32 opid,
+ GError *error)
+{
+ EBookBackend *backend;
+ GSimpleAsyncResult *simple;
+
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+
+ backend = e_data_book_ref_backend (book);
+ g_return_if_fail (backend != NULL);
+
+ simple = e_book_backend_prepare_for_completion (backend, opid, NULL);
+ g_return_if_fail (simple);
+
+ /* Translators: This is prefix to a detailed error message */
+ g_prefix_error (&error, "%s", _("Cannot refresh address book: "));
+
+ if (error != NULL)
+ g_simple_async_result_take_error (simple, error);
+
+ g_simple_async_result_complete_in_idle (simple);
+
+ g_object_unref (simple);
+ g_object_unref (backend);
+}
+
+/**
+ * e_data_book_respond_get_contact:
+ * @book: An #EDataBook
+ * @opid: An operation ID
+ * @error: Operation error, if any, automatically freed if passed it
+ *
+ * Notifies listeners of the completion of the get_contact method call.
+ */
+void
+e_data_book_respond_get_contact (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const gchar *vcard)
+{
+ EBookBackend *backend;
+ GSimpleAsyncResult *simple;
+ GQueue *queue = NULL;
+
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+
+ backend = e_data_book_ref_backend (book);
+ g_return_if_fail (backend != NULL);
+
+ simple = e_book_backend_prepare_for_completion (backend, opid, &queue);
+ g_return_if_fail (simple != NULL);
+ g_return_if_fail (queue != NULL);
+
+ /* Translators: This is prefix to a detailed error message */
+ g_prefix_error (&error, "%s", _("Cannot get contact: "));
+
+ if (error == NULL) {
+ EContact *contact;
+
+ contact = e_contact_new_from_vcard (vcard);
+ g_queue_push_tail (queue, g_object_ref (contact));
+ g_object_unref (contact);
+ } else {
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ g_simple_async_result_complete_in_idle (simple);
+
+ g_object_unref (simple);
+ g_object_unref (backend);
+}
+
+/**
+ * e_data_book_respond_get_contact_list:
+ * @book: An #EDataBook
+ * @opid: An operation ID
+ * @error: Operation error, if any, automatically freed if passed it
+ * @cards: (allow-none) (element-type gchar *): A list of vCard strings, or %NULL on error
+ *
+ * Finishes a call to get list of vCards which satisfy certain criteria.
+ *
+ * Since: 3.2
+ **/
+void
+e_data_book_respond_get_contact_list (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *cards)
+{
+ EBookBackend *backend;
+ GSimpleAsyncResult *simple;
+ GQueue *queue = NULL;
+
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+
+ backend = e_data_book_ref_backend (book);
+ g_return_if_fail (backend != NULL);
+
+ simple = e_book_backend_prepare_for_completion (backend, opid, &queue);
+ g_return_if_fail (simple != NULL);
+ g_return_if_fail (queue != NULL);
+
+ /* Translators: This is prefix to a detailed error message */
+ g_prefix_error (&error, "%s", _("Cannot get contact list: "));
+
+ if (error == NULL) {
+ GSList *list, *link;
+
+ list = (GSList *) cards;
+
+ for (link = list; link != NULL; link = g_slist_next (link)) {
+ EContact *contact;
+
+ contact = e_contact_new_from_vcard (link->data);
+ g_queue_push_tail (queue, g_object_ref (contact));
+ g_object_unref (contact);
+ }
+
+ } else {
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ g_simple_async_result_complete_in_idle (simple);
+
+ g_object_unref (simple);
+ g_object_unref (backend);
+}
+
+/**
+ * e_data_book_respond_get_contact_list_uids:
+ * @book: An #EDataBook
+ * @opid: An operation ID
+ * @error: Operation error, if any, automatically freed if passed it
+ * @uids: (allow-none) (element-type gchar *): A list of picked UIDs, or %NULL on error
+ *
+ * Finishes a call to get list of UIDs which satisfy certain criteria.
+ *
+ * Since: 3.2
+ **/
+void
+e_data_book_respond_get_contact_list_uids (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *uids)
+{
+ EBookBackend *backend;
+ GSimpleAsyncResult *simple;
+ GQueue *queue = NULL;
+
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+
+ backend = e_data_book_ref_backend (book);
+ g_return_if_fail (backend != NULL);
+
+ simple = e_book_backend_prepare_for_completion (backend, opid, &queue);
+ g_return_if_fail (simple != NULL);
+ g_return_if_fail (queue != NULL);
+
+ /* Translators: This is prefix to a detailed error message */
+ g_prefix_error (&error, "%s", _("Cannot get contact list uids: "));
+
+ if (error == NULL) {
+ GSList *list, *link;
+
+ list = (GSList *) uids;
+
+ for (link = list; link != NULL; link = g_slist_next (link))
+ g_queue_push_tail (queue, g_strdup (link->data));
+
+ } else {
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ g_simple_async_result_complete_in_idle (simple);
+
+ g_object_unref (simple);
+ g_object_unref (backend);
+}
+
+/**
+ * e_data_book_respond_create_contacts:
+ * @book: An #EDataBook
+ * @opid: An operation ID
+ * @error: Operation error, if any, automatically freed if passed it
+ * @contacts: (allow-none) (element-type EContact): A list of created #EContact-s, or %NULL on error
+ *
+ * Finishes a call to create a list contacts.
+ *
+ * Since: 3.4
+ **/
+void
+e_data_book_respond_create_contacts (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *contacts)
+{
+ EBookBackend *backend;
+ GSimpleAsyncResult *simple;
+ GQueue *queue = NULL;
+
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+
+ backend = e_data_book_ref_backend (book);
+ g_return_if_fail (backend != NULL);
+
+ simple = e_book_backend_prepare_for_completion (backend, opid, &queue);
+ g_return_if_fail (simple != NULL);
+ g_return_if_fail (queue != NULL);
+
+ /* Translators: This is prefix to a detailed error message */
+ g_prefix_error (&error, "%s", _("Cannot add contact: "));
+
+ if (error == NULL) {
+ GSList *list, *link;
+
+ list = (GSList *) contacts;
+
+ for (link = list; link != NULL; link = g_slist_next (link)) {
+ EContact *contact = E_CONTACT (link->data);
+ g_queue_push_tail (queue, g_object_ref (contact));
+ }
+
+ } else {
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ g_simple_async_result_complete_in_idle (simple);
+
+ g_object_unref (simple);
+ g_object_unref (backend);
+}
+
+/**
+ * e_data_book_respond_modify_contacts:
+ * @book: An #EDataBook
+ * @opid: An operation ID
+ * @error: Operation error, if any, automatically freed if passed it
+ * @contacts: (allow-none) (element-type EContact): A list of modified #EContact-s, or %NULL on error
+ *
+ * Finishes a call to modify a list of contacts.
+ *
+ * Since: 3.4
+ **/
+void
+e_data_book_respond_modify_contacts (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *contacts)
+{
+ EBookBackend *backend;
+ GSimpleAsyncResult *simple;
+ GQueue *queue = NULL;
+
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+
+ backend = e_data_book_ref_backend (book);
+ g_return_if_fail (backend != NULL);
+
+ simple = e_book_backend_prepare_for_completion (backend, opid, &queue);
+ g_return_if_fail (simple != NULL);
+ g_return_if_fail (queue != NULL);
+
+ /* Translators: This is prefix to a detailed error message */
+ g_prefix_error (&error, "%s", _("Cannot modify contacts: "));
+
+ if (error == NULL) {
+ GSList *list, *link;
+
+ list = (GSList *) contacts;
+
+ for (link = list; link != NULL; link = g_slist_next (link)) {
+ EContact *contact = E_CONTACT (contacts->data);
+ g_queue_push_tail (queue, g_object_ref (contact));
+ }
+
+ } else {
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ g_simple_async_result_complete_in_idle (simple);
+
+ g_object_unref (simple);
+ g_object_unref (backend);
+}
+
+/**
+ * e_data_book_respond_remove_contacts:
+ * @book: An #EDataBook
+ * @opid: An operation ID
+ * @error: Operation error, if any, automatically freed if passed it
+ * @ids: (allow-none) (element-type gchar *): A list of removed contact UID-s, or %NULL on error
+ *
+ * Finishes a call to remove a list of contacts.
+ *
+ * Since: 3.4
+ **/
+void
+e_data_book_respond_remove_contacts (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *ids)
+{
+ EBookBackend *backend;
+ GSimpleAsyncResult *simple;
+ GQueue *queue = NULL;
+
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+
+ backend = e_data_book_ref_backend (book);
+ g_return_if_fail (backend != NULL);
+
+ simple = e_book_backend_prepare_for_completion (backend, opid, &queue);
+ g_return_if_fail (simple != NULL);
+ g_return_if_fail (queue != NULL);
+
+ /* Translators: This is prefix to a detailed error message */
+ g_prefix_error (&error, "%s", _("Cannot remove contacts: "));
+
+ if (error == NULL) {
+ GSList *list, *link;
+
+ list = (GSList *) ids;
+
+ for (link = list; link != NULL; link = g_slist_next (link))
+ g_queue_push_tail (queue, g_strdup (link->data));
+
+ } else {
+ g_simple_async_result_take_error (simple, error);
+ }
+
+ g_simple_async_result_complete_in_idle (simple);
+
+ g_object_unref (simple);
+ g_object_unref (backend);
+}
+
+/**
+ * e_data_book_report_error:
+ * @book: An #EDataBook
+ * @message: An error message
+ *
+ * Notifies the clients about an error, which happened out of any client-initiate operation.
+ *
+ * Since: 3.2
+ **/
+void
+e_data_book_report_error (EDataBook *book,
+ const gchar *message)
+{
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+ g_return_if_fail (message != NULL);
+
+ e_dbus_address_book_emit_error (book->priv->dbus_interface, message);
+}
+
+/**
+ * e_data_book_report_backend_property_changed:
+ * @book: An #EDataBook
+ * @prop_name: Property name which changed
+ * @prop_value: The new property value
+ *
+ * Notifies the clients about a property change.
+ *
+ * Since: 3.2
+ **/
+void
+e_data_book_report_backend_property_changed (EDataBook *book,
+ const gchar *prop_name,
+ const gchar *prop_value)
+{
+ EDBusAddressBook *dbus_interface;
+ gchar **strv;
+
+ g_return_if_fail (E_IS_DATA_BOOK (book));
+ g_return_if_fail (prop_name != NULL);
+
+ if (prop_value == NULL)
+ prop_value = "";
+
+ dbus_interface = book->priv->dbus_interface;
+
+ /* XXX This will be NULL in direct access mode. No way to
+ * report property changes, I guess. Return silently. */
+ if (dbus_interface == NULL)
+ return;
+
+ if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+ strv = g_strsplit (prop_value, ",", -1);
+ e_dbus_address_book_set_capabilities (
+ dbus_interface, (const gchar * const *) strv);
+ g_strfreev (strv);
+ }
+
+ if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_REVISION))
+ e_dbus_address_book_set_revision (dbus_interface, prop_value);
+
+ if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
+ strv = g_strsplit (prop_value, ",", -1);
+ e_dbus_address_book_set_required_fields (
+ dbus_interface, (const gchar * const *) strv);
+ g_strfreev (strv);
+ }
+
+ if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
+ strv = g_strsplit (prop_value, ",", -1);
+ e_dbus_address_book_set_supported_fields (
+ dbus_interface, (const gchar * const *) strv);
+ g_strfreev (strv);
+ }
+
+ /* Disregard anything else. */
+}
+
+static void
+data_book_set_backend (EDataBook *book,
+ EBookBackend *backend)
+{
+ g_return_if_fail (E_IS_BOOK_BACKEND (backend));
+
+ g_weak_ref_set (&book->priv->backend, backend);
+}
+
+static void
+data_book_set_connection (EDataBook *book,
+ GDBusConnection *connection)
+{
+ g_return_if_fail (connection == NULL ||
+ G_IS_DBUS_CONNECTION (connection));
+ g_return_if_fail (book->priv->connection == NULL);
+
+ if (connection)
+ book->priv->connection = g_object_ref (connection);
+}
+
+static void
+data_book_set_object_path (EDataBook *book,
+ const gchar *object_path)
+{
+ g_return_if_fail (book->priv->object_path == NULL);
+
+ book->priv->object_path = g_strdup (object_path);
+}
+
+static void
+data_book_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_BACKEND:
+ data_book_set_backend (
+ E_DATA_BOOK (object),
+ g_value_get_object (value));
+ return;
+
+ case PROP_CONNECTION:
+ data_book_set_connection (
+ E_DATA_BOOK (object),
+ g_value_get_object (value));
+ return;
+
+ case PROP_OBJECT_PATH:
+ data_book_set_object_path (
+ E_DATA_BOOK (object),
+ g_value_get_string (value));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+data_book_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_BACKEND:
+ g_value_take_object (
+ value,
+ e_data_book_ref_backend (
+ E_DATA_BOOK (object)));
+ return;
+
+ case PROP_CONNECTION:
+ g_value_set_object (
+ value,
+ e_data_book_get_connection (
+ E_DATA_BOOK (object)));
+ return;
+
+ case PROP_OBJECT_PATH:
+ g_value_set_string (
+ value,
+ e_data_book_get_object_path (
+ E_DATA_BOOK (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+data_book_dispose (GObject *object)
+{
+ EDataBookPrivate *priv;
+
+ priv = E_DATA_BOOK_GET_PRIVATE (object);
+
+ g_weak_ref_set (&priv->backend, NULL);
+
+ if (priv->connection != NULL) {
+ g_object_unref (priv->connection);
+ priv->connection = NULL;
+ }
+
+ if (priv->direct_book) {
+ g_object_unref (priv->direct_book);
+ priv->direct_book = NULL;
+ }
+
+ if (priv->direct_module) {
+ g_type_module_unuse (G_TYPE_MODULE (priv->direct_module));
+ priv->direct_module = NULL;
+ }
+
+ g_hash_table_remove_all (priv->sender_table);
+
+ /* Chain up to parent's dispose() metnod. */
+ G_OBJECT_CLASS (e_data_book_parent_class)->dispose (object);
+}
+
+static void
+data_book_finalize (GObject *object)
+{
+ EDataBookPrivate *priv;
+
+ priv = E_DATA_BOOK_GET_PRIVATE (object);
+
+ g_free (priv->object_path);
+
+ g_mutex_clear (&priv->sender_lock);
+ g_weak_ref_clear (&priv->backend);
+ g_hash_table_destroy (priv->sender_table);
+
+ if (priv->dbus_interface) {
+ g_object_unref (priv->dbus_interface);
+ priv->dbus_interface = NULL;
+ }
+
+ /* Chain up to parent's finalize() method. */
+ G_OBJECT_CLASS (e_data_book_parent_class)->finalize (object);
+}
+
+static void
+data_book_constructed (GObject *object)
+{
+ EDataBook *book = E_DATA_BOOK (object);
+ EBookBackend *backend;
+ const gchar *prop_name;
+ gchar *prop_value;
+
+ /* Chain up to parent's constructed() method. */
+ G_OBJECT_CLASS (e_data_book_parent_class)->constructed (object);
+
+ backend = e_data_book_ref_backend (book);
+ g_warn_if_fail (backend != NULL);
+
+ /* Attach ourselves to the EBookBackend. */
+ e_book_backend_set_data_book (backend, book);
+
+ e_binding_bind_property (
+ backend, "cache-dir",
+ book->priv->dbus_interface, "cache-dir",
+ G_BINDING_SYNC_CREATE);
+
+ e_binding_bind_property (
+ backend, "online",
+ book->priv->dbus_interface, "online",
+ G_BINDING_SYNC_CREATE);
+
+ e_binding_bind_property (
+ backend, "writable",
+ book->priv->dbus_interface, "writable",
+ G_BINDING_SYNC_CREATE);
+
+ /* XXX Initialize the rest of the properties. */
+
+ prop_name = CLIENT_BACKEND_PROPERTY_CAPABILITIES;
+ prop_value = e_book_backend_get_backend_property (backend, prop_name);
+ e_data_book_report_backend_property_changed (
+ book, prop_name, prop_value);
+ g_free (prop_value);
+
+ prop_name = CLIENT_BACKEND_PROPERTY_REVISION;
+ prop_value = e_book_backend_get_backend_property (backend, prop_name);
+ e_data_book_report_backend_property_changed (
+ book, prop_name, prop_value);
+ g_free (prop_value);
+
+ prop_name = BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS;
+ prop_value = e_book_backend_get_backend_property (backend, prop_name);
+ e_data_book_report_backend_property_changed (
+ book, prop_name, prop_value);
+ g_free (prop_value);
+
+ prop_name = BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS;
+ prop_value = e_book_backend_get_backend_property (backend, prop_name);
+ e_data_book_report_backend_property_changed (
+ book, prop_name, prop_value);
+ g_free (prop_value);
+
+ /* Initialize the locale to the value reported by setlocale() until
+ * systemd says otherwise.
+ */
+ e_dbus_address_book_set_locale (
+ book->priv->dbus_interface,
+ setlocale (LC_COLLATE, NULL));
+
+ g_object_unref (backend);
+}
+
+static gboolean
+data_book_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookBackend *backend;
+ EDataBook *book;
+ gchar *locale;
+
+ book = E_DATA_BOOK (initable);
+
+ /* XXX If we're serving a direct access backend only for the
+ * purpose of catching "respond" calls, skip this stuff. */
+ if (book->priv->connection == NULL)
+ return TRUE;
+ if (book->priv->object_path == NULL)
+ return TRUE;
+
+ /* This will be NULL for a backend that
+ * does not support direct read access. */
+ backend = e_data_book_ref_backend (book);
+ book->priv->direct_book = e_book_backend_get_direct_book (backend);
+ g_object_unref (backend);
+
+ if (book->priv->direct_book != NULL) {
+ gboolean success;
+
+ success = e_data_book_direct_register_gdbus_object (
+ book->priv->direct_book,
+ book->priv->connection,
+ book->priv->object_path,
+ error);
+
+ if (!success)
+ return FALSE;
+ }
+
+ /* Fetch backend configured locale and set that as the initial
+ * value on the dbus object
+ */
+ locale = e_book_backend_dup_locale (backend);
+ e_dbus_address_book_set_locale (book->priv->dbus_interface, locale);
+ g_free (locale);
+
+ return g_dbus_interface_skeleton_export (
+ G_DBUS_INTERFACE_SKELETON (book->priv->dbus_interface),
+ book->priv->connection,
+ book->priv->object_path,
+ error);
+}
+
+static void
+e_data_book_class_init (EDataBookClass *class)
+{
+ GObjectClass *object_class;
+
+ g_type_class_add_private (class, sizeof (EDataBookPrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->set_property = data_book_set_property;
+ object_class->get_property = data_book_get_property;
+ object_class->dispose = data_book_dispose;
+ object_class->finalize = data_book_finalize;
+ object_class->constructed = data_book_constructed;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_BACKEND,
+ g_param_spec_object (
+ "backend",
+ "Backend",
+ "The backend driving this connection",
+ E_TYPE_BOOK_BACKEND,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_CONNECTION,
+ g_param_spec_object (
+ "connection",
+ "Connection",
+ "The GDBusConnection on which to "
+ "export the address book interface",
+ G_TYPE_DBUS_CONNECTION,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_OBJECT_PATH,
+ g_param_spec_string (
+ "object-path",
+ "Object Path",
+ "The object path at which to "
+ "export the address book interface",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+e_data_book_initable_init (GInitableIface *iface)
+{
+ iface->init = data_book_initable_init;
+}
+
+static void
+e_data_book_init (EDataBook *data_book)
+{
+ EDBusAddressBook *dbus_interface;
+
+ data_book->priv = E_DATA_BOOK_GET_PRIVATE (data_book);
+
+ dbus_interface = e_dbus_address_book_skeleton_new ();
+ data_book->priv->dbus_interface = dbus_interface;
+
+ g_mutex_init (&data_book->priv->sender_lock);
+ g_weak_ref_init (&data_book->priv->backend, NULL);
+
+ data_book->priv->sender_table = g_hash_table_new_full (
+ (GHashFunc) g_str_hash,
+ (GEqualFunc) g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) g_ptr_array_unref);
+
+ g_signal_connect (
+ dbus_interface, "handle-retrieve-properties",
+ G_CALLBACK (data_book_handle_retrieve_properties_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-open",
+ G_CALLBACK (data_book_handle_open_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-refresh",
+ G_CALLBACK (data_book_handle_refresh_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-get-contact",
+ G_CALLBACK (data_book_handle_get_contact_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-get-contact-list",
+ G_CALLBACK (data_book_handle_get_contact_list_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-get-contact-list-uids",
+ G_CALLBACK (data_book_handle_get_contact_list_uids_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-create-contacts",
+ G_CALLBACK (data_book_handle_create_contacts_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-remove-contacts",
+ G_CALLBACK (data_book_handle_remove_contacts_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-modify-contacts",
+ G_CALLBACK (data_book_handle_modify_contacts_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-get-view",
+ G_CALLBACK (data_book_handle_get_view_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-get-cursor",
+ G_CALLBACK (data_book_handle_get_cursor_cb),
+ data_book);
+ g_signal_connect (
+ dbus_interface, "handle-close",
+ G_CALLBACK (data_book_handle_close_cb),
+ data_book);
+}
+
+/**
+ * e_data_book_new:
+ * @backend: an #EBookBackend
+ * @connection: a #GDBusConnection
+ * @object_path: object path for the D-Bus interface
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EDataBook and exports the AddressBook D-Bus interface
+ * on @connection at @object_path. The #EDataBook handles incoming remote
+ * method invocations and forwards them to the @backend. If the AddressBook
+ * interface fails to export, the function sets @error and returns %NULL.
+ *
+ * Returns: an #EDataBook, or %NULL on error
+ **/
+EDataBook *
+e_data_book_new (EBookBackend *backend,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+ g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
+ g_return_val_if_fail (object_path != NULL, NULL);
+
+ return g_initable_new (
+ E_TYPE_DATA_BOOK, NULL, error,
+ "backend", backend,
+ "connection", connection,
+ "object-path", object_path,
+ NULL);
+}
+
+/**
+ * e_data_book_ref_backend:
+ * @book: an #EDataBook
+ *
+ * Returns the #EBookBackend to which incoming remote method invocations
+ * are being forwarded.
+ *
+ * The returned #EBookBackend is referenced for thread-safety and should
+ * be unreferenced with g_object_unref() when finished with it.
+ *
+ * Returns: an #EBookBackend
+ *
+ * Since: 3.10
+ **/
+EBookBackend *
+e_data_book_ref_backend (EDataBook *book)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK (book), NULL);
+
+ return g_weak_ref_get (&book->priv->backend);
+}
+
+/**
+ * e_data_book_get_connection:
+ * @book: an #EDataBook
+ *
+ * Returns the #GDBusConnection on which the AddressBook D-Bus interface
+ * is exported.
+ *
+ * Returns: the #GDBusConnection
+ *
+ * Since: 3.8
+ **/
+GDBusConnection *
+e_data_book_get_connection (EDataBook *book)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK (book), NULL);
+
+ return book->priv->connection;
+}
+
+/**
+ * e_data_book_get_object_path:
+ * @book: an #EDataBook
+ *
+ * Returns the object path at which the AddressBook D-Bus interface is
+ * exported.
+ *
+ * Returns: the object path
+ *
+ * Since: 3.8
+ **/
+const gchar *
+e_data_book_get_object_path (EDataBook *book)
+{
+ g_return_val_if_fail (E_IS_DATA_BOOK (book), NULL);
+
+ return book->priv->object_path;
+}
+
+/**
+ * e_data_book_set_locale:
+ * @book: an #EDataBook
+ * @locale: the new locale to set for this book
+ * @cancellable: (allow-none): a #GCancellable
+ * @error: (allow-none): a location to store any error which might occur
+ *
+ * Set's the locale for this addressbook, this can result in renormalization of
+ * locale sensitive data.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.12
+ */
+gboolean
+e_data_book_set_locale (EDataBook *book,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookBackend *backend;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_DATA_BOOK (book), FALSE);
+
+ backend = e_data_book_ref_backend (book);
+ success = e_book_backend_set_locale (
+ backend, locale, cancellable, error);
+
+ if (success) {
+ e_dbus_address_book_set_locale (
+ book->priv->dbus_interface, locale);
+ g_dbus_interface_skeleton_flush (
+ G_DBUS_INTERFACE_SKELETON (book->priv->dbus_interface));
+ }
+
+ g_object_unref (backend);
+
+ return success;
+}
diff --git a/src/addressbook/libedata-book/e-data-book.h b/src/addressbook/libedata-book/e-data-book.h
new file mode 100644
index 000000000..48e8a0bad
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book.h
@@ -0,0 +1,146 @@
+/*
+ * e-data-book.h
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_DATA_BOOK_H
+#define E_DATA_BOOK_H
+
+#include <libedataserver/libedataserver.h>
+
+/* Standard GObject macros */
+#define E_TYPE_DATA_BOOK \
+ (e_data_book_get_type ())
+#define E_DATA_BOOK(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_DATA_BOOK, EDataBook))
+#define E_DATA_BOOK_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_DATA_BOOK, EDataBookClass))
+#define E_IS_DATA_BOOK(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_DATA_BOOK))
+#define E_IS_DATA_BOOK_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_DATA_BOOK))
+#define E_DATA_BOOK_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_DATA_BOOK, EDataBookClass))
+
+G_BEGIN_DECLS
+
+struct _EBookBackend;
+
+typedef struct _EDataBook EDataBook;
+typedef struct _EDataBookClass EDataBookClass;
+typedef struct _EDataBookPrivate EDataBookPrivate;
+
+struct _EDataBook {
+ /*< private >*/
+ GObject parent;
+ EDataBookPrivate *priv;
+};
+
+struct _EDataBookClass {
+ /*< private >*/
+ GObjectClass parent_class;
+};
+
+GQuark e_data_book_error_quark (void);
+
+/**
+ * E_DATA_BOOK_ERROR:
+ *
+ * Since: 2.30
+ **/
+#define E_DATA_BOOK_ERROR e_data_book_error_quark ()
+
+GError * e_data_book_create_error (EDataBookStatus status,
+ const gchar *custom_msg);
+
+GError * e_data_book_create_error_fmt (EDataBookStatus status,
+ const gchar *custom_msg_fmt,
+ ...) G_GNUC_PRINTF (2, 3);
+
+const gchar * e_data_book_status_to_string (EDataBookStatus status);
+
+GType e_data_book_get_type (void) G_GNUC_CONST;
+EDataBook * e_data_book_new (struct _EBookBackend *backend,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error);
+struct _EBookBackend *
+ e_data_book_ref_backend (EDataBook *book);
+GDBusConnection *
+ e_data_book_get_connection (EDataBook *book);
+const gchar * e_data_book_get_object_path (EDataBook *book);
+
+gboolean e_data_book_set_locale (EDataBook *book,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error);
+void e_data_book_respond_open (EDataBook *book,
+ guint32 opid,
+ GError *error);
+void e_data_book_respond_refresh (EDataBook *book,
+ guint32 opid,
+ GError *error);
+void e_data_book_respond_create_contacts
+ (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *contacts);
+void e_data_book_respond_remove_contacts
+ (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *ids);
+void e_data_book_respond_modify_contacts
+ (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *contacts);
+void e_data_book_respond_get_contact (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const gchar *vcard);
+void e_data_book_respond_get_contact_list
+ (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *cards);
+void e_data_book_respond_get_contact_list_uids
+ (EDataBook *book,
+ guint32 opid,
+ GError *error,
+ const GSList *uids);
+
+void e_data_book_report_error (EDataBook *book,
+ const gchar *message);
+void e_data_book_report_backend_property_changed
+ (EDataBook *book,
+ const gchar *prop_name,
+ const gchar *prop_value);
+
+gchar * e_data_book_string_slist_to_comma_string
+ (const GSList *strings);
+
+G_END_DECLS
+
+#endif /* E_DATA_BOOK_H */
diff --git a/src/addressbook/libedata-book/e-subprocess-book-factory.c b/src/addressbook/libedata-book/e-subprocess-book-factory.c
new file mode 100644
index 000000000..406285fb5
--- /dev/null
+++ b/src/addressbook/libedata-book/e-subprocess-book-factory.c
@@ -0,0 +1,418 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2014 Red Hat, Inc. (www.redhat.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Fabiano FidĂȘncio <fidencio@redhat.com>
+ */
+
+/*
+ * This class handles and creates #EBackend objects from inside
+ * their own subprocesses and also serves as the layer that does
+ * the communication between #EDataBookFactory and #EBackend
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <locale.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <glib/gi18n-lib.h>
+
+#include "e-book-backend.h"
+#include "e-book-backend-factory.h"
+#include "e-data-book.h"
+#include "e-dbus-localed.h"
+#include "e-subprocess-book-factory.h"
+
+#include <e-dbus-subprocess-backend.h>
+
+#define E_SUBPROCESS_BOOK_FACTORY_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_SUBPROCESS_BOOK_FACTORY, ESubprocessBookFactoryPrivate))
+
+struct _ESubprocessBookFactoryPrivate {
+ /* Watching "org.freedesktop.locale1" for locale changes */
+ guint localed_watch_id;
+ guint subprocess_watch_id;
+ EDBusLocale1 *localed_proxy;
+ GCancellable *localed_cancel;
+ gchar *locale;
+};
+
+static GInitableIface *initable_parent_interface;
+
+/* Forward Declarations */
+static void e_subprocess_book_factory_initable_init
+ (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (
+ ESubprocessBookFactory,
+ e_subprocess_book_factory,
+ E_TYPE_SUBPROCESS_FACTORY,
+ G_IMPLEMENT_INTERFACE (
+ G_TYPE_INITABLE,
+ e_subprocess_book_factory_initable_init))
+
+static gchar *
+subprocess_book_factory_open (ESubprocessFactory *subprocess_factory,
+ EBackend *backend,
+ GDBusConnection *connection,
+ gpointer data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ESubprocessBookFactory *subprocess_book_factory = E_SUBPROCESS_BOOK_FACTORY (subprocess_factory);
+ EDataBook *data_book;
+ gchar *object_path;
+
+ /* If the backend already has an EDataBook installed, return its
+ * object path. Otherwise we need to install a new EDataBook. */
+ data_book = e_book_backend_ref_data_book (E_BOOK_BACKEND (backend));
+
+ if (data_book != NULL) {
+ object_path = g_strdup (e_data_book_get_object_path (data_book));
+ } else {
+ object_path = e_subprocess_factory_construct_path ();
+
+ /* The EDataBook will attach itself to EBookBackend,
+ * so no need to call e_book_backend_set_data_book(). */
+ data_book = e_data_book_new (
+ E_BOOK_BACKEND (backend),
+ connection, object_path, error);
+
+ if (data_book != NULL) {
+ e_subprocess_factory_set_backend_callbacks (
+ subprocess_factory, backend, data);
+
+ /* Don't set the locale on a new book if we have not
+ * yet received a notification of a locale change
+ */
+ if (subprocess_book_factory->priv->locale)
+ e_data_book_set_locale (
+ data_book,
+ subprocess_book_factory->priv->locale,
+ NULL, NULL);
+ } else {
+ g_free (object_path);
+ object_path = NULL;
+ }
+ }
+
+ g_clear_object (&data_book);
+
+ return object_path;
+}
+
+static EBackend *
+subprocess_book_factory_ref_backend (ESourceRegistry *registry,
+ ESource *source,
+ const gchar *backend_factory_type_name)
+{
+ EBookBackendFactoryClass *backend_factory_class;
+ GType backend_factory_type;
+
+ backend_factory_type = g_type_from_name (backend_factory_type_name);
+ if (!backend_factory_type)
+ return NULL;
+
+ backend_factory_class = g_type_class_ref (backend_factory_type);
+ if (!backend_factory_class)
+ return NULL;
+
+ return g_object_new (
+ backend_factory_class->backend_type,
+ "registry", registry,
+ "source", source, NULL);
+}
+
+static void
+subprocess_book_factory_dispose (GObject *object)
+{
+ ESubprocessBookFactory *subprocess_factory;
+ ESubprocessBookFactoryPrivate *priv;
+
+ subprocess_factory = E_SUBPROCESS_BOOK_FACTORY (object);
+ priv = subprocess_factory->priv;
+
+ if (priv->localed_cancel)
+ g_cancellable_cancel (priv->localed_cancel);
+
+ g_clear_object (&priv->localed_cancel);
+ g_clear_object (&priv->localed_proxy);
+
+ if (priv->localed_watch_id > 0)
+ g_bus_unwatch_name (priv->localed_watch_id);
+
+ if (priv->subprocess_watch_id > 0)
+ g_bus_unwatch_name (priv->subprocess_watch_id);
+
+ /* Chain up to parent's dispose() method. */
+ G_OBJECT_CLASS (e_subprocess_book_factory_parent_class)->dispose (object);
+}
+
+static void
+subprocess_book_factory_finalize (GObject *object)
+{
+ ESubprocessBookFactory *subprocess_factory;
+
+ subprocess_factory = E_SUBPROCESS_BOOK_FACTORY (object);
+
+ g_free (subprocess_factory->priv->locale);
+
+ /* Chain up to parent's finalize() method. */
+ G_OBJECT_CLASS (e_subprocess_book_factory_parent_class)->finalize (object);
+}
+
+static gchar *
+subprocess_book_factory_interpret_locale_value (const gchar *value)
+{
+ gchar *interpreted_value = NULL;
+ gchar **split;
+
+ split = g_strsplit (value, "=", 2);
+
+ if (split && split[0] && split[1])
+ interpreted_value = g_strdup (split[1]);
+
+ g_strfreev (split);
+
+ if (!interpreted_value)
+ g_warning ("Failed to interpret locale value: %s", value);
+
+ return interpreted_value;
+}
+
+static gchar *
+subprocess_book_factory_interpret_locale (const gchar * const * locale)
+{
+ gint i;
+ gchar *interpreted_locale = NULL;
+
+ /* Prioritize LC_COLLATE and then LANG values
+ * in the 'locale' specified by localed.
+ *
+ * If localed explicitly specifies no locale, then
+ * default to checking system locale.
+ */
+ if (locale) {
+ for (i = 0; locale[i] != NULL && interpreted_locale == NULL; i++) {
+ if (strncmp (locale[i], "LC_COLLATE", 10) == 0)
+ interpreted_locale =
+ subprocess_book_factory_interpret_locale_value (locale[i]);
+ }
+
+ for (i = 0; locale[i] != NULL && interpreted_locale == NULL; i++) {
+ if (strncmp (locale[i], "LANG", 4) == 0)
+ interpreted_locale =
+ subprocess_book_factory_interpret_locale_value (locale[i]);
+ }
+ }
+
+ if (!interpreted_locale) {
+ const gchar *system_locale = setlocale (LC_COLLATE, NULL);
+
+ interpreted_locale = g_strdup (system_locale);
+ }
+
+ return interpreted_locale;
+}
+
+static void
+subprocess_book_factory_set_locale (ESubprocessBookFactory *subprocess_factory,
+ const gchar *locale)
+{
+ ESubprocessBookFactoryPrivate *priv = subprocess_factory->priv;
+ GError *error = NULL;
+
+ if (g_strcmp0 (priv->locale, locale) != 0) {
+ GList *backends, *l;
+
+ g_free (priv->locale);
+ priv->locale = g_strdup (locale);
+
+ backends = e_subprocess_factory_get_backends_list (E_SUBPROCESS_FACTORY (subprocess_factory));
+
+ for (l = backends; l != NULL; l = g_list_next (l)) {
+ EBackend *backend = l->data;
+ EDataBook *data_book;
+
+ data_book = e_book_backend_ref_data_book (E_BOOK_BACKEND (backend));
+
+ if (!e_data_book_set_locale (data_book, locale, NULL, &error)) {
+ g_warning (
+ "Failed to set locale on addressbook: %s",
+ error->message);
+ g_clear_error (&error);
+ }
+
+ g_object_unref (data_book);
+ }
+
+ g_list_free_full (backends, g_object_unref);
+ }
+}
+
+static void
+subprocess_book_factory_locale_changed (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ EDBusLocale1 *locale_proxy = E_DBUS_LOCALE1 (object);
+ ESubprocessBookFactory *factory = (ESubprocessBookFactory *) user_data;
+ const gchar * const *locale;
+ gchar *interpreted_locale;
+
+ locale = e_dbus_locale1_get_locale (locale_proxy);
+ interpreted_locale = subprocess_book_factory_interpret_locale (locale);
+
+ subprocess_book_factory_set_locale (factory, interpreted_locale);
+
+ g_free (interpreted_locale);
+}
+
+static void
+subprocess_book_factory_localed_ready (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ ESubprocessBookFactory *subprocess_factory = (ESubprocessBookFactory *) user_data;
+ GError *error = NULL;
+
+ subprocess_factory->priv->localed_proxy = e_dbus_locale1_proxy_new_finish (res, &error);
+
+ if (subprocess_factory->priv->localed_proxy == NULL) {
+ g_warning ("Error fetching localed proxy: %s", error->message);
+ g_error_free (error);
+ }
+
+ g_clear_object (&subprocess_factory->priv->localed_cancel);
+
+ if (subprocess_factory->priv->localed_proxy) {
+ g_signal_connect (
+ subprocess_factory->priv->localed_proxy, "notify::locale",
+ G_CALLBACK (subprocess_book_factory_locale_changed), subprocess_factory);
+
+ /* Initial refresh of the locale */
+ subprocess_book_factory_locale_changed (
+ G_OBJECT (subprocess_factory->priv->localed_proxy), NULL, subprocess_factory);
+ }
+}
+
+static void
+subprocess_book_factory_localed_appeared (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ ESubprocessBookFactory *subprocess_factory = (ESubprocessBookFactory *) user_data;
+
+ subprocess_factory->priv->localed_cancel = g_cancellable_new ();
+
+ e_dbus_locale1_proxy_new (
+ connection,
+ G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES,
+ "org.freedesktop.locale1",
+ "/org/freedesktop/locale1",
+ subprocess_factory->priv->localed_cancel,
+ subprocess_book_factory_localed_ready,
+ subprocess_factory);
+}
+
+static void
+subprocess_book_factory_localed_vanished (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ ESubprocessBookFactory *subprocess_factory = (ESubprocessBookFactory *) user_data;
+
+ if (subprocess_factory->priv->localed_cancel) {
+ g_cancellable_cancel (subprocess_factory->priv->localed_cancel);
+ g_clear_object (&subprocess_factory->priv->localed_cancel);
+ }
+
+ g_clear_object (&subprocess_factory->priv->localed_proxy);
+}
+
+static void
+e_subprocess_book_factory_class_init (ESubprocessBookFactoryClass *class)
+{
+ GObjectClass *object_class;
+ ESubprocessFactoryClass *subprocess_factory_class;
+
+ g_type_class_add_private (class, sizeof (ESubprocessBookFactoryPrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->dispose = subprocess_book_factory_dispose;
+ object_class->finalize = subprocess_book_factory_finalize;
+
+ subprocess_factory_class = E_SUBPROCESS_FACTORY_CLASS (class);
+ subprocess_factory_class->ref_backend = subprocess_book_factory_ref_backend;
+ subprocess_factory_class->open_data = subprocess_book_factory_open;
+}
+
+static gboolean
+subprocess_book_factory_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ESubprocessBookFactory *subprocess_factory;
+ GBusType bus_type = G_BUS_TYPE_SYSTEM;
+
+ subprocess_factory = E_SUBPROCESS_BOOK_FACTORY (initable);
+
+ /* When running tests, we pretend to be the "org.freedesktop.locale1" service
+ * on the session bus instead of the real location on the system bus.
+ */
+ if (g_getenv ("EDS_TESTING") != NULL)
+ bus_type = G_BUS_TYPE_SESSION;
+
+ /* Watch system bus for locale change notifications */
+ subprocess_factory->priv->localed_watch_id =
+ g_bus_watch_name (
+ bus_type,
+ "org.freedesktop.locale1",
+ G_BUS_NAME_WATCHER_FLAGS_NONE,
+ subprocess_book_factory_localed_appeared,
+ subprocess_book_factory_localed_vanished,
+ initable,
+ NULL);
+
+ /* Chain up to parent interface's init() method. */
+ return initable_parent_interface->init (initable, cancellable, error);
+}
+
+static void
+e_subprocess_book_factory_initable_init (GInitableIface *iface)
+{
+ initable_parent_interface = g_type_interface_peek_parent (iface);
+
+ iface->init = subprocess_book_factory_initable_init;
+}
+
+static void
+e_subprocess_book_factory_init (ESubprocessBookFactory *subprocess_factory)
+{
+ subprocess_factory->priv = E_SUBPROCESS_BOOK_FACTORY_GET_PRIVATE (subprocess_factory);
+}
+
+ESubprocessBookFactory *
+e_subprocess_book_factory_new (GCancellable *cancellable,
+ GError **error)
+{
+ return g_initable_new (
+ E_TYPE_SUBPROCESS_BOOK_FACTORY,
+ cancellable, error, NULL);
+}
diff --git a/src/addressbook/libedata-book/e-subprocess-book-factory.h b/src/addressbook/libedata-book/e-subprocess-book-factory.h
new file mode 100644
index 000000000..b5c1ebc22
--- /dev/null
+++ b/src/addressbook/libedata-book/e-subprocess-book-factory.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2014 Red Hat, Inc. (www.redhat.com)
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_SUBPROCESS_BOOK_FACTORY_H
+#define E_SUBPROCESS_BOOK_FACTORY_H
+
+#include <libebackend/libebackend.h>
+
+/* Standard GObject macros */
+#define E_TYPE_SUBPROCESS_BOOK_FACTORY \
+ (e_subprocess_book_factory_get_type ())
+#define E_SUBPROCESS_BOOK_FACTORY(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_SUBPROCESS_BOOK_FACTORY, ESubprocessBookFactory))
+#define E_SUBPROCESS_BOOK_FACTORY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_SUBPROCESS_BOOK_FACTORY, ESubprocessBookFactoryClass))
+#define E_IS_SUBPROCESS_BOOK_FACTORY(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_SUBPROCESS_BOOK_FACTORY))
+#define E_IS_SUBPROCESS_BOOK_FACTORY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_SUBPROCESS_BOOK_FACTORY))
+#define E_SUBPROCESS_BOOK_FACTORY_GET_CLASS(cls) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_SUBPROCESS_BOOK_FACTORY, ESubprocessBookFactoryClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ESubprocessBookFactory ESubprocessBookFactory;
+typedef struct _ESubprocessBookFactoryClass ESubprocessBookFactoryClass;
+typedef struct _ESubprocessBookFactoryPrivate ESubprocessBookFactoryPrivate;
+
+struct _ESubprocessBookFactory {
+ ESubprocessFactory parent;
+ ESubprocessBookFactoryPrivate *priv;
+};
+
+struct _ESubprocessBookFactoryClass {
+ ESubprocessFactoryClass parent_class;
+};
+
+GType e_subprocess_book_factory_get_type (void) G_GNUC_CONST;
+ESubprocessBookFactory *
+ e_subprocess_book_factory_new (GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* E_SUBPROCESS_BOOK_FACTORY_H */
diff --git a/src/addressbook/libedata-book/evolution-addressbook-factory-subprocess.c b/src/addressbook/libedata-book/evolution-addressbook-factory-subprocess.c
new file mode 100644
index 000000000..00a6b85eb
--- /dev/null
+++ b/src/addressbook/libedata-book/evolution-addressbook-factory-subprocess.c
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2014 Red Hat, Inc. (www.redhat.com)
+ *
+ * This program 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.
+ *
+ * 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 Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <locale.h>
+#include <stdlib.h>
+#include <glib/gi18n.h>
+
+#if defined (ENABLE_MAINTAINER_MODE) && defined (HAVE_GTK)
+#include <gtk/gtk.h>
+#endif
+
+#include <e-dbus-subprocess-backend.h>
+#include <libebackend/libebackend.h>
+#include <libedataserver/libedataserver.h>
+#include <libedata-book/libedata-book.h>
+
+typedef struct _SubprocessData SubprocessData;
+
+struct _SubprocessData {
+ GMainLoop *loop;
+ GDBusObjectManagerServer *manager;
+ ESubprocessBookFactory *subprocess_book_factory;
+};
+
+static const gchar *factory_name = NULL;
+static const gchar *bus_name = NULL;
+static const gchar *path = NULL;
+
+static GOptionEntry entries[] = {
+ { "factory", 'f', 0, G_OPTION_ARG_STRING, &factory_name, "Just for easier debugging", NULL },
+ { "bus-name", 'b', 0, G_OPTION_ARG_STRING, &bus_name, NULL, NULL },
+ { "own-path", 'p', 0, G_OPTION_ARG_STRING, &path, NULL, NULL },
+ { NULL }
+};
+
+static void
+prepare_shutdown_and_quit (ESubprocessBookFactory *subprocess_book_factory,
+ SubprocessData *sd)
+{
+ e_subprocess_factory_call_backends_prepare_shutdown (E_SUBPROCESS_FACTORY (subprocess_book_factory));
+
+ if (sd->loop) {
+ g_main_loop_quit (sd->loop);
+ sd->loop = NULL;
+ }
+}
+
+static gboolean
+subprocess_backend_handle_create_cb (EDBusSubprocessBackend *proxy,
+ GDBusMethodInvocation *invocation,
+ const gchar *uid,
+ const gchar *backend_factory_type_name,
+ const gchar *module_filename,
+ ESubprocessBookFactory *subprocess_book_factory)
+{
+ gchar *object_path = NULL;
+ GDBusConnection *connection;
+ GError *error = NULL;
+
+ connection = g_dbus_method_invocation_get_connection (invocation);
+
+ object_path = e_subprocess_factory_open_backend (
+ E_SUBPROCESS_FACTORY (subprocess_book_factory),
+ connection,
+ uid,
+ backend_factory_type_name,
+ module_filename,
+ G_DBUS_INTERFACE_SKELETON (proxy),
+ NULL,
+ &error);
+
+ if (object_path != NULL) {
+ e_dbus_subprocess_backend_complete_create (proxy, invocation, object_path);
+ g_free (object_path);
+ } else {
+ g_dbus_method_invocation_take_error (invocation, error);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+subprocess_backend_handle_close_cb (EDBusSubprocessBackend *proxy,
+ GDBusMethodInvocation *invocation,
+ SubprocessData *sd)
+{
+ prepare_shutdown_and_quit (sd->subprocess_book_factory, sd);
+
+ return TRUE;
+}
+
+static void
+on_bus_acquired (GDBusConnection *connection,
+ const gchar *name,
+ SubprocessData *sd)
+{
+ EDBusSubprocessBackend *proxy;
+ EDBusSubprocessObjectSkeleton *object;
+
+ object = e_dbus_subprocess_object_skeleton_new (path);
+
+ proxy = e_dbus_subprocess_backend_skeleton_new ();
+ e_dbus_subprocess_object_skeleton_set_backend (object, proxy);
+
+ g_signal_connect (
+ proxy, "handle-create",
+ G_CALLBACK (subprocess_backend_handle_create_cb),
+ sd->subprocess_book_factory);
+
+ g_signal_connect (
+ proxy, "handle-close",
+ G_CALLBACK (subprocess_backend_handle_close_cb),
+ sd);
+
+ g_dbus_object_manager_server_export (sd->manager, G_DBUS_OBJECT_SKELETON (object));
+ g_object_unref (proxy);
+ g_object_unref (object);
+
+ g_dbus_object_manager_server_set_connection (sd->manager, connection);
+}
+
+static void
+vanished_cb (GDBusConnection *connection,
+ const gchar *name,
+ SubprocessData *sd)
+{
+ prepare_shutdown_and_quit (sd->subprocess_book_factory, sd);
+}
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ guint id;
+ guint watched_id;
+ ESubprocessBookFactory *subprocess_book_factory;
+ GMainLoop *loop;
+ GDBusObjectManagerServer *manager;
+ GOptionContext *context;
+ SubprocessData sd;
+ GError *error = NULL;
+
+#ifdef G_OS_WIN32
+ e_util_win32_initialize ();
+#endif
+
+ setlocale (LC_ALL, "");
+ bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+ /* Workaround https://bugzilla.gnome.org/show_bug.cgi?id=674885 */
+ g_type_ensure (G_TYPE_DBUS_CONNECTION);
+
+#if defined (ENABLE_MAINTAINER_MODE) && defined (HAVE_GTK)
+ if (g_getenv ("EDS_TESTING") == NULL)
+ /* This is only to load gtk-modules, like
+ * bug-buddy's gnomesegvhandler, if possible */
+ gtk_init_check (&argc, &argv);
+#endif
+
+ context = g_option_context_new (NULL);
+ g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE);
+ g_option_context_parse (context, &argc, &argv, &error);
+ g_option_context_free (context);
+
+ if (error != NULL) {
+ g_printerr ("%s\n", error->message);
+ exit (EXIT_FAILURE);
+ }
+
+ loop = g_main_loop_new (NULL, FALSE);
+
+ manager = g_dbus_object_manager_server_new ("/org/gnome/evolution/dataserver/Subprocess/Backend");
+
+ subprocess_book_factory = e_subprocess_book_factory_new (NULL, NULL);
+
+ sd.loop = loop;
+ sd.manager = manager;
+ sd.subprocess_book_factory = subprocess_book_factory;
+
+ /* Watch the factory name and close the subprocess if the factory dies/crashes */
+ watched_id = g_bus_watch_name (
+ G_BUS_TYPE_SESSION,
+ ADDRESS_BOOK_DBUS_SERVICE_NAME,
+ G_BUS_NAME_WATCHER_FLAGS_NONE,
+ NULL,
+ (GBusNameVanishedCallback) vanished_cb,
+ &sd,
+ NULL);
+
+ id = g_bus_own_name (
+ G_BUS_TYPE_SESSION,
+ bus_name,
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ (GBusAcquiredCallback) on_bus_acquired,
+ NULL,
+ NULL,
+ &sd,
+ NULL);
+
+ g_main_loop_run (loop);
+
+ g_bus_unown_name (id);
+ g_bus_unwatch_name (watched_id);
+
+ g_clear_object (&subprocess_book_factory);
+ g_clear_object (&manager);
+ g_main_loop_unref (loop);
+
+ return 0;
+}
diff --git a/src/addressbook/libedata-book/evolutionperson.schema b/src/addressbook/libedata-book/evolutionperson.schema
new file mode 100644
index 000000000..18fd052a1
--- /dev/null
+++ b/src/addressbook/libedata-book/evolutionperson.schema
@@ -0,0 +1,212 @@
+#
+# Depends upon
+# Definition of an X.500 Attribute Type and an Object Class to Hold
+# Uniform Resource Identifiers (URIs) [RFC2079]
+# (core.schema)
+#
+# A Summary of the X.500(96) User Schema for use with LDAPv3 [RFC2256]
+# (core.schema)
+#
+# The COSINE and Internet X.500 Schema [RFC1274] (cosine.schema)
+#
+# The Internet Organizational Person Schema (inetorgperson)
+#
+# OIDs are broken up into the following:
+# 1.3.6.1.4.1.8506.1.?
+# .1 Syntaxes
+# .2 Attributes
+# .3 Objectclasses
+
+# primaryPhone
+attributetype ( 1.3.6.1.4.1.8506.1.2.1
+ NAME 'primaryPhone'
+ DESC 'preferred phone number used to contact a person'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE )
+
+# carPhone
+attributetype ( 1.3.6.1.4.1.8506.1.2.2
+ NAME 'carPhone'
+ DESC 'car phone telephone number of the person'
+ EQUALITY telephoneNumberMatch
+ SUBSTR telephoneNumberSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.50
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.3
+ NAME ( 'homeFacsimileTelephoneNumber' 'homeFax' )
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.4
+ NAME 'otherPhone'
+ EQUALITY telephoneNumberMatch
+ SUBSTR telephoneNumberSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.5
+ NAME 'businessRole'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.6
+ NAME 'managerName'
+ SUP name )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.7
+ NAME 'assistantName'
+ SUP name )
+
+# spouseName
+# single valued (/me smirks)
+attributetype ( 1.3.6.1.4.1.8506.1.2.8
+ NAME 'spouseName'
+ SUP name
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.9
+ NAME 'otherPostalAddress'
+ EQUALITY caseIgnoreListMatch
+ SUBSTR caseIgnoreListSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.10
+ NAME ( 'mailer' 'mua' )
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32} )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.11
+ NAME ( 'birthDate' 'dob' )
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.12
+ NAME 'anniversary'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.13
+ NAME 'note'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.14
+ NAME 'evolutionArbitrary'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{4096} )
+ )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.15
+ NAME 'fileAs'
+ SUP name )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.16
+ NAME 'assistantPhone'
+ EQUALITY telephoneNumberMatch
+ SUBSTR telephoneNumberSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.17
+ NAME 'companyPhone'
+ EQUALITY telephoneNumberMatch
+ SUBSTR telephoneNumberSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.18
+ NAME 'callbackPhone'
+ EQUALITY telephoneNumberMatch
+ SUBSTR telephoneNumberSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.19
+ NAME ( 'otherFacsimileTelephoneNumber' 'otherFax' )
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.20
+ NAME 'radio'
+ EQUALITY telephoneNumberMatch
+ SUBSTR telephoneNumberSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.21
+ NAME 'telex'
+ EQUALITY telephoneNumberMatch
+ SUBSTR telephoneNumberSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.22
+ NAME 'tty'
+ EQUALITY telephoneNumberMatch
+ SUBSTR telephoneNumberSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )
+
+# deprecated - use the multivalued category
+attributetype ( 1.3.6.1.4.1.8506.1.2.23
+ NAME 'categories'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{4096} )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.24
+ NAME 'contact'
+ EQUALITY distinguishedNameMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.25
+ NAME 'listName'
+ SUP name
+ SINGLE-VALUE )
+
+# deprecated - use calEntry and its attributes from RFC 2739
+attributetype ( 1.3.6.1.4.1.8506.1.2.26
+ NAME 'calendarURI'
+ EQUALITY caseExactIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE )
+
+# deprecated - use calEntry and its attributes from RFC 2739
+attributetype ( 1.3.6.1.4.1.8506.1.2.27
+ NAME 'freeBusyURI'
+ EQUALITY caseExactIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.8506.1.2.28
+ NAME 'category'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{4096} )
+
+
+# evolutionPerson
+objectclass ( 1.3.6.1.4.1.8506.1.3.1
+ NAME 'evolutionPerson'
+ DESC 'Objectclass geared to Evolution Usage'
+ SUP top
+ AUXILIARY
+ MAY (
+ fileAs $ primaryPhone $ carPhone $ homeFacsimileTelephoneNumber $
+ otherPhone $ businessRole $ managerName $ assistantName $ assistantPhone $
+ otherPostalAddress $ mailer $ birthDate $ anniversary $ spouseName $
+ note $ companyPhone $ callbackPhone $ otherFacsimileTelephoneNumber $
+ radio $ telex $ tty $ categories $ category $ calendarURI $ freeBusyURI )
+ )
+
+# evolutionPersonList
+objectclass ( 1.3.6.1.4.1.8506.1.3.2
+ NAME 'evolutionPersonList'
+ DESC 'Objectclass geared to Evolution Contact Lists'
+ SUP top
+ AUXILIARY
+ MUST (
+ listName )
+ MAY (
+ mail $ contact )
+ )
diff --git a/src/addressbook/libedata-book/libedata-book.h b/src/addressbook/libedata-book/libedata-book.h
new file mode 100644
index 000000000..ff070ecfe
--- /dev/null
+++ b/src/addressbook/libedata-book/libedata-book.h
@@ -0,0 +1,44 @@
+/*
+ * libedata-book.h
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef LIBEDATA_BOOK_H
+#define LIBEDATA_BOOK_H
+
+#define __LIBEDATA_BOOK_H_INSIDE__
+
+#include <libebook-contacts/libebook-contacts.h>
+#include <libebackend/libebackend.h>
+
+#include <libedata-book/e-book-backend-cache.h>
+#include <libedata-book/e-book-backend-factory.h>
+#include <libedata-book/e-book-backend-sexp.h>
+#include <libedata-book/e-book-backend-sqlitedb.h>
+#include <libedata-book/e-book-backend-summary.h>
+#include <libedata-book/e-book-backend.h>
+#include <libedata-book/e-book-sqlite.h>
+#include <libedata-book/e-data-book-cursor.h>
+#include <libedata-book/e-data-book-cursor-sqlite.h>
+#include <libedata-book/e-data-book-direct.h>
+#include <libedata-book/e-data-book-factory.h>
+#include <libedata-book/e-data-book-view.h>
+#include <libedata-book/e-data-book.h>
+#include <libedata-book/e-subprocess-book-factory.h>
+
+#undef __LIBEDATA_BOOK_H_INSIDE__
+
+#endif /* LIBEDATA_BOOK_H */
+
diff --git a/src/addressbook/libedata-book/libedata-book.pc.in b/src/addressbook/libedata-book/libedata-book.pc.in
new file mode 100644
index 000000000..606edcf55
--- /dev/null
+++ b/src/addressbook/libedata-book/libedata-book.pc.in
@@ -0,0 +1,18 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+libdir=@LIB_INSTALL_DIR@
+libexecdir=@LIBEXEC_INSTALL_DIR@
+includedir=@INCLUDE_INSTALL_DIR@
+datarootdir=@SHARE_INSTALL_DIR@
+datadir=@SHARE_INSTALL_DIR@
+
+privlibdir=@privlibdir@
+privincludedir=@privincludedir@
+
+backenddir=@ebook_backenddir@
+
+Name: libedatabook
+Description: Backend library for evolution address books
+Version: @PROJECT_VERSION@
+Requires: libebackend-@API_VERSION@ libebook-contacts-@API_VERSION@
+Libs: -L${libdir} -ledata-book-@API_VERSION@
+Cflags: -I${privincludedir}
diff --git a/src/addressbook/libedata-book/ximian-vcard.h b/src/addressbook/libedata-book/ximian-vcard.h
new file mode 100644
index 000000000..782d37b1c
--- /dev/null
+++ b/src/addressbook/libedata-book/ximian-vcard.h
@@ -0,0 +1,80 @@
+#define XIMIAN_VCARD \
+"BEGIN:VCARD\n" \
+"X-EVOLUTION-FILE-AS:Novell Ximian Group\n" \
+"ADR;TYPE=WORK:;Suite 500;8 Cambridge Center;Cambridge;MA;02142;USA\n" \
+"LABEL;TYPE=WORK:8 Cambridge Center, Suite 500\\nCambridge\\, MA\\n02142\\nUSA\n" \
+"TEL;WORK;VOICE:(617) 613-2000\n" \
+"TEL;WORK;FAX:(617) 613-2001\n" \
+"EMAIL;INTERNET:hello@ximian.com\n" \
+"URL:http://www.ximian.com/\n" \
+"ORG:Novell;Ximian Group\n" \
+"PHOTO;ENCODING=b;TYPE=JPEG:/9j/4AAQSkZJRgABAQEARwBHAAD//gAXQ3JlYXRlZCB3aXRo\n" \
+" IFRoZSBHSU1Q/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCM\n" \
+" cHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMj\n" \
+" IyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAbgBkAwEiAAIRAQMRAf/EA\n" \
+" BwAAAIDAQEBAQAAAAAAAAAAAAAHBQYIBAMBAv/EAEYQAAEDAwEFBgMEBgQPAAAAAAECAwQABREG\n" \
+" BxIhMWETIkFRcYEUkaEIMkLBFSNSsbLRFmJydRgkMzY3Q0RGgpKTosLh8P/EABsBAQACAwEBAAA\n" \
+" AAAAAAAAAAAAEBQIDBgEH/8QALREAAQMCAwYGAgMAAAAAAAAAAQACAwQREiFRBRMiMUFhMnGBkb\n" \
+" HRBsEUofD/2gAMAwEAAhEDEQA/AH/RRRREVwXe9W2wwFzbpNZixkc1uqwPQeZ6CoHXevLfom1ds\n" \
+" 9h6a6D8PGCsFZHMk+CR4n86yzdbrqfaZqYBSnp0hRPZMoG62ynoOSR1Pua8Lg0XPJegX5Jv6k+0\n" \
+" bBjrWxp22LlkcBIlHs0HqEjiR64peT9umupqyWrhHhpP4WI6eHureNW7Tmw+DGaTI1FJVJdxksM\n" \
+" qKG09CrmfbFMCHpCw2xATDs8JrH4gykq+Z4mqifbMUZsxpd/QUllK53M2SCb2xa+bXvf0gcV0Uw\n" \
+" 0R/DVktH2hdUwlpFxjQrg1490tLPuOH0pvv2qE4jdchx1p8lNAj91Va87OtM3RCt+2Nx3Dyci/q\n" \
+" yPYcD7g1EZ+RR4rSMI9b/S2mhdbhKsmkdtWmNTuNxnXVW2cvgGZRASo+SV8j74PSmOlQUMpORWP\n" \
+" NU7MrjY0rlQFmdDTxOE4cQOo8R1Hyqe2Z7ZJ2m32bXfHnJVpJCUuqO8uP7+Kenh4eVXkFRFUMxx\n" \
+" G4UOSN0Zs4LU1FeEOWxOityYzqHWXEhSFoOQoHkQa963rBFFFFERUdfr1E09Y5d1mr3Y8ZsrV5n\n" \
+" yA6k4A6mpGkL9ojUym0W/TrLmAsGU+AeYBwgfPJ/4RREqrrcb1tJ1oUpBXLmObqUZ7rSByT0SkZ\n" \
+" J8zk1pHQmiLXo+zpbabC3SAp55Q7zyvM9PIUudiGmURbS7fpCMvzFFton8LSTxx6qH0FM7VV9VY\n" \
+" 9MzZ7aQt5tASw3+26ohKB/zEVSVFVvZzGMw02tqe/kpbI8LMR6/C/Xxq9QagfbbP+IW1QQ4Rycf\n" \
+" xncHRAIJ/rEfsmu2a9Fgsl2XIZjtj8bqwgfM1+9L2VFksESAV9o6hG886ebjqjvLWepUSarutdn\n" \
+" MXV+obRcZks/CwCQ5DKMpeBOTxzwzgA9KwfTtfxPOSB5GQUXc9pOjoC+zXe2HV5xiOC6PmkEfWp\n" \
+" xe6tAWghSVDIIOQRXxekNOx4b0WPZYLLTram19mwlJKSMHjjNUzQd2dZM7SNxczcLOsttqVzdYz\n" \
+" 3FewI9iKpK2mjMZdFe7ed9NfT9qZDI4OAd1Vkko50ndoui22kuXq2NBOO9JZSOH9sD9/z86c8gc\n" \
+" DUJNQlaFJUkKSoYII4EVGoKp9PIHt9e6lyRNlZhcqlsJ2guQpydL3F4mO7kw1KP3Fcyj0PEjrnz\n" \
+" rSAIIyOVYfvsJ3TGqlCKpTfYuJfjLHMDOR8jw9q2Foy+o1FpWBckY/XMpUoeRxxHsciu/jeJGB7\n" \
+" eRXPvaWuLT0U/RRRWaxQeVY82x3BVw2oXbJyhgoZR0AQM/UmthK+6fSsWbRQW9pV73x/tZPtwNE\n" \
+" Wj9Nw0WuwwIKQAGI6G/cAZ+tRW0lx5nTEW4Ntqdat9xjy5CEjJLSFZP5H2qaYdCkpUk5BGQa7Ap\n" \
+" DrSm3EpWhYKVJUMgg8wRXz+kqyyTG7VXUsV22Clrfc48+CzMiPIejvIC23EHIUDXNe79b7HbXbh\n" \
+" c5SI8ZvmtZ5nyA5k9BS7d0nfdMPuSdD3JtEZaitdom5Uznx3DzT6cPWkvq/V1611fGW5nZtBCgy\n" \
+" zFbXhtCycE5JxknxPhXR07RUeB3D11H+9lAfwcxmrrqLbxcHpikWGAw1FScByUkqWvrgEBPpxqi\n" \
+" ztdXWdqmNqIIjx7gykJUphJCXAM/eBJ5g4PQCmBZNiDKWEu364uF0jJYh4AT6qUDn2FVu6bPIkT\n" \
+" aTB08xKeMOU2H99eCtKRvZGQMZ7hwceNZxVGzsbmMzIBv5dfNeOjnsCdUwbTtKsV8nJgIccZkqw\n" \
+" lJcThDqvJJz8s4zUtLVzpc2vZZKt+qBIkyUKt0V0ONKSe+7g5SCPDr9Kv0tznXP1cNMyQfxnXBC\n" \
+" tqUyuB3gslftPjJLkGWB3u82o/Ij86bf2e7iqRoxyIpWfhpC0JHQ4V/5GlVtJcBt0RPiXif+00w\n" \
+" Ps5BQtNxP4TJP8Ka6rZZJpW37/Kq68ATlPeiiirBQ0HlWR9t9qVbtpEp/dwiY0h5J8Mgbp/h+ta\n" \
+" 4pM7fdKLumn2rxGbKn4BKl4HEtn73ywD7GiL7o28JuulLbKCsqLKUL/tJ7p+oqyIe4c6RGyzU4g\n" \
+" THLNJc3WpCt9gk8A54j3GPcdaZuoosy82V23QpaYpkEIdeIJKUeIAHieXPkTXA11DuassJsCefY\n" \
+" /SvYZN5FiGZU9edRwLDAXJny2mRukoStQBWQOQHjSjg7PYE7ZmzcZb7cG6KK5CZD6txOCcJQvPg\n" \
+" QAQfAn2q6RNOWi1D9J3R5dwlR2xmZPVv9mlI8ByTj59ar09Lm0jUIQl5Y0zAUMrQSPiXfHHpyz4\n" \
+" D1qTRvMQIieQAQXOtllfIDre/X2WqVmI8Qz6D9q0bP9SO37SrSpW8ZUVXw7q+YcKeSgeRyMZ65q\n" \
+" qammvWTalEv1yjOJtaWfh25CBvBOUkHPlxUeHlyq/MiPCitxorSGWG07qG0DASK45xZlx3GJDaH\n" \
+" WljCkLGQR6VGinY2ofIG8Lri2gOi37hxYG3zC+uT2HY6ZDbyFMrAUlwK7pB5HNRcp7nxqpzdN3G\n" \
+" CFQ7NObTa3nApcaSN/suOe4SDw6VK3O4swojsp9WGmxk9fIDrW4UzWkbt2K/v691vjec8YtZUTa\n" \
+" BL+IuMaIjiWWytXQn/wBD608tgtrVC0W2+tOFSFqd9icD6AVnmFFl6n1AhoAmRPdwcfgR4n2H7q\n" \
+" 2Ppi1N2exRojaQlKEBIHkAK7Gmi3MTWaLn6iTeSF+qmaKKK3rSiuedEanQ3I7qQpC0kEEZzXRRR\n" \
+" FjnaRoSVoq/KcYQv9HOr3mHB/qzz3SenhVi0ftAbnNNwLo6G5iQEodUcJd9fJX760ZqLTkHUdsd\n" \
+" hTWEOtuJwQoVl/XGyS7aakOPwGnJcDORujK0DqPH2qJV0cdUzC/0Oi3QTuhddqY84IuFukwnFFK\n" \
+" JDSmlEcwFDGR86ISI1tgtQ4jYaYaTuoSP/udJS1azvFoAZLnbsp4dm/klPQHmKs0faVEWkfEw32\n" \
+" 1f1CFj8q56XZNSwYG5t7fSt46yB5ucimM5L4c643pXWqU5tCteMpRKUfIIH86ipmvnnAUwoQSf2\n" \
+" 3lZ+g/nWEey5yfCtrquBo8Su0+4sQ46pEp1LTSeZUfoPOlnfr67fZKQlK0QkK/VtficV5nrXOkX\n" \
+" XUk9KQHp0gnghI7qPyAp1bOdkCmH2rneQHHxxQjHdb9OvWr2j2c2Didm74VZVVplGFuQXRsc2fO\n" \
+" Qgb1cmsSXQN1JH+TT4D+dPEAAADkK848duMylppISkDGBXrVkoCKKKKIiiqrrbX9m0JARIua1re\n" \
+" dJDMdoArcI58+AA8zVLsO26RqiS9Gsukpct5lHaKbTLaSrd8wFEZ9s0RN6vGRGZktlDqAoHzFKq\n" \
+" JtomzrPOuzGjZvwEBRTJfckttpbUOae9jJ5cBk8R514Wrbo7e489+3aTlvtQGTIkqElsdm2Mkq4\n" \
+" 4zyPKiKf1Hsj09flKdXEQh4/jR3VfMUvJ/2et1ZMOe8keSgFfyqz2LbfJ1M9IZs2kJsx2O0XnEN\n" \
+" yEAhA4ZwcZ58hxr7ZdtkvUS5SbTo2fJMRsuPkPoSG0jzKsDPPhz4HyoipDewC47+FXFWOjYH51Y\n" \
+" bTsAgtrSqc88/jwWrA+QxUlYtujupZ6oNo0nLlSUtqdKEyW04SMZOVYHiKjP8ACUt5/wB3pX/XT\n" \
+" /KiJnWLQ1nsTSURorad39lIFWZKUoThIAHSlNqDbLP0siKu96MnQ0ygSyVyGzvYxnlnB4jga87F\n" \
+" ttlamXJbs2j50xcZvtXUtyEZCfPB5+gyaIm9RSetm3J68RbhJgaSmPM25vtZaviW09knjxIOM8j\n" \
+" y8q7LHtzstwv/AOhrlBftkkudkFOLS43v5xgqSeHHx5daImrRX5QtK0hSTkGiiLMP2ho8wa1iSn\n" \
+" QoxVRQ20fAKClFQ9eIqq7LLJe7vreG7ZZCoZhqD8iZjustjnnwORkY8c+Wa1ZqbStt1PBMa4MId\n" \
+" Rz7wzg+dL8bEbA1vpa7RtK+CkpdWAfXjRFB7UpCNe6Kdm6NnJft1qluKuUJlvdKznPbYH3hzPXJ\n" \
+" PMGqZsk/zc2gf3G5/Cumc3sRsTO92Rcb3uB3XVjP1r4jYfYGwoN76QsYUEurGR5HjREudhUt2BP\n" \
+" 1TMYID0eyuuoJGRvJII+oq96I2iwtVz7rb7ZZWbalyzyJ9wKUjLsrKEkjH4cE8+Jz049bew+wNb\n" \
+" 3Z76N4YO66sZHlzob2H2Bkktb6CRglLqxkeXOiJZbAv9IMj+7X/wB6ag9lGnEaj17CRJA+BhZmy\n" \
+" lK+6EI44PQq3R6E06W9h9gZVvNb6FYxlLqwcfOhvYhYWt7s99G8MK3XVjI68aIo7UxgbR9IajhQ\n" \
+" 7/Du9yiSF3S3tMNrStlkAAt94DPDI4eJFUvYfNetqNYz4xAfjWZx5skZAUnJHD1FMVrYhYWVbzW\n" \
+" +2ojGUOrBx86EbD7A0FBvfRvDCt11YyPI8aIo23zdP6i2e621TaUJiXCfa1IucFPJt5KVnfHRWS\n" \
+" euPPNZ2YadfkNsspUp1aglCU8yTyrTSNh9gbCgjfSFjCgl1YyPI8al9PbItP2WamUywkuJ5KOVE\n" \
+" emeVEVw02ZH9H4YkEqdDYCifE4oqXbaS02lCRhIGBRRF//Z\n" \
+"END:VCARD"