diff options
author | Milan Crha <mcrha@redhat.com> | 2016-10-11 11:47:14 +0200 |
---|---|---|
committer | Milan Crha <mcrha@redhat.com> | 2016-10-11 11:47:14 +0200 |
commit | d7931c6dd9db1e090f4bb466983c3dced19e2201 (patch) | |
tree | 31e31eef195355e800c63be6b4dcfefe6e37bb84 /src/addressbook/libedata-book | |
parent | 4febe3ae82e850ca9f17229dd2dbd9cdd8708a8f (diff) | |
download | evolution-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')
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, ¤t_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" |