[glib/wip/gsettingsbackendchangeset: 3/5] Add new API GSettingsBackendChangeset



commit 64a45e2a0fa40586b2075c6db1e89999a30f9f75
Author: Allison Ryan Lortie <desrt desrt ca>
Date:   Wed Jan 13 12:22:20 2016 -0500

    Add new API GSettingsBackendChangeset
    
    This is an import of the DConfChangeset API.  It will eventually be used
    as the primary way of communicating changesets between GSettings and
    various GSettingsBackend implementations.
    
    It will also be useful in simplifying the implementation of several
    GSettingsBackend implementations contained within GLib as well as some
    new ones being authored outside of it.
    
    GSettingsBackend has always more or less represented a 'dconf-like'
    database structure, which is why DConfChangeset is a generally useful
    thing to have as part of the GSettingsBackend API.

 docs/reference/gio/gio-sections.txt |   18 +
 gio/Makefile.am                     |    1 +
 gio/gsettingsbackend.h              |   72 +++
 gio/gsettingsbackendchangeset.c     |  842 +++++++++++++++++++++++++++++++++++
 4 files changed, 933 insertions(+), 0 deletions(-)
---
diff --git a/docs/reference/gio/gio-sections.txt b/docs/reference/gio/gio-sections.txt
index 8be86c6..f6a2dd2 100644
--- a/docs/reference/gio/gio-sections.txt
+++ b/docs/reference/gio/gio-sections.txt
@@ -2508,6 +2508,24 @@ g_settings_backend_path_writable_changed
 g_settings_backend_writable_changed
 g_settings_backend_changed_tree
 g_settings_backend_flatten_tree
+GSettingsBackendChangeset
+GSettingsBackendChangesetPredicate
+g_settings_backend_changeset_all
+g_settings_backend_changeset_change
+g_settings_backend_changeset_describe
+g_settings_backend_changeset_deserialise
+g_settings_backend_changeset_diff
+g_settings_backend_changeset_get
+g_settings_backend_changeset_is_empty
+g_settings_backend_changeset_is_similar_to
+g_settings_backend_changeset_new
+g_settings_backend_changeset_new_database
+g_settings_backend_changeset_new_write
+g_settings_backend_changeset_ref
+g_settings_backend_changeset_seal
+g_settings_backend_changeset_serialise
+g_settings_backend_changeset_set
+g_settings_backend_changeset_unref
 g_keyfile_settings_backend_new
 g_memory_settings_backend_new
 g_null_settings_backend_new
diff --git a/gio/Makefile.am b/gio/Makefile.am
index 3aff3e5..98f22f4 100644
--- a/gio/Makefile.am
+++ b/gio/Makefile.am
@@ -116,6 +116,7 @@ settings_sources = \
        gnullsettingsbackend.c          \
        gsettingsbackendinternal.h      \
        gsettingsbackend.c              \
+       gsettingsbackendchangeset.c     \
        gsettingsschema.h               \
        gsettingsschema-internal.h      \
        gsettingsschema.c               \
diff --git a/gio/gsettingsbackend.h b/gio/gsettingsbackend.h
index 499b2d4..f43ff87 100644
--- a/gio/gsettingsbackend.h
+++ b/gio/gsettingsbackend.h
@@ -60,6 +60,13 @@ typedef struct _GSettingsBackendPrivate                     GSettingsBackendPriv
 typedef struct _GSettingsBackendClass                       GSettingsBackendClass;
 
 /**
+ * GSettingsBackendChangeset:
+ *
+ * A utility class for grouping changes to a #GSettingsBackend.
+ */
+typedef struct _GSettingsBackendChangeset                   GSettingsBackendChangeset;
+
+/**
  * GSettingsBackendClass:
  * @read: virtual method to read a key's value
  * @get_writable: virtual method to get if a key is writable
@@ -176,7 +183,72 @@ gboolean                g_settings_backend_is_key                       (const g
 GLIB_AVAILABLE_IN_2_48
 gboolean                g_settings_backend_is_dir                       (const gchar         *string);
 
+
+typedef gboolean             (* GSettingsBackendChangesetPredicate)             (const gchar                 
        *path,
+                                                                                 GVariant                    
        *value,
+                                                                                 gpointer                    
         user_data);
+
+GLIB_AVAILABLE_IN_2_48
+GSettingsBackendChangeset *     g_settings_backend_changeset_new                (void);
+GLIB_AVAILABLE_IN_2_48
+GSettingsBackendChangeset *     g_settings_backend_changeset_new_database       (GSettingsBackendChangeset   
        *copy_of);
+
+GLIB_AVAILABLE_IN_2_48
+GSettingsBackendChangeset *     g_settings_backend_changeset_new_write          (const gchar                 
        *path,
+                                                                                 GVariant                    
        *value);
+
+GLIB_AVAILABLE_IN_2_48
+GSettingsBackendChangeset *     g_settings_backend_changeset_ref                (GSettingsBackendChangeset   
        *changeset);
+GLIB_AVAILABLE_IN_2_48
+void                            g_settings_backend_changeset_unref              (GSettingsBackendChangeset   
        *changeset);
+
+GLIB_AVAILABLE_IN_2_48
+gboolean                        g_settings_backend_changeset_is_empty           (GSettingsBackendChangeset   
        *changeset);
+
+GLIB_AVAILABLE_IN_2_48
+void                            g_settings_backend_changeset_set                (GSettingsBackendChangeset   
        *changeset,
+                                                                                 const gchar                 
        *path,
+                                                                                 GVariant                    
        *value);
+
+GLIB_AVAILABLE_IN_2_48
+gboolean                        g_settings_backend_changeset_get                (GSettingsBackendChangeset   
        *changeset,
+                                                                                 const gchar                 
        *key,
+                                                                                 GVariant                    
       **value);
+
+GLIB_AVAILABLE_IN_2_48
+gboolean                        g_settings_backend_changeset_is_similar_to      (GSettingsBackendChangeset   
        *changeset,
+                                                                                 GSettingsBackendChangeset   
        *other);
+
+GLIB_AVAILABLE_IN_2_48
+gboolean                        g_settings_backend_changeset_all                (GSettingsBackendChangeset   
        *changeset,
+                                                                                 
GSettingsBackendChangesetPredicate   predicate,
+                                                                                 gpointer                    
         user_data);
+
+GLIB_AVAILABLE_IN_2_48
+guint                           g_settings_backend_changeset_describe           (GSettingsBackendChangeset   
        *changeset,
+                                                                                 const gchar                 
       **prefix,
+                                                                                 const gchar * const         
       **paths,
+                                                                                 GVariant * const            
       **values);
+
+GLIB_AVAILABLE_IN_2_48
+GVariant *                      g_settings_backend_changeset_serialise          (GSettingsBackendChangeset   
        *changeset);
+GLIB_AVAILABLE_IN_2_48
+GSettingsBackendChangeset *     g_settings_backend_changeset_deserialise        (GVariant                    
        *serialised);
+
+GLIB_AVAILABLE_IN_2_48
+void                            g_settings_backend_changeset_change             (GSettingsBackendChangeset   
        *changeset,
+                                                                                 GSettingsBackendChangeset   
        *changes);
+
+GLIB_AVAILABLE_IN_2_48
+GSettingsBackendChangeset *     g_settings_backend_changeset_diff               (GSettingsBackendChangeset   
        *from,
+                                                                                 GSettingsBackendChangeset   
        *to);
+
+GLIB_AVAILABLE_IN_2_48
+void                            g_settings_backend_changeset_seal               (GSettingsBackendChangeset   
        *changeset);
+
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(GSettingsBackend, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GSettingsBackendChangeset, g_settings_backend_changeset_unref)
+
 G_END_DECLS
 
 #endif /* __G_SETTINGS_BACKEND_H__ */
diff --git a/gio/gsettingsbackendchangeset.c b/gio/gsettingsbackendchangeset.c
new file mode 100644
index 0000000..1e11184
--- /dev/null
+++ b/gio/gsettingsbackendchangeset.c
@@ -0,0 +1,842 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * 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/>.
+ *
+ * Author: Allison Ryan Lortie <desrt desrt ca>
+ */
+
+#include "config.h"
+
+#include "gsettingsbackend.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+/**
+ * SECTION:changeset
+ * @title: GSettingsBackendChangeset
+ * @Short_description: A set of changes to a g_settings_backend database
+ *
+ * #GSettingsBackendChangeset represents a set of changes that can be made to a
+ * g_settings_backend database.  Currently supported operations are writing new
+ * values to keys and resetting keys and dirs.
+ *
+ * Create the changeset with g_settings_backend_changeset_new() and populate it with
+ * g_settings_backend_changeset_set().  Submit it to g_settings_backend with
+ * g_settings_backend_client_change_fast() or g_settings_backend_client_change_sync().
+ * g_settings_backend_changeset_new_write() is a convenience constructor for the
+ * common case of writing or resetting a single value.
+ **/
+
+/**
+ * GSettingsBackendChangeset:
+ *
+ * This is a reference counted opaque structure type.  It is not a
+ * #GObject.
+ *
+ * Use g_settings_backend_changeset_ref() and g_settings_backend_changeset_unref() to manipulate
+ * references.
+ **/
+
+struct _GSettingsBackendChangeset
+{
+  GHashTable *table;
+  GHashTable *dir_resets;
+  guint is_database : 1;
+  guint is_sealed : 1;
+  gint ref_count;
+
+  gchar *prefix;
+  const gchar **paths;
+  GVariant **values;
+};
+
+static void
+unref_gvariant0 (gpointer data)
+{
+  if (data)
+    g_variant_unref (data);
+}
+
+/**
+ * g_settings_backend_changeset_new:
+ *
+ * Creates a new, empty, #GSettingsBackendChangeset.
+ *
+ * Returns: the new #GSettingsBackendChangeset.
+ **/
+GSettingsBackendChangeset *
+g_settings_backend_changeset_new (void)
+{
+  GSettingsBackendChangeset *changeset;
+
+  changeset = g_slice_new0 (GSettingsBackendChangeset);
+  changeset->table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, unref_gvariant0);
+  changeset->ref_count = 1;
+
+  return changeset;
+}
+
+/**
+ * g_settings_backend_changeset_new_database:
+ * @copy_of: (allow-none): a #GSettingsBackendChangeset to copy
+ *
+ * Creates a new #GSettingsBackendChangeset in "database" mode, possibly
+ * initialising it with the values of another changeset.
+ *
+ * In a certain sense it's possible to imagine that a #GSettingsBackendChangeset
+ * could express the contents of an entire g_settings_backend database -- the
+ * contents are the database are what you would have if you applied the
+ * changeset to an empty database.  One thing that fails to map in this
+ * analogy are reset operations -- if we start with an empty database
+ * then reset operations are meaningless.
+ *
+ * A "database" mode changeset is therefore a changeset which is
+ * incapable of containing reset operations.
+ *
+ * It is not permitted to use a database-mode changeset for most
+ * operations (such as the @change argument to g_settings_backend_changeset_change()
+ * or the @changeset argument to #GSettingsBackendClient APIs).
+ *
+ * If @copy_of is non-%NULL then its contents will be copied into the
+ * created changeset.  @copy_of must be a database-mode changeset.
+ *
+ * Returns: a new #GSettingsBackendChangeset in "database" mode
+ *
+ * Since: 0.16
+ */
+GSettingsBackendChangeset *
+g_settings_backend_changeset_new_database (GSettingsBackendChangeset *copy_of)
+{
+  GSettingsBackendChangeset *changeset;
+
+  g_return_val_if_fail (copy_of == NULL || copy_of->is_database, NULL);
+
+  changeset = g_settings_backend_changeset_new ();
+  changeset->is_database = TRUE;
+
+  if (copy_of)
+    {
+      GHashTableIter iter;
+      gpointer key, value;
+
+      g_hash_table_iter_init (&iter, copy_of->table);
+      while (g_hash_table_iter_next (&iter, &key, &value))
+        g_hash_table_insert (changeset->table, g_strdup (key), g_variant_ref (value));
+    }
+
+  return changeset;
+}
+
+/**
+ * g_settings_backend_changeset_unref:
+ * @changeset: a #GSettingsBackendChangeset
+ *
+ * Releases a #GSettingsBackendChangeset reference.
+ **/
+void
+g_settings_backend_changeset_unref (GSettingsBackendChangeset *changeset)
+{
+  if (g_atomic_int_dec_and_test (&changeset->ref_count))
+    {
+      g_free (changeset->prefix);
+      g_free (changeset->paths);
+      g_free (changeset->values);
+
+      g_hash_table_unref (changeset->table);
+
+      if (changeset->dir_resets)
+        g_hash_table_unref (changeset->dir_resets);
+
+      g_slice_free (GSettingsBackendChangeset, changeset);
+    }
+}
+
+/**
+ * g_settings_backend_changeset_ref:
+ * @changeset: a #GSettingsBackendChangeset
+ *
+ * Increases the reference count on @changeset
+ *
+ * Returns: @changeset
+ **/
+GSettingsBackendChangeset *
+g_settings_backend_changeset_ref (GSettingsBackendChangeset *changeset)
+{
+  g_atomic_int_inc (&changeset->ref_count);
+
+  return changeset;
+}
+
+static void
+g_settings_backend_changeset_record_dir_reset (GSettingsBackendChangeset *changeset,
+                                               const gchar               *dir)
+{
+  g_return_if_fail (g_settings_backend_is_dir (dir));
+  g_return_if_fail (!changeset->is_database);
+  g_return_if_fail (!changeset->is_sealed);
+
+  if (!changeset->dir_resets)
+    changeset->dir_resets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+  g_hash_table_insert (changeset->table, g_strdup (dir), NULL);
+  g_hash_table_add (changeset->dir_resets, g_strdup (dir));
+}
+
+/**
+ * g_settings_backend_changeset_set:
+ * @changeset: a #GSettingsBackendChangeset
+ * @path: a path to modify
+ * @value: the value for the key, or %NULL to reset
+ *
+ * Adds an operation to modify @path to a #GSettingsBackendChangeset.
+ *
+ * @path may either be a key or a dir.  If it is a key then @value may
+ * be a #GVariant, or %NULL (to set or reset the key).
+ *
+ * If @path is a dir then this must be a reset operation: @value must be
+ * %NULL.  It is not permitted to assign a #GVariant value to a dir.
+ **/
+void
+g_settings_backend_changeset_set (GSettingsBackendChangeset *changeset,
+                                  const gchar               *path,
+                                  GVariant                  *value)
+{
+  g_return_if_fail (!changeset->is_sealed);
+  g_return_if_fail (g_settings_backend_is_path (path));
+
+  /* Check if we are performing a path reset */
+  if (g_str_has_suffix (path, "/"))
+    {
+      GHashTableIter iter;
+      gpointer key;
+
+      g_return_if_fail (value == NULL);
+
+      /* When we reset a path we must also reset all keys within that
+       * path.
+       */
+      g_hash_table_iter_init (&iter, changeset->table);
+      while (g_hash_table_iter_next (&iter, &key, NULL))
+        if (g_str_has_prefix (key, path))
+          g_hash_table_iter_remove (&iter);
+
+      /* If this is a non-database then record the reset itself. */
+      if (!changeset->is_database)
+        g_settings_backend_changeset_record_dir_reset (changeset, path);
+    }
+
+  /* ...or a value reset */
+  else if (value == NULL)
+    {
+      /* If we're a non-database, record the reset explicitly.
+       * Otherwise, just reset whatever may be there already.
+       */
+      if (!changeset->is_database)
+        g_hash_table_insert (changeset->table, g_strdup (path), NULL);
+      else
+        g_hash_table_remove (changeset->table, path);
+    }
+
+  /* ...or a normal write. */
+  else
+    g_hash_table_insert (changeset->table, g_strdup (path), g_variant_ref_sink (value));
+}
+
+/**
+ * g_settings_backend_changeset_get:
+ * @changeset: a #GSettingsBackendChangeset
+ * @key: the key to check
+ * @value: a return location for the value, or %NULL
+ *
+ * Checks if a #GSettingsBackendChangeset has an outstanding request to change
+ * the value of the given @key.
+ *
+ * If the change doesn't involve @key then %FALSE is returned and the
+ * @value is unmodified.
+ *
+ * If the change modifies @key then @value is set either to the value
+ * for that key, or %NULL in the case that the key is being reset by the
+ * request.
+ *
+ * Returns: %TRUE if the key is being modified by the change
+ */
+gboolean
+g_settings_backend_changeset_get (GSettingsBackendChangeset  *changeset,
+                                  const gchar                *key,
+                                  GVariant                  **value)
+{
+  gpointer tmp;
+
+  if (!g_hash_table_lookup_extended (changeset->table, key, NULL, &tmp))
+    {
+      /* Did not find an exact match, so check for dir resets */
+      if (changeset->dir_resets)
+        {
+          GHashTableIter iter;
+          gpointer dir;
+
+          g_hash_table_iter_init (&iter, changeset->dir_resets);
+          while (g_hash_table_iter_next (&iter, &dir, NULL))
+            if (g_str_has_prefix (key, dir))
+              {
+                if (value)
+                  *value = NULL;
+
+                return TRUE;
+              }
+        }
+
+      return FALSE;
+    }
+
+  if (value)
+    *value = tmp ? g_variant_ref (tmp) : NULL;
+
+  return TRUE;
+}
+
+/**
+ * g_settings_backend_changeset_is_similar_to:
+ * @changeset: a #GSettingsBackendChangeset
+ * @other: another #GSettingsBackendChangeset
+ *
+ * Checks if @changeset is similar to @other.
+ *
+ * Two changes are considered similar if they write to the exact same
+ * set of keys.  The values written are not considered.
+ *
+ * This check is used to prevent building up a queue of repeated writes
+ * of the same keys.  This is often seen when an application writes to a
+ * key on every move of a slider or an application window.
+ *
+ * Strictly speaking, a write resettings all of "/a/" after a write
+ * containing "/a/b" could cause the later to be removed from the queue,
+ * but this situation is difficult to detect and is expected to be
+ * extremely rare.
+ *
+ * Returns: %TRUE if the changes are similar
+ **/
+gboolean
+g_settings_backend_changeset_is_similar_to (GSettingsBackendChangeset *changeset,
+                                            GSettingsBackendChangeset *other)
+{
+  GHashTableIter iter;
+  gpointer key;
+
+  if (g_hash_table_size (changeset->table) != g_hash_table_size (other->table))
+    return FALSE;
+
+  g_hash_table_iter_init (&iter, changeset->table);
+  while (g_hash_table_iter_next (&iter, &key, NULL))
+    if (!g_hash_table_contains (other->table, key))
+      return FALSE;
+
+  return TRUE;
+}
+
+/**
+ * GSettingsBackendChangesetPredicate:
+ * @path: a path, as per g_settings_backend_is_path()
+ * @value: a #GVariant, or %NULL
+ * @user_data: user data pointer
+ *
+ * Callback function type for predicates over items in a
+ * #GSettingsBackendChangeset.
+ *
+ * Use with g_settings_backend_changeset_all().
+ *
+ * Returns: %TRUE if the predicate is met for the given @path and @value
+ **/
+
+/**
+ * g_settings_backend_changeset_all:
+ * @changeset: a #GSettingsBackendChangeset
+ * @predicate: a #GSettingsBackendChangesetPredicate
+ * @user_data: user data to pass to @predicate
+ *
+ * Checks if all changes in the changeset satisfy @predicate.
+ *
+ * @predicate is called on each item in the changeset, in turn, until it
+ * returns %FALSE.
+ *
+ * If @predicate returns %FALSE for any item, this function returns
+ * %FALSE.  If not (including the case of no items) then this function
+ * returns %TRUE.
+ *
+ * Returns: %TRUE if all items in @changeset satisfy @predicate
+ */
+gboolean
+g_settings_backend_changeset_all (GSettingsBackendChangeset          *changeset,
+                                  GSettingsBackendChangesetPredicate  predicate,
+                                  gpointer                            user_data)
+{
+  GHashTableIter iter;
+  gpointer key, value;
+
+  g_hash_table_iter_init (&iter, changeset->table);
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    if (!(* predicate) (key, value, user_data))
+      return FALSE;
+
+  return TRUE;
+}
+
+static gint
+g_settings_backend_changeset_string_ptr_compare (gconstpointer a_p,
+                                                 gconstpointer b_p)
+{
+  const gchar * const *a = a_p;
+  const gchar * const *b = b_p;
+
+  return strcmp (*a, *b);
+}
+
+/**
+ * g_settings_backend_changeset_seal:
+ * @changeset: a #GSettingsBackendChangeset
+ *
+ * Seals @changeset.
+ *
+ * When a #GSettingsBackendChangeset is first created, it is mutable and
+ * non-threadsafe.  Once the changeset is populated with the required
+ * changes, it can be shared between multiple threads, but only by
+ * making it immutable by "sealing" it.
+ *
+ * After the changeset is sealed, you cannot call g_settings_backend_changeset_set()
+ * or any other functions that would modify it.  It is safe, however, to
+ * share it between multiple threads.
+ *
+ * All changesets are unsealed on creation, including those that are
+ * made by copying changesets that are sealed.
+ * g_settings_backend_changeset_describe() will implicitly seal a changeset.
+ *
+ * This function is idempotent.
+ *
+ * Since: 0.18
+ **/
+void
+g_settings_backend_changeset_seal (GSettingsBackendChangeset *changeset)
+{
+  gsize prefix_length;
+  gint n_items;
+
+  if (changeset->is_sealed)
+    return;
+
+  changeset->is_sealed = TRUE;
+
+  /* This function used to be called g_settings_backend_changeset_build_description()
+   * because that's basically what sealing is...
+   */
+
+  n_items = g_hash_table_size (changeset->table);
+
+  /* If there are no items then what is there to describe? */
+  if (n_items == 0)
+    return;
+
+  /* We do three separate passes.  This might take a bit longer than
+   * doing it all at once but it keeps the complexity down.
+   *
+   * First, we iterate the table in order to determine the common
+   * prefix.
+   *
+   * Next, we iterate the table again to pull the strings out excluding
+   * the leading prefix.
+   *
+   * We sort the list of paths at this point because the writer
+   * requires a sorted list in order to ensure that dir resets come
+   * before writes to keys in that dir.
+   *
+   * Finally, we iterate over the sorted list and use the normal
+   * hashtable lookup in order to populate the values array in the same
+   * order.
+   *
+   * Doing it this way avoids the complication of trying to sort two
+   * arrays (keys and values) at the same time.
+   */
+
+  /* Pass 1: determine the common prefix. */
+  {
+    GHashTableIter iter;
+    const gchar *first;
+    gboolean have_one;
+    gpointer key;
+
+    g_hash_table_iter_init (&iter, changeset->table);
+
+    /* We checked above that we have at least one item. */
+    have_one = g_hash_table_iter_next (&iter, &key, NULL);
+    g_assert (have_one);
+
+    prefix_length = strlen (key);
+    first = key;
+
+    /* Consider the remaining items to find the common prefix */
+    while (g_hash_table_iter_next (&iter, &key, NULL))
+      {
+        const gchar *this = key;
+        gint i;
+
+        for (i = 0; i < prefix_length; i++)
+          if (first[i] != this[i])
+            {
+              prefix_length = i;
+              break;
+            }
+      }
+
+    /* We must surely always have a common prefix of '/' */
+    g_assert (prefix_length > 0);
+    g_assert (first[0] == '/');
+
+    /* We may find that "/a/ab" and "/a/ac" have a common prefix of
+     * "/a/a" but really we want to trim that back to "/a/".
+     *
+     * If there is only one item, leave it alone.
+     */
+    if (n_items > 1)
+      {
+        while (first[prefix_length - 1] != '/')
+          prefix_length--;
+      }
+
+    changeset->prefix = g_strndup (first, prefix_length);
+  }
+
+  /* Pass 2: collect the list of keys, dropping the prefix */
+  {
+    GHashTableIter iter;
+    gpointer key;
+    gint i = 0;
+
+    changeset->paths = g_new (const gchar *, n_items + 1);
+
+    g_hash_table_iter_init (&iter, changeset->table);
+    while (g_hash_table_iter_next (&iter, &key, NULL))
+      {
+        const gchar *path = key;
+
+        changeset->paths[i++] = path + prefix_length;
+      }
+    changeset->paths[i] = NULL;
+    g_assert (i == n_items);
+
+    /* Sort the list of keys */
+    qsort (changeset->paths, n_items, sizeof (const gchar *), 
g_settings_backend_changeset_string_ptr_compare);
+  }
+
+  /* Pass 3: collect the list of values */
+  {
+    gint i;
+
+    changeset->values = g_new (GVariant *, n_items);
+
+    for (i = 0; i < n_items; i++)
+      /* We dropped the prefix when collecting the array.
+       * Bring it back temporarily, for the lookup.
+       */
+      changeset->values[i] = g_hash_table_lookup (changeset->table, changeset->paths[i] - prefix_length);
+  }
+}
+
+/**
+ * g_settings_backend_changeset_describe:
+ * @changeset: a #GSettingsBackendChangeset
+ * @prefix: the prefix under which changes have been requested
+ * @paths: the list of paths changed, relative to @prefix
+ * @values: the list of values changed
+ *
+ * Describes @changeset.
+ *
+ * @prefix and @paths are presented in the same way as they are for the
+ * GSettingsBackend::keys-changed signal.  @values is an array of the
+ * same length as @paths.  For each key described by an element in
+ * @paths, @values will contain either a #GVariant (the requested new
+ * value of that key) or %NULL (to reset a reset).
+ *
+ * The @paths array is returned in an order such that dir will always
+ * come before keys contained within those dirs.
+ *
+ * If @changeset is not already sealed then this call will implicitly
+ * seal it.  See g_settings_backend_changeset_seal().
+ *
+ * Returns: the number of changes (the length of @changes and @values).
+ **/
+guint
+g_settings_backend_changeset_describe (GSettingsBackendChangeset  *changeset,
+                                       const gchar               **prefix,
+                                       const gchar * const       **paths,
+                                       GVariant * const          **values)
+{
+  gint n_items;
+
+  n_items = g_hash_table_size (changeset->table);
+
+  g_settings_backend_changeset_seal (changeset);
+
+  if (prefix)
+    *prefix = changeset->prefix;
+
+  if (paths)
+    *paths = changeset->paths;
+
+  if (values)
+    *values = changeset->values;
+
+  return n_items;
+}
+
+/**
+ * g_settings_backend_changeset_serialise:
+ * @changeset: a #GSettingsBackendChangeset
+ *
+ * Serialises a #GSettingsBackendChangeset.
+ *
+ * The returned value has no particular format and should only be passed
+ * to g_settings_backend_changeset_deserialise().
+ *
+ * Returns: a floating #GVariant
+ **/
+GVariant *
+g_settings_backend_changeset_serialise (GSettingsBackendChangeset *changeset)
+{
+  GVariantBuilder builder;
+  GHashTableIter iter;
+  gpointer key, value;
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{smv}"));
+
+  g_hash_table_iter_init (&iter, changeset->table);
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    g_variant_builder_add (&builder, "{smv}", key, value);
+
+  return g_variant_builder_end (&builder);
+}
+
+/**
+ * g_settings_backend_changeset_deserialise:
+ * @serialised: a #GVariant from g_settings_backend_changeset_serialise()
+ *
+ * Creates a #GSettingsBackendChangeset according to a serialised description
+ * returned from an earlier call to g_settings_backend_changeset_serialise().
+ *
+ * @serialised has no particular format -- you should only pass a value
+ * that resulted from an earlier serialise operation.
+ *
+ * This call never fails, even if @serialised is not in the correct
+ * format.  Improperly-formatted parts are simply ignored.
+ *
+ * Returns: a new #GSettingsBackendChangeset
+ **/
+GSettingsBackendChangeset *
+g_settings_backend_changeset_deserialise (GVariant *serialised)
+{
+  GSettingsBackendChangeset *changeset;
+  GVariantIter iter;
+  const gchar *key;
+  GVariant *value;
+
+  changeset = g_settings_backend_changeset_new ();
+  g_variant_iter_init (&iter, serialised);
+  while (g_variant_iter_loop (&iter, "{&smv}", &key, &value))
+    {
+      /* If value is NULL: we may be resetting a key or a dir (a path).
+       * If value is non-NULL: we must be setting a key.
+       *
+       * ie: it is not possible to set a value to a directory.
+       *
+       * If we get an invalid case, just fall through and ignore it.
+       */
+      if (g_settings_backend_is_key (key))
+        g_hash_table_insert (changeset->table, g_strdup (key), value ? g_variant_ref (value) : NULL);
+
+      else if (g_settings_backend_is_dir (key) && value == NULL)
+        g_settings_backend_changeset_record_dir_reset (changeset, key);
+    }
+
+  return changeset;
+}
+
+/**
+ * g_settings_backend_changeset_new_write:
+ * @path: a g_settings_backend path
+ * @value: a #GVariant, or %NULL
+ *
+ * Creates a new #GSettingsBackendChangeset with one change.  This is equivalent to
+ * calling g_settings_backend_changeset_new() and then g_settings_backend_changeset_set() with
+ * @path and @value.
+ *
+ * Returns: a new #GSettingsBackendChangeset
+ **/
+GSettingsBackendChangeset *
+g_settings_backend_changeset_new_write (const gchar *path,
+                                        GVariant    *value)
+{
+  GSettingsBackendChangeset *changeset;
+
+  changeset = g_settings_backend_changeset_new ();
+  g_settings_backend_changeset_set (changeset, path, value);
+
+  return changeset;
+}
+
+/**
+ * g_settings_backend_changeset_is_empty:
+ * @changeset: a #GSettingsBackendChangeset
+ *
+ * Checks if @changeset is empty (ie: contains no changes).
+ *
+ * Returns: %TRUE if @changeset is empty
+ **/
+gboolean
+g_settings_backend_changeset_is_empty (GSettingsBackendChangeset *changeset)
+{
+  return !g_hash_table_size (changeset->table);
+}
+
+/**
+ * g_settings_backend_changeset_change:
+ * @changeset: a #GSettingsBackendChangeset (to be changed)
+ * @changes: the changes to make to @changeset
+ *
+ * Applies @changes to @changeset.
+ *
+ * If @changeset is a normal changeset then reset requests in @changes
+ * will be allied to @changeset and then copied down into it.  In this
+ * case the two changesets are effectively being merged.
+ *
+ * If @changeset is in database mode then the reset operations in
+ * @changes will simply be applied to @changeset.
+ *
+ * Since: 0.16
+ **/
+void
+g_settings_backend_changeset_change (GSettingsBackendChangeset *changeset,
+                                     GSettingsBackendChangeset *changes)
+{
+  gsize prefix_len;
+  gint i;
+
+  g_return_if_fail (!changeset->is_sealed);
+
+  /* Handling resets is a little bit tricky...
+   *
+   * Consider the case that we have @changeset containing a key /a/b and
+   * @changes containing a reset request for /a/ and a set request for
+   * /a/c.
+   *
+   * It's clear that at the end of this all, we should have only /a/c
+   * but in order for that to be the case, we need to make sure that we
+   * process the reset of /a/ before we process the set of /a/c.
+   *
+   * The easiest way to do this is to visit the strings in sorted order.
+   * That removes the possibility of iterating over the hash table, but
+   * g_settings_backend_changeset_build_description() makes the list in the order we
+   * need so just call it and then iterate over the result.
+   */
+
+  if (!g_settings_backend_changeset_describe (changes, NULL, NULL, NULL))
+    return;
+
+  prefix_len = strlen (changes->prefix);
+  for (i = 0; changes->paths[i]; i++)
+    {
+      const gchar *path;
+      GVariant *value;
+
+      /* The changes->paths are just pointers into the keys of the
+       * hashtable, fast-forwarded past the prefix.  Rewind a bit.
+       */
+      path = changes->paths[i] - prefix_len;
+      value = changes->values[i];
+
+      g_settings_backend_changeset_set (changeset, path, value);
+    }
+}
+
+/**
+ * g_settings_backend_changeset_diff:
+ * @from: a database mode changeset
+ * @to: a database mode changeset
+ *
+ * Compares to database-mode changesets and produces a changeset that
+ * describes their differences.
+ *
+ * If there is no difference, %NULL is returned.
+ *
+ * Applying the returned changeset to @from using
+ * g_settings_backend_changeset_change() will result in the two changesets being
+ * equal.
+ *
+ * Returns: (transfer full): the changes, or %NULL
+ *
+ * Since: 0.16
+ */
+GSettingsBackendChangeset *
+g_settings_backend_changeset_diff (GSettingsBackendChangeset *from,
+                                   GSettingsBackendChangeset *to)
+{
+  GSettingsBackendChangeset *changeset = NULL;
+  GHashTableIter iter;
+  gpointer key, val;
+
+  g_return_val_if_fail (from->is_database, NULL);
+  g_return_val_if_fail (to->is_database, NULL);
+
+  /* We make no attempt to do dir resets, but we could...
+   *
+   * For now, we just reset each key individually.
+   *
+   * We create our list of changes in two steps:
+   *
+   *   - iterate the 'to' changeset and note any keys that do not have
+   *     the same value in the 'from' changeset
+   *
+   *   - iterate the 'from' changeset and note any keys not present in
+   *     the 'to' changeset, recording resets for them
+   *
+   * This will cover all changes.
+   *
+   * Note: because 'from' and 'to' are database changesets we don't have
+   * to worry about seeing NULL values or dirs.
+   */
+  g_hash_table_iter_init (&iter, to->table);
+  while (g_hash_table_iter_next (&iter, &key, &val))
+    {
+      GVariant *from_val = g_hash_table_lookup (from->table, key);
+
+      if (from_val == NULL || !g_variant_equal (val, from_val))
+        {
+          if (!changeset)
+            changeset = g_settings_backend_changeset_new ();
+
+          g_settings_backend_changeset_set (changeset, key, val);
+        }
+    }
+
+  g_hash_table_iter_init (&iter, from->table);
+  while (g_hash_table_iter_next (&iter, &key, &val))
+    if (!g_hash_table_lookup (to->table, key))
+      {
+        if (!changeset)
+          changeset = g_settings_backend_changeset_new ();
+
+        g_settings_backend_changeset_set (changeset, key, NULL);
+      }
+
+  return changeset;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]