summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addressbook/libebook-contacts/e-book-contacts-types.h34
-rw-r--r--addressbook/libedata-book/Makefile.am4
-rw-r--r--addressbook/libedata-book/e-data-book-cursor-sqlite.c559
-rw-r--r--addressbook/libedata-book/e-data-book-cursor-sqlite.h81
-rw-r--r--addressbook/libedata-book/e-data-book-cursor.c1209
-rw-r--r--addressbook/libedata-book/e-data-book-cursor.h314
-rw-r--r--addressbook/libedata-book/libedata-book.h4
-rw-r--r--po/POTFILES.in3
8 files changed, 2207 insertions, 1 deletions
diff --git a/addressbook/libebook-contacts/e-book-contacts-types.h b/addressbook/libebook-contacts/e-book-contacts-types.h
index 1f9d7fc05..01a6dfa04 100644
--- a/addressbook/libebook-contacts/e-book-contacts-types.h
+++ b/addressbook/libebook-contacts/e-book-contacts-types.h
@@ -149,6 +149,40 @@ typedef enum {
E_BOOK_CURSOR_SORT_DESCENDING
} EBookCursorSortType;
+/**
+ * EBookCursorOrigin:
+ * @E_BOOK_CURSOR_ORIGIN_CURRENT: The current cursor position
+ * @E_BOOK_CURSOR_ORIGIN_BEGIN: The beginning of the cursor results.
+ * @E_BOOK_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_client_cursor_step().
+ *
+ * When an #EBookClientCursor is created, the current position implied by %E_BOOK_CURSOR_ORIGIN_CURRENT
+ * is the same as %E_BOOK_CURSOR_ORIGIN_BEGIN.
+ *
+ * Since: 3.12
+ */
+typedef enum {
+ E_BOOK_CURSOR_ORIGIN_CURRENT,
+ E_BOOK_CURSOR_ORIGIN_BEGIN,
+ E_BOOK_CURSOR_ORIGIN_END
+} EBookCursorOrigin;
+
+/**
+ * EBookCursorStepFlags:
+ * @E_BOOK_CURSOR_STEP_MOVE: The cursor position should be modified while stepping
+ * @E_BOOK_CURSOR_STEP_FETCH: Traversed contacts should be listed and returned while stepping.
+ *
+ * Defines the behaviour of e_book_client_cursor_step().
+ *
+ * Since: 3.12
+ */
+typedef enum {
+ E_BOOK_CURSOR_STEP_MOVE = (1 << 0),
+ E_BOOK_CURSOR_STEP_FETCH = (1 << 1)
+} EBookCursorStepFlags;
+
GQuark e_book_client_error_quark (void) G_GNUC_CONST;
const gchar * e_book_client_error_to_string (EBookClientError code);
diff --git a/addressbook/libedata-book/Makefile.am b/addressbook/libedata-book/Makefile.am
index 2779cc0e1..880d1e632 100644
--- a/addressbook/libedata-book/Makefile.am
+++ b/addressbook/libedata-book/Makefile.am
@@ -29,6 +29,8 @@ libedata_book_1_2_la_SOURCES = \
e-book-backend-sqlitedb.c \
e-book-backend.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 \
@@ -60,6 +62,8 @@ libedata_bookinclude_HEADERS = \
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 \
diff --git a/addressbook/libedata-book/e-data-book-cursor-sqlite.c b/addressbook/libedata-book/e-data-book-cursor-sqlite.c
new file mode 100644
index 000000000..408a031a2
--- /dev/null
+++ b/addressbook/libedata-book/e-data-book-cursor-sqlite.c
@@ -0,0 +1,559 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * 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; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * 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 the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: 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 #EBookBackendSqliteDB.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <glib/gi18n.h>
+
+#include "e-data-book-cursor-sqlite.h"
+
+/* 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,
+ 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,
+ 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 {
+ EBookBackendSqliteDB *ebsdb;
+ EbSdbCursor *cursor;
+ gchar *folder_id;
+};
+
+enum {
+ PROP_0,
+ PROP_EBSDB,
+ PROP_FOLDER_ID,
+ 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_EBSDB,
+ g_param_spec_object (
+ "ebsdb", "EBookBackendSqliteDB",
+ "The EBookBackendSqliteDB to use for queries",
+ E_TYPE_BOOK_BACKEND_SQLITEDB,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_FOLDER_ID,
+ g_param_spec_string (
+ "folder-id", "Folder ID",
+ "The folder identifier to use with the EBookBackendSqliteDB object",
+ NULL,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_CURSOR,
+ g_param_spec_pointer (
+ "cursor", "Cursor",
+ "The EbSdbCursor 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 =
+ G_TYPE_INSTANCE_GET_PRIVATE (cursor,
+ E_TYPE_DATA_BOOK_CURSOR_SQLITE,
+ EDataBookCursorSqlitePrivate);
+}
+
+static void
+e_data_book_cursor_sqlite_dispose (GObject *object)
+{
+ EDataBookCursorSqlite *cursor = E_DATA_BOOK_CURSOR_SQLITE (object);
+ EDataBookCursorSqlitePrivate *priv = cursor->priv;
+
+ if (priv->ebsdb) {
+
+ if (priv->cursor)
+ e_book_backend_sqlitedb_cursor_free (priv->ebsdb,
+ priv->cursor);
+
+ g_object_unref (priv->ebsdb);
+ priv->ebsdb = 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->folder_id);
+
+ 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_EBSDB:
+ /* Construct-only, can only be set once */
+ priv->ebsdb = g_value_dup_object (value);
+ break;
+ case PROP_FOLDER_ID:
+ /* Construct-only, can only be set once */
+ priv->folder_id = 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_backend_sqlitedb_cursor_set_sexp (priv->ebsdb,
+ priv->cursor,
+ sexp,
+ &local_error);
+
+ if (!success) {
+ if (g_error_matches (local_error,
+ E_BOOK_SDB_ERROR,
+ E_BOOK_SDB_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,
+ EbSdbCursorOrigin *dest_origin,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ switch (src_origin) {
+ case E_BOOK_CURSOR_ORIGIN_CURRENT:
+ *dest_origin = EBSDB_CURSOR_ORIGIN_CURRENT;
+ break;
+ case E_BOOK_CURSOR_ORIGIN_BEGIN:
+ *dest_origin = EBSDB_CURSOR_ORIGIN_BEGIN;
+ break;
+ case E_BOOK_CURSOR_ORIGIN_END:
+ *dest_origin = EBSDB_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,
+ EbSdbCursorStepFlags *dest_flags)
+{
+ if (src_flags & E_BOOK_CURSOR_STEP_MOVE)
+ *dest_flags |= EBSDB_CURSOR_STEP_MOVE;
+
+ if (src_flags & E_BOOK_CURSOR_STEP_FETCH)
+ *dest_flags |= EBSDB_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,
+ GError **error)
+{
+ EDataBookCursorSqlite *cursor_sqlite;
+ EDataBookCursorSqlitePrivate *priv;
+ GSList *local_results = NULL, *local_converted_results = NULL, *l;
+ EbSdbCursorOrigin sqlitedb_origin = EBSDB_CURSOR_ORIGIN_CURRENT;
+ EbSdbCursorStepFlags sqlitedb_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, &sqlitedb_origin, error))
+ return FALSE;
+
+ convert_flags (flags, &sqlitedb_flags);
+
+ /* Here we check the EBookBackendSqliteDB 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_backend_sqlitedb_lock_updates (priv->ebsdb, error);
+
+ if (success && revision_guard)
+ success = e_book_backend_sqlitedb_get_revision (priv->ebsdb,
+ priv->folder_id,
+ &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_backend_sqlitedb_cursor_step (priv->ebsdb,
+ priv->cursor,
+ sqlitedb_flags,
+ sqlitedb_origin,
+ count,
+ &local_results,
+ &local_error);
+
+ if (n_results < 0) {
+
+ /* Convert the SQLite backend error to an EClient error */
+ if (g_error_matches (local_error,
+ E_BOOK_SDB_ERROR,
+ E_BOOK_SDB_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_backend_sqlitedb_unlock_updates (priv->ebsdb, TRUE, error);
+
+ } else {
+ GError *local_error = NULL;
+
+ /* Rollback transaction */
+ if (!e_book_backend_sqlitedb_unlock_updates (priv->ebsdb, FALSE, &local_error)) {
+ g_warning ("Failed to rollback transaction after failing move cursor: %s",
+ local_error->message);
+ g_clear_error (&local_error);
+ }
+ }
+
+ for (l = local_results; l; l = l->next) {
+ EbSdbSearchData *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_backend_sqlitedb_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_backend_sqlitedb_get_locale (priv->ebsdb, priv->folder_id,
+ &current_locale, error))
+ return FALSE;
+
+ /* Locale mismatch, need to report error */
+ if (g_strcmp0 (current_locale, locale) != 0) {
+ g_set_error_literal (error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_OUT_OF_SYNC,
+ _("Alphabetic index was set for incorrect locale"));
+ g_free (current_locale);
+ return FALSE;
+ }
+
+ e_book_backend_sqlitedb_cursor_set_target_alphabetic_index (priv->ebsdb,
+ priv->cursor,
+ index);
+ g_free (current_locale);
+ return TRUE;
+}
+
+static gboolean
+e_data_book_cursor_sqlite_get_position (EDataBookCursor *cursor,
+ gint *total,
+ gint *position,
+ GError **error)
+{
+ EDataBookCursorSqlite *cursor_sqlite;
+ EDataBookCursorSqlitePrivate *priv;
+
+ cursor_sqlite = E_DATA_BOOK_CURSOR_SQLITE (cursor);
+ priv = cursor_sqlite->priv;
+
+ return e_book_backend_sqlitedb_cursor_calculate (priv->ebsdb,
+ priv->cursor,
+ total, position,
+ 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_backend_sqlitedb_cursor_compare_contact (priv->ebsdb,
+ 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_backend_sqlitedb_get_locale (priv->ebsdb,
+ priv->folder_id,
+ locale,
+ error);
+}
+
+/************************************************
+ * API *
+ ************************************************/
+/**
+ * e_data_book_cursor_sqlite_new:
+ * @backend: the #EBookBackend creating this cursor
+ * @ebsdb: the #EBookBackendSqliteDB object to base this cursor on
+ * @folder_id: the folder identifier to be used in EBookBackendSqliteDB API calls
+ * @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 @ebsdb object.
+ *
+ * This is a suitable cursor type for any backend which stores its contacts
+ * using the #EBookBackendSqliteDB 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,
+ EBookBackendSqliteDB *ebsdb,
+ const gchar *folder_id,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error)
+{
+ EDataBookCursor *cursor = NULL;
+ EbSdbCursor *ebsdb_cursor;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (backend), NULL);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+ g_return_val_if_fail (folder_id && folder_id[0], NULL);
+
+ ebsdb_cursor = e_book_backend_sqlitedb_cursor_new (ebsdb, folder_id, NULL,
+ sort_fields,
+ sort_types,
+ n_fields,
+ &local_error);
+
+ if (ebsdb_cursor) {
+ cursor = g_object_new (E_TYPE_DATA_BOOK_CURSOR_SQLITE,
+ "backend", backend,
+ "ebsdb", ebsdb,
+ "folder-id", folder_id,
+ "cursor", ebsdb_cursor,
+ NULL);
+
+ /* Initially created cursors should have a position & total */
+ if (!e_data_book_cursor_load_locale (E_DATA_BOOK_CURSOR (cursor),
+ NULL, error))
+ g_clear_object (&cursor);
+
+ } else if (g_error_matches (local_error,
+ E_BOOK_SDB_ERROR,
+ E_BOOK_SDB_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/addressbook/libedata-book/e-data-book-cursor-sqlite.h b/addressbook/libedata-book/e-data-book-cursor-sqlite.h
new file mode 100644
index 000000000..88ea57701
--- /dev/null
+++ b/addressbook/libedata-book/e-data-book-cursor-sqlite.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * 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; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * 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 the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: 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-backend-sqlitedb.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,
+ EBookBackendSqliteDB *ebsdb,
+ const gchar *folder_id,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* E_DATA_BOOK_CURSOR_SQLITE_H */
diff --git a/addressbook/libedata-book/e-data-book-cursor.c b/addressbook/libedata-book/e-data-book-cursor.c
new file mode 100644
index 000000000..591e9953a
--- /dev/null
+++ b/addressbook/libedata-book/e-data-book-cursor.c
@@ -0,0 +1,1209 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * 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; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * 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 the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: 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>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#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>
+
+/* 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 gboolean data_book_cursor_compare_contact (EDataBookCursor *cursor,
+ EContact *contact,
+ gint *result,
+ 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 =
+ G_TYPE_INSTANCE_GET_PRIVATE (cursor,
+ E_TYPE_DATA_BOOK_CURSOR,
+ EDataBookCursorPrivate);
+}
+
+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, &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 gboolean
+data_book_cursor_compare_contact (EDataBookCursor *cursor,
+ EContact *contact,
+ gint *result,
+ gboolean *matches_sexp)
+{
+ if (!E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->compare_contact)
+ return FALSE;
+
+ g_object_ref (cursor);
+ *result = (* E_DATA_BOOK_CURSOR_GET_CLASS (cursor)->compare_contact) (cursor,
+ contact,
+ matches_sexp);
+ g_object_unref (cursor);
+
+ return TRUE;
+}
+
+static void
+calculate_step_position (EDataBookCursor *cursor,
+ EBookCursorOrigin origin,
+ gint count,
+ gint results)
+{
+ EDataBookCursorPrivate *priv = cursor->priv;
+ gint new_position = priv->position;
+ gint offset = results;
+
+ /* 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,
+ &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);
+ }
+
+ }
+
+ 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,
+ &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, &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
+ * @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,
+ 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, &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
+ * @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,
+ 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,
+ 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
+ * @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,
+ 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, &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
+ * @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,
+ 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,
+ 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
+ * @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,
+ 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, &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),
+ &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;
+
+ if (!data_book_cursor_compare_contact (cursor, contact, &comparison, &matches_sexp)) {
+ GError *error = NULL;
+
+ /* Comparisons not supported, must recalculate entirely */
+ if (!e_data_book_cursor_recalculate (cursor, &error)) {
+ g_warning ("Failed to recalculate the cursor value "
+ "after a contact was added: %s",
+ error->message);
+ g_clear_error (&error);
+ }
+
+ return;
+ }
+
+ /* 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;
+
+ if (!data_book_cursor_compare_contact (cursor, contact, &comparison, &matches_sexp)) {
+ GError *error = NULL;
+
+ /* Comparisons not supported, must recalculate entirely */
+ if (!e_data_book_cursor_recalculate (cursor, &error)) {
+ g_warning ("Failed to recalculate the cursor value "
+ "after a contact was added: %s",
+ error->message);
+ g_clear_error (&error);
+ }
+
+ return;
+ }
+
+ /* 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/addressbook/libedata-book/e-data-book-cursor.h b/addressbook/libedata-book/e-data-book-cursor.h
new file mode 100644
index 000000000..8d938b5a5
--- /dev/null
+++ b/addressbook/libedata-book/e-data-book-cursor.h
@@ -0,0 +1,314 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * 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; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * 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 the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: 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
+ * @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,
+ 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
+ * @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,
+ 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 should 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(). Comparing the changed contact details is usually
+ * much less of an intensive operation than calling #EDataBookCursorClass.get_position(),
+ * however if this method is left unimplemented then #EDataBookCursorClass.get_position()
+ * will be used in it's place to recalculate the cursor position and total from scratch.
+ *
+ * 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,
+ GError **error);
+gint e_data_book_cursor_step (EDataBookCursor *cursor,
+ const gchar *revision_guard,
+ EBookCursorStepFlags flags,
+ EBookCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GError **error);
+gboolean e_data_book_cursor_set_alphabetic_index (EDataBookCursor *cursor,
+ gint index,
+ const gchar *locale,
+ GError **error);
+gboolean e_data_book_cursor_recalculate (EDataBookCursor *cursor,
+ GError **error);
+gboolean e_data_book_cursor_load_locale (EDataBookCursor *cursor,
+ gchar **locale,
+ 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/addressbook/libedata-book/libedata-book.h b/addressbook/libedata-book/libedata-book.h
index 3bca3b2c7..d6f4251fa 100644
--- a/addressbook/libedata-book/libedata-book.h
+++ b/addressbook/libedata-book/libedata-book.h
@@ -31,8 +31,10 @@
#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-data-book-factory.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>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 04d20cbc9..606fea1db 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -12,12 +12,15 @@ addressbook/libebook-contacts/e-contact.c
addressbook/libebook-contacts/e-phone-number.c
addressbook/libebook/e-book.c
addressbook/libebook/e-book-client.c
+addressbook/libebook/e-book-client-cursor.c
addressbook/libebook/e-book-client-view.c
addressbook/libebook/e-destination.c
addressbook/libedata-book/e-book-backend.c
addressbook/libedata-book/e-book-backend-sexp.c
addressbook/libedata-book/e-book-backend-sqlitedb.c
addressbook/libedata-book/e-data-book.c
+addressbook/libedata-book/e-data-book-cursor.c
+addressbook/libedata-book/e-data-book-cursor-sqlite.c
addressbook/libedata-book/e-data-book-factory.c
calendar/backends/caldav/e-cal-backend-caldav.c
calendar/backends/contacts/e-cal-backend-contacts.c