[libgdata] core: Add support for JSON to GDataParsable, GDataEntry and GDataFeed



commit 9771af32dac2a78f67b2ff76f9cea35677e8b99f
Author: Peteris Krisjanis <pecisk gmail com>
Date:   Mon Jul 8 18:29:17 2013 +0300

    core: Add support for JSON to GDataParsable, GDataEntry and GDataFeed
    
    This also includes initial code for detection of the Content-Type of received
    messages, and parsing JSON or XML depending on that.
    
    This breaks ABI (but not API), and adds a dependency on json-glib ≥ 0.15.
    
    Complete unit tests are included. Further work is expected for integrating
    JSON support into GDataService, ready for use with the Tasks service.
    
    This work is originally by Pēteris Krišjānis <pecisk gmail com>, with
    additions by Philip Withnall <philip tecnocode co uk>.
    
    Helps: https://bugzilla.gnome.org/show_bug.cgi?id=657539

 Makefile.am                       |    4 +-
 README                            |    1 +
 configure.ac                      |    3 +-
 docs/reference/gdata-sections.txt |    2 +
 gdata/GData-0.0.metadata          |    3 +-
 gdata/gdata-entry.c               |   98 +++++++++++++
 gdata/gdata-feed.c                |   77 ++++++++++
 gdata/gdata-parsable.c            |  292 +++++++++++++++++++++++++++++++++++++
 gdata/gdata-parsable.h            |   15 ++-
 gdata/gdata-parser.c              |  243 ++++++++++++++++++++++++++++++
 gdata/gdata-parser.h              |   10 ++
 gdata/gdata-private.h             |    8 +
 gdata/gdata-service.c             |   23 +++-
 gdata/gdata.symbols               |    2 +
 gdata/tests/common.c              |  161 ++++++++++++++++++++
 gdata/tests/common.h              |   11 ++-
 gdata/tests/general.c             |  173 ++++++++++++++++++++++-
 17 files changed, 1115 insertions(+), 11 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 8282e5b..cdf67cc 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -487,7 +487,7 @@ EXTRA_DIST += m4/introspection.m4
 
 if HAVE_INTROSPECTION
 gdata/GData- GDATA_API_VERSION_MAJOR@  GDATA_API_VERSION_MINOR@.gir: gdata/libgdata.la
-gdata_GData_ GDATA_API_VERSION_MAJOR@_ GDATA_API_VERSION_MINOR@_gir_INCLUDES = GObject-2.0 libxml2-2.0 
Soup-2.4
+gdata_GData_ GDATA_API_VERSION_MAJOR@_ GDATA_API_VERSION_MINOR@_gir_INCLUDES = GObject-2.0 libxml2-2.0 
Soup-2.4 Json-1.0
 if ENABLE_GOA
 gdata_GData_ GDATA_API_VERSION_MAJOR@_ GDATA_API_VERSION_MINOR@_gir_INCLUDES += Goa-1.0
 endif
@@ -521,7 +521,7 @@ gdata/libgdata.vapi: gdata/GData- GDATA_API_VERSION_MAJOR@  GDATA_API_VERSION_MI
 
 VAPIGEN_VAPIS = gdata/libgdata.vapi
 
-gdata_libgdata_vapi_DEPS = libxml-2.0 libsoup-2.4
+gdata_libgdata_vapi_DEPS = libxml-2.0 libsoup-2.4 json-glib-1.0
 gdata_libgdata_vapi_METADATADIRS = $(srcdir)/gdata
 gdata_libgdata_vapi_FILES = gdata/GData- GDATA_API_VERSION_MAJOR@  GDATA_API_VERSION_MINOR@.gir
 
diff --git a/README b/README
index adb667b..a30c7b9 100644
--- a/README
+++ b/README
@@ -18,6 +18,7 @@ Dependencies
  • gio-2.0 ≥ 2.17.3
  • libsoup-2.4 ≥ 2.37.91
  • liboauth ≥ 0.9.4
+ • json-glib ≥ 0.15.0
 
 If compiling with --enable-gnome (for GNOME support):
  • libsoup-gnome-2.4
diff --git a/configure.ac b/configure.ac
index 6757fd8..0d070c5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -39,6 +39,7 @@ SOUP_REQS=2.37.91
 OAUTH_REQS=0.9.4
 GTK_REQS=2.91.2
 GOA_REQS=3.2
+JSON_GLIB_REQS=0.15
 
 # Before making a release, the GDATA_LT_VERSION string should be modified. The string is of the form c:r:a. 
Follow these instructions sequentially:
 #
@@ -64,7 +65,7 @@ AC_SUBST(GDATA_API_VERSION)
 AC_SUBST(GDATA_API_VERSION_MAJOR)
 AC_SUBST(GDATA_API_VERSION_MINOR)
 
-GDATA_PACKAGES_PUBLIC="gobject-2.0 glib-2.0 >= $GLIB_REQS gio-2.0 >= $GIO_REQS libxml-2.0 libsoup-2.4 >= 
$SOUP_REQS"
+GDATA_PACKAGES_PUBLIC="gobject-2.0 glib-2.0 >= $GLIB_REQS gio-2.0 >= $GIO_REQS libxml-2.0 libsoup-2.4 >= 
$SOUP_REQS json-glib-1.0 >= $JSON_GLIB_REQS"
 GDATA_PACKAGES_PRIVATE="gthread-2.0 oauth >= $OAUTH_REQS"
 GDATA_PACKAGES="$GDATA_PACKAGES_PUBLIC $GDATA_PACKAGES_PRIVATE"
 AC_SUBST([GDATA_PACKAGES_PUBLIC])
diff --git a/docs/reference/gdata-sections.txt b/docs/reference/gdata-sections.txt
index 5c31a73..63991ac 100644
--- a/docs/reference/gdata-sections.txt
+++ b/docs/reference/gdata-sections.txt
@@ -708,6 +708,8 @@ GDataParsable
 GDataParsableClass
 gdata_parsable_new_from_xml
 gdata_parsable_get_xml
+gdata_parsable_new_from_json
+gdata_parsable_get_json
 <SUBSECTION Standard>
 gdata_parsable_get_type
 GDATA_IS_PARSABLE
diff --git a/gdata/GData-0.0.metadata b/gdata/GData-0.0.metadata
index 2995eda..9b5009a 100644
--- a/gdata/GData-0.0.metadata
+++ b/gdata/GData-0.0.metadata
@@ -1,6 +1,7 @@
 Color struct
 
 Parsable.get_xml#method skip
+Parsable.get_json#method skip
 Query.get_query_uri#method skip
 DOCUMENTS_PRESENTATION_* name="DOCUMENTS_PRESENTATION_(.+)" parent="GData.DocumentsPresentationFormat"
 DOCUMENTS_TEXT_* name="DOCUMENTS_TEXT_(.+)" parent="GData.DocumentsTextFormat"
@@ -8,4 +9,4 @@ DOCUMENTS_SPREADSHEET_* name="DOCUMENTS_SPREADSHEET_(.+)" parent="GData.Document
 CONTACTS_GENDER_* name="CONTACTS_GENDER_(.+)" parent="GData.ContactsGender"
 CONTACTS_GROUP_* name="CONTACTS_GROUP_(.+)" parent="GData.ContactsGroupType"
 CONTACTS_PRIORITY_* name="CONTACTS_PRIORITY_(.+)" parent="GData.ContactsPriority"
-CONTACTS_SENSITIVITY_* name="CONTACTS_SENSITIVITY_(.+)" parent="GData.ContactsSensitivity"
\ No newline at end of file
+CONTACTS_SENSITIVITY_* name="CONTACTS_SENSITIVITY_(.+)" parent="GData.ContactsSensitivity"
diff --git a/gdata/gdata-entry.c b/gdata/gdata-entry.c
index b46cc8e..c393980 100644
--- a/gdata/gdata-entry.c
+++ b/gdata/gdata-entry.c
@@ -33,6 +33,7 @@
 #include <glib/gi18n-lib.h>
 #include <libxml/parser.h>
 #include <string.h>
+#include <json-glib/json-glib.h>
 
 #include "gdata-entry.h"
 #include "gdata-types.h"
@@ -55,6 +56,8 @@ static void pre_get_xml (GDataParsable *parsable, GString *xml_string);
 static void get_xml (GDataParsable *parsable, GString *xml_string);
 static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
 static gchar *get_entry_uri (const gchar *id) G_GNUC_WARN_UNUSED_RESULT;
+static gboolean parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error);
+static void get_json (GDataParsable *parsable, JsonBuilder *builder);
 
 struct _GDataEntryPrivate {
        gchar *title;
@@ -112,6 +115,9 @@ gdata_entry_class_init (GDataEntryClass *klass)
        parsable_class->get_namespaces = get_namespaces;
        parsable_class->element_name = "entry";
 
+       parsable_class->parse_json = parse_json;
+       parsable_class->get_json = get_json;
+
        klass->get_entry_uri = get_entry_uri;
 
        /**
@@ -584,6 +590,98 @@ get_entry_uri (const gchar *id)
        return g_strdup (id);
 }
 
+static gboolean
+parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error)
+{
+       gboolean success;
+       GDataEntryPrivate *priv = GDATA_ENTRY (parsable)->priv;
+
+       if (gdata_parser_string_from_json_member (reader, "title", P_DEFAULT | P_NO_DUPES, &(priv->title), 
&success, error) == TRUE ||
+           gdata_parser_string_from_json_member (reader, "id", P_NON_EMPTY | P_NO_DUPES, &(priv->id), 
&success, error) == TRUE ||
+           gdata_parser_int64_time_from_json_member (reader, "updated", P_REQUIRED | P_NO_DUPES, 
&(priv->updated), &success, error) == TRUE ||
+           gdata_parser_string_from_json_member (reader, "etag", P_NON_EMPTY | P_NO_DUPES, &(priv->etag), 
&success, error) == TRUE) {
+               return success;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "selfLink") == 0) {
+               GDataLink *_link;
+               const gchar *uri;
+
+               /* Empty URI? */
+               uri = json_reader_get_string_value (reader);
+               if (uri == NULL || *uri == '\0') {
+                       return gdata_parser_error_required_json_content_missing (reader, error);
+               }
+
+               _link = gdata_link_new (uri, GDATA_LINK_SELF);
+               gdata_entry_add_link (GDATA_ENTRY (parsable), _link);
+               g_object_unref (_link);
+
+               return TRUE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "kind") == 0) {
+               GDataCategory *category;
+               const gchar *kind;
+
+               /* Empty kind? */
+               kind = json_reader_get_string_value (reader);
+               if (kind == NULL || *kind == '\0') {
+                       return gdata_parser_error_required_json_content_missing (reader, error);
+               }
+
+               category = gdata_category_new (kind, "http://schemas.google.com/g/2005#kind";, NULL);
+               gdata_entry_add_category (GDATA_ENTRY (parsable), category);
+               g_object_unref (category);
+
+               return TRUE;
+       }
+
+       return GDATA_PARSABLE_CLASS (gdata_entry_parent_class)->parse_json (parsable, reader, user_data, 
error);
+}
+
+static void
+get_json (GDataParsable *parsable, JsonBuilder *builder)
+{
+       GDataEntryPrivate *priv = GDATA_ENTRY (parsable)->priv;
+       GList *i;
+       GDataLink *_link;
+
+       json_builder_set_member_name (builder, "title");
+       json_builder_add_string_value (builder, priv->title);
+
+       if (priv->id != NULL) {
+               json_builder_set_member_name (builder, "id");
+               json_builder_add_string_value (builder, priv->id);
+       }
+
+       if (priv->updated != -1) {
+               gchar *updated = gdata_parser_int64_to_iso8601 (priv->updated);
+               json_builder_set_member_name (builder, "updated");
+               json_builder_add_string_value (builder, updated);
+               g_free (updated);
+       }
+
+       /* If we have a "kind" category, add that. */
+       for (i = priv->categories; i != NULL; i = i->next) {
+               GDataCategory *category = GDATA_CATEGORY (i->data);
+
+               if (g_strcmp0 (gdata_category_get_scheme (category), "http://schemas.google.com/g/2005#kind";) 
== 0) {
+                       json_builder_set_member_name (builder, "kind");
+                       json_builder_add_string_value (builder, gdata_category_get_term (category));
+               }
+       }
+
+       /* Add the ETag, if available. */
+       if (gdata_entry_get_etag (GDATA_ENTRY (parsable)) != NULL) {
+               json_builder_set_member_name (builder, "etag");
+               json_builder_add_string_value (builder, priv->etag);
+       }
+
+       /* Add the self-link. */
+       _link = gdata_entry_look_up_link (GDATA_ENTRY (parsable), GDATA_LINK_SELF);
+       if (_link != NULL) {
+               json_builder_set_member_name (builder, "selfLink");
+               json_builder_add_string_value (builder, gdata_link_get_uri (_link));
+       }
+}
+
 /**
  * gdata_entry_new:
  * @id: (allow-none): the entry's ID, or %NULL
diff --git a/gdata/gdata-feed.c b/gdata/gdata-feed.c
index 0e51269..b82fb21 100644
--- a/gdata/gdata-feed.c
+++ b/gdata/gdata-feed.c
@@ -36,6 +36,7 @@
 #include <glib/gi18n-lib.h>
 #include <libxml/parser.h>
 #include <string.h>
+#include <json-glib/json-glib.h>
 
 #include "gdata-feed.h"
 #include "gdata-entry.h"
@@ -57,6 +58,9 @@ static void _gdata_feed_add_category (GDataFeed *self, GDataCategory *category);
 static void _gdata_feed_add_link (GDataFeed *self, GDataLink *link);
 static void _gdata_feed_add_author (GDataFeed *self, GDataAuthor *author);
 
+static gboolean parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error);
+static gboolean post_parse_json (GDataParsable *parsable, gpointer user_data, GError **error);
+
 struct _GDataFeedPrivate {
        GList *entries;
        gchar *title;
@@ -112,6 +116,9 @@ gdata_feed_class_init (GDataFeedClass *klass)
        parsable_class->get_namespaces = get_namespaces;
        parsable_class->element_name = "feed";
 
+       parsable_class->parse_json = parse_json;
+       parsable_class->post_parse_json = post_parse_json;
+
        /**
         * GDataFeed:title:
         *
@@ -584,6 +591,57 @@ get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
                GDATA_PARSABLE_GET_CLASS (i->data)->get_namespaces (GDATA_PARSABLE (i->data), namespaces);
 }
 
+static gboolean
+parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error)
+{
+       GDataFeed *self = GDATA_FEED (parsable);
+       ParseData *data = user_data;
+
+       if (g_strcmp0 (json_reader_get_member_name (reader), "items") == 0) {
+               gint i, elements;
+
+               /* Loop through the elements array. */
+               for (i = 0, elements = json_reader_count_elements (reader); i < elements; i++) {
+                       GDataEntry *entry;
+                       GType entry_type;
+
+                       json_reader_read_element (reader, i);
+
+                       /* Allow @data to be %NULL, and assume we're parsing a vanilla feed, so that we can 
test #GDataFeed in tests/general.c.
+                        * A little hacky, but not too much so, and valuable for testing. */
+                       entry_type = (data != NULL) ? data->entry_type : GDATA_TYPE_ENTRY;
+
+                       /* Parse the node, passing it the reader cursor. */
+                       entry = GDATA_ENTRY (_gdata_parsable_new_from_json_node (entry_type, reader, NULL, 
error));
+                       if (entry == NULL)
+                               return FALSE;
+
+                       /* Calls the callbacks in the main thread */
+                       if (data != NULL)
+                               _gdata_feed_call_progress_callback (self, data, entry);
+                       _gdata_feed_add_entry (self, entry);
+                       g_object_unref (entry);
+
+                       json_reader_end_element (reader);
+               }
+       } else {
+               return GDATA_PARSABLE_CLASS (gdata_feed_parent_class)->parse_json (parsable, reader, 
user_data, error);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+post_parse_json (GDataParsable *parsable, gpointer user_data, GError **error)
+{
+       GDataFeedPrivate *priv = GDATA_FEED (parsable)->priv;
+
+       /* Reverse our lists of stuff. */
+       priv->entries = g_list_reverse (priv->entries);
+
+       return TRUE;
+}
+
 /*
  * _gdata_feed_new:
  * @title: the feed's title
@@ -632,6 +690,25 @@ _gdata_feed_new_from_xml (GType feed_type, const gchar *xml, gint length, GType
        return feed;
 }
 
+GDataFeed *
+_gdata_feed_new_from_json (GType feed_type, const gchar *json, gint length, GType entry_type,
+                          GDataQueryProgressCallback progress_callback, gpointer progress_user_data, 
gboolean is_async, GError **error)
+{
+       ParseData *data;
+       GDataFeed *feed;
+
+       g_return_val_if_fail (g_type_is_a (feed_type, GDATA_TYPE_FEED), NULL);
+       g_return_val_if_fail (json != NULL, NULL);
+       g_return_val_if_fail (g_type_is_a (entry_type, GDATA_TYPE_ENTRY), NULL);
+       g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+       data = _gdata_feed_parse_data_new (entry_type, progress_callback, progress_user_data, is_async);
+       feed = GDATA_FEED (_gdata_parsable_new_from_json (feed_type, json, length, data, error));
+       _gdata_feed_parse_data_free (data);
+
+       return feed;
+}
+
 /**
  * gdata_feed_get_entries:
  * @self: a #GDataFeed
diff --git a/gdata/gdata-parsable.c b/gdata/gdata-parsable.c
index bada35b..f9e1dfb 100644
--- a/gdata/gdata-parsable.c
+++ b/gdata/gdata-parsable.c
@@ -36,6 +36,7 @@
 #include <glib/gi18n-lib.h>
 #include <string.h>
 #include <libxml/parser.h>
+#include <json-glib/json-glib.h>
 
 #include "gdata-parsable.h"
 #include "gdata-private.h"
@@ -51,10 +52,16 @@ static void gdata_parsable_get_property (GObject *object, guint property_id, GVa
 static void gdata_parsable_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec 
*pspec);
 static void gdata_parsable_finalize (GObject *object);
 static gboolean real_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, 
GError **error);
+static gboolean real_parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError 
**error);
 
 struct _GDataParsablePrivate {
+       /* XML stuff. */
        GString *extra_xml;
        GHashTable *extra_namespaces;
+
+       /* JSON stuff. */
+       GHashTable/*<gchar*, owned JsonNode*>*/ *extra_json;
+
        gboolean constructed_from_xml;
 };
 
@@ -75,6 +82,7 @@ gdata_parsable_class_init (GDataParsableClass *klass)
        gobject_class->set_property = gdata_parsable_set_property;
        gobject_class->finalize = gdata_parsable_finalize;
        klass->parse_xml = real_parse_xml;
+       klass->parse_json = real_parse_json;
 
        /**
         * GDataParsable:constructed-from-xml:
@@ -98,6 +106,9 @@ gdata_parsable_init (GDataParsable *self)
 
        self->priv->extra_xml = g_string_new ("");
        self->priv->extra_namespaces = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+       self->priv->extra_json = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) 
json_node_free);
+
        self->priv->constructed_from_xml = FALSE;
 }
 
@@ -142,6 +153,8 @@ gdata_parsable_finalize (GObject *object)
        g_string_free (priv->extra_xml, TRUE);
        g_hash_table_destroy (priv->extra_namespaces);
 
+       g_hash_table_destroy (priv->extra_json);
+
        /* Chain up to the parent class */
        G_OBJECT_CLASS (gdata_parsable_parent_class)->finalize (object);
 }
@@ -178,6 +191,90 @@ real_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer us
        return TRUE;
 }
 
+/* Extract the member node. This would be a lot easier if JsonReader had an API to return
+ * the current node (regardless of whether it's a value, object or array). FIXME: bgo#707100. */
+static JsonNode * /* transfer full */
+_json_reader_dup_current_node (JsonReader *reader)
+{
+       JsonNode *value;
+
+       if (json_reader_is_value (reader) == TRUE) {
+               /* Value nodes are easy. Well, ignoring the complication of nulls. */
+               if (json_reader_get_null_value (reader) == TRUE) {
+                       value = json_node_new (JSON_NODE_NULL);
+               } else {
+                       value = json_node_copy (json_reader_get_value (reader));
+               }
+       } else if (json_reader_is_object (reader) == TRUE) {
+               /* Object nodes require deep copies. */
+               gint i, members;
+               JsonObject *obj;
+
+               obj = json_object_new ();
+
+               for (i = 0, members = json_reader_count_members (reader); i < members; i++) {
+                       json_reader_read_element (reader, i);
+                       json_object_set_member (obj, json_reader_get_member_name (reader), 
_json_reader_dup_current_node (reader));
+                       json_reader_end_element (reader);
+               }
+
+               value = json_node_new (JSON_NODE_OBJECT);
+               json_node_take_object (value, obj);
+       } else if (json_reader_is_array (reader) == TRUE) {
+               /* Array nodes require deep copies. */
+               gint i, elements;
+               JsonArray *arr;
+
+               arr = json_array_new ();
+
+               for (i = 0, elements = json_reader_count_elements (reader); i < elements; i++) {
+                       json_reader_read_element (reader, i);
+                       json_array_add_element (arr, _json_reader_dup_current_node (reader));
+                       json_reader_end_element (reader);
+               }
+
+               value = json_node_new (JSON_NODE_ARRAY);
+               json_node_take_array (value, arr);
+       } else {
+               /* Uh-oh. */
+               g_assert_not_reached ();
+       }
+
+       return value;
+}
+
+static gboolean
+real_parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error)
+{
+       gchar *json, *member_name;
+       JsonGenerator *generator;
+       JsonNode *value;
+
+       /* Unhandled JSON member. Save it and its value to ->extra_xml so that it's not lost if we
+        * re-upload this Parsable to the server. */
+       member_name = g_strdup (json_reader_get_member_name (reader));
+       g_assert (member_name != NULL);
+
+       /* Extract a copy of the current node. */
+       value = _json_reader_dup_current_node (reader);
+       g_assert (value != NULL);
+
+       /* Serialise the value for debugging. */
+       generator = json_generator_new ();
+       json_generator_set_root (generator, value);
+
+       json = json_generator_to_data (generator, NULL);
+       g_debug ("Unhandled JSON member ‘%s’ in %s: %s", member_name, G_OBJECT_TYPE_NAME (parsable), json);
+       g_free (json);
+
+       g_object_unref (generator);
+
+       /* Save the value. Transfer ownership of the member_name and value. */
+       g_hash_table_replace (parsable->priv->extra_json, (gpointer) member_name, (gpointer) value);
+
+       return TRUE;
+}
+
 /**
  * gdata_parsable_new_from_xml:
  * @parsable_type: the type of the class represented by the XML
@@ -320,6 +417,125 @@ _gdata_parsable_new_from_xml_node (GType parsable_type, xmlDoc *doc, xmlNode *no
        return parsable;
 }
 
+/**
+ * gdata_parsable_new_from_json:
+ * @parsable_type: the type of the class represented by the JSON
+ * @json: the JSON for just the parsable object
+ * @length: the length of @json, or -1
+ * @error: a #GError, or %NULL
+ *
+ * Creates a new #GDataParsable subclass (of the given @parsable_type) from the given @json.
+ *
+ * An object of the given @parsable_type is created, and its <function>parse_json</function> and
+ * <function>post_parse_json</function> class functions called on the JSON node obtained from @json.
+ * <function>post_parse_json</function> is called once on the root node, while 
<function>parse_json</function> is called for
+ * each of the node's members.
+ *
+ * If @length is -1, @json will be assumed to be nul-terminated.
+ *
+ * If an error occurs during parsing, a suitable error from #GDataParserError will be returned.
+ *
+ * Return value: a new #GDataParsable, or %NULL; unref with g_object_unref()
+ *
+ * Since: UNRELEASED
+ */
+GDataParsable *
+gdata_parsable_new_from_json (GType parsable_type, const gchar *json, gint length, GError **error)
+{
+       g_return_val_if_fail (g_type_is_a (parsable_type, GDATA_TYPE_PARSABLE), NULL);
+       g_return_val_if_fail (json != NULL && *json != '\0', NULL);
+       g_return_val_if_fail (length >= -1, NULL);
+       g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+       return _gdata_parsable_new_from_json (parsable_type, json, length, NULL, error);
+}
+
+GDataParsable *
+_gdata_parsable_new_from_json (GType parsable_type, const gchar *json, gint length, gpointer user_data, 
GError **error)
+{
+       JsonParser *parser;
+       JsonReader *reader;
+       GDataParsable *parsable;
+       GError *child_error = NULL;
+
+       g_return_val_if_fail (g_type_is_a (parsable_type, GDATA_TYPE_PARSABLE), NULL);
+       g_return_val_if_fail (json != NULL && *json != '\0', NULL);
+       g_return_val_if_fail (length >= -1, NULL);
+       g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+       if (length == -1)
+               length = strlen (json);
+
+       parser = json_parser_new ();
+       if (!json_parser_load_from_data (parser, json, length, &child_error)) {
+               g_set_error (error, GDATA_PARSER_ERROR, GDATA_PARSER_ERROR_PARSING_STRING,
+                            /* Translators: the parameter is an error message */
+                            _("Error parsing JSON: %s"), child_error->message);
+               g_error_free (child_error);
+               g_object_unref (parser);
+
+               return NULL;
+       }
+
+       reader = json_reader_new (json_parser_get_root (parser));
+       parsable = _gdata_parsable_new_from_json_node (parsable_type, reader, user_data, error);
+
+       g_object_unref (reader);
+       g_object_unref (parser);
+
+       return parsable;
+}
+
+GDataParsable *
+_gdata_parsable_new_from_json_node (GType parsable_type, JsonReader *reader, gpointer user_data, GError 
**error)
+{
+       GDataParsable *parsable;
+       GDataParsableClass *klass;
+       gint i;
+
+       g_return_val_if_fail (g_type_is_a (parsable_type, GDATA_TYPE_PARSABLE), NULL);
+       g_return_val_if_fail (reader != NULL, NULL);
+       g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+       /* Indicator property which allows distinguishing between locally created and server based objects
+        * as it is used for non-XML tasks, and adding another one for JSON would be a bit pointless. */
+       parsable = g_object_new (parsable_type, "constructed-from-xml", TRUE, NULL);
+
+       klass = GDATA_PARSABLE_GET_CLASS (parsable);
+       g_assert (klass->parse_json != NULL);
+
+       /* Check that the outermost node is an object. */
+       if (json_reader_is_object (reader) == FALSE) {
+               g_set_error (error, GDATA_PARSER_ERROR, GDATA_PARSER_ERROR_PARSING_STRING,
+                            /* Translators: the parameter is an error message */
+                            _("Error parsing JSON: %s"),
+                            _("Outermost JSON node is not an object."));
+               g_object_unref (parsable);
+               return NULL;
+       }
+
+       /* Parse each child member. This assumes the outermost node is an object. */
+       for (i = 0; i < json_reader_count_members (reader); i++) {
+               g_return_val_if_fail (json_reader_read_element (reader, i), NULL);
+
+               if (klass->parse_json (parsable, reader, user_data, error) == FALSE) {
+                       g_object_unref (parsable);
+                       return NULL;
+               }
+
+               json_reader_end_element (reader);
+       }
+
+       /* Call the post-parse function */
+       if (klass->post_parse_json != NULL &&
+           klass->post_parse_json (parsable, user_data, error) == FALSE) {
+               g_object_unref (parsable);
+               return NULL;
+       }
+
+       return parsable;
+}
+
 static void
 build_namespaces_cb (gchar *prefix, gchar *href, GString *output)
 {
@@ -437,6 +653,82 @@ _gdata_parsable_get_xml (GDataParsable *self, GString *xml_string, gboolean decl
                g_string_append_printf (xml_string, "</%s>", klass->element_name);
 }
 
+/**
+ * gdata_parsable_get_json:
+ * @self: a #GDataParsable
+ *
+ * Builds a JSON representation of the #GDataParsable in its current state, such that it could be inserted 
on the server. The JSON
+ * is valid for stand-alone use.
+ *
+ * Return value: the object's JSON; free with g_free()
+ *
+ * Since: UNRELEASED
+ */
+gchar *
+gdata_parsable_get_json (GDataParsable *self)
+{
+       JsonGenerator *generator;
+       JsonBuilder *builder;
+       JsonNode *root;
+       gchar *output;
+
+       g_return_val_if_fail (GDATA_IS_PARSABLE (self), NULL);
+
+       /* Build the JSON tree. */
+       builder = json_builder_new ();
+       _gdata_parsable_get_json (self, builder);
+       root = json_builder_get_root (builder);
+       g_object_unref (builder);
+
+       /* Serialise it to a string. */
+       generator = json_generator_new ();
+       json_generator_set_root (generator, root);
+       output = json_generator_to_data (generator, NULL);
+       g_object_unref (generator);
+
+       json_node_free (root);
+
+       return output;
+}
+
+/*
+ * _gdata_parsable_get_json:
+ * @self: a #GDataParsable
+ * @builder: a #JsonBuilder to build the JSON in
+ *
+ * Builds a JSON representation of the #GDataParsable in its current state, such that it could be inserted 
on the server.
+ *
+ * Since: UNRELEASED
+ */
+void
+_gdata_parsable_get_json (GDataParsable *self, JsonBuilder *builder)
+{
+       GDataParsableClass *klass;
+       GHashTableIter iter;
+       gchar *member_name;
+       JsonNode *value;
+
+       g_return_if_fail (GDATA_IS_PARSABLE (self));
+       g_return_if_fail (JSON_IS_BUILDER (builder));
+
+       klass = GDATA_PARSABLE_GET_CLASS (self);
+
+       json_builder_begin_object (builder);
+
+       /* Add the JSON. */
+       if (klass->get_json != NULL)
+               klass->get_json (self, builder);
+
+       /* Any extra JSON which we couldn't parse before? */
+       g_hash_table_iter_init (&iter, self->priv->extra_json);
+       while (g_hash_table_iter_next (&iter, (gpointer *) &member_name, (gpointer *) &value) == TRUE) {
+               json_builder_set_member_name (builder, member_name);
+               json_builder_add_value (builder, json_node_copy (value)); /* transfers ownership */
+       }
+
+       json_builder_end_object (builder);
+}
+
 /*
  * _gdata_parsable_is_constructed_from_xml:
  * @self: a #GDataParsable
diff --git a/gdata/gdata-parsable.h b/gdata/gdata-parsable.h
index ed0dc72..2f13c25 100644
--- a/gdata/gdata-parsable.h
+++ b/gdata/gdata-parsable.h
@@ -23,6 +23,7 @@
 #include <glib.h>
 #include <glib-object.h>
 #include <libxml/parser.h>
+#include <json-glib/json-glib.h>
 
 G_BEGIN_DECLS
 
@@ -73,10 +74,14 @@ typedef struct {
  * XML node to be added to @xml_string
  * @get_xml: a function to build an XML representation of the #GDataParsable in its current state, appending 
it to the provided #GString
  * @get_namespaces: a function to return a string containing the namespace declarations used by the 
@parsable when represented in XML form
+ * @parse_json: a function to parse a JSON representation of the #GDataParsable to set the properties of the 
@parsable
+ * @post_parse_json: a function called after parsing a JSON object, to allow the @parsable to validate the 
parsed properties
+ * @get_json: a function to build a JSON representation of the #GDataParsable in its current state, 
appending it to the provided #JsonBuilder
  * @element_name: the name of the XML element which represents this parsable
  * @element_namespace: the prefix of the XML namespace used for the parsable
  *
- * The class structure for the #GDataParsable class.
+ * The class structure for the #GDataParsable class. Note that JSON and XML functions are mutually exclusive:
+ * a given implementation of #GDataParsable is represented as exactly one of JSON and XML.
  *
  * Since: 0.3.0
  **/
@@ -91,6 +96,10 @@ typedef struct {
        void (*get_xml) (GDataParsable *parsable, GString *xml_string);
        void (*get_namespaces) (GDataParsable *parsable, GHashTable *namespaces);
 
+       gboolean (*parse_json) (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError 
**error);
+       gboolean (*post_parse_json) (GDataParsable *parsable, gpointer user_data, GError **error);
+       void (*get_json) (GDataParsable *parsable, JsonBuilder *builder);
+
        const gchar *element_name;
        const gchar *element_namespace;
 } GDataParsableClass;
@@ -101,6 +110,10 @@ GDataParsable *gdata_parsable_new_from_xml (GType parsable_type, const gchar *xm
                                             GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
 gchar *gdata_parsable_get_xml (GDataParsable *self) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
 
+GDataParsable *gdata_parsable_new_from_json (GType parsable_type, const gchar *json, gint length,
+                                             GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+gchar *gdata_parsable_get_json (GDataParsable *self) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+
 G_END_DECLS
 
 #endif /* !GDATA_PARSABLE_H */
diff --git a/gdata/gdata-parser.c b/gdata/gdata-parser.c
index 57388bf..1bb926b 100644
--- a/gdata/gdata-parser.c
+++ b/gdata/gdata-parser.c
@@ -23,7 +23,9 @@
 #include <glib/gi18n-lib.h>
 #include <sys/time.h>
 #include <time.h>
+#include <string.h>
 #include <libxml/parser.h>
+#include <json-glib/json-glib.h>
 
 #include "gdata-parser.h"
 #include "gdata-service.h"
@@ -259,6 +261,60 @@ gdata_parser_int64_from_iso8601 (const gchar *date, gint64 *_time)
        return FALSE;
 }
 
+gboolean
+gdata_parser_error_required_json_content_missing (JsonReader *reader, GError **error)
+{
+       const gchar *element_string = json_reader_get_member_name (reader);
+
+       /* Translators: the parameter is the name of an JSON element.
+        *
+        * For example:
+        *  A 'title' element was missing required content. */
+       g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("A \'%s\' element was 
missing required content."), element_string);
+
+       return FALSE;
+}
+
+gboolean
+gdata_parser_error_duplicate_json_element (JsonReader *reader, GError **error)
+{
+       const gchar *element_string = json_reader_get_member_name (reader);
+
+       /* Translators: the parameter is the name of an JSON element.
+        *
+        * For example:
+        *  A singleton element (title) was duplicated. */
+       g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("A singleton element 
(%s) was duplicated."), element_string);
+
+       return FALSE;
+}
+
+gboolean
+gdata_parser_error_not_iso8601_format_json (JsonReader *reader, const gchar *actual_value, GError **error)
+{
+       const gchar *element_string = json_reader_get_member_name (reader);
+
+       g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
+                    /* Translators: the first parameter is the name of an JSON element,
+                     * and the second parameter is the erroneous value (which was not in ISO 8601 format).
+                     *
+                     * For example:
+                     *  The content of a 'uploaded' element ("2009-05-06 26:30Z") was not in ISO 8601 
format. */
+                    _("The content of a \'%s\' element (\"%s\") was not in ISO 8601 format."), 
element_string, actual_value);
+
+       return FALSE;
+}
+
+static gboolean
+parser_error_from_json_error (JsonReader *reader, const GError *json_error, GError **error)
+{
+       g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
+                    /* Translators: the parameter is an error message. */
+                    _("Invalid JSON was received from the server: %s"), json_error->message);
+
+       return FALSE;
+}
+
 /*
  * gdata_parser_boolean_from_property:
  * @element: the XML element which owns the property to parse
@@ -673,6 +729,193 @@ gdata_parser_object_from_element (xmlNode *element, const gchar *element_name, G
        return TRUE;
 }
 
+/*
+ * gdata_parser_string_from_json_member:
+ * @reader: #JsonReader cursor object to read JSON node from
+ * @member_name: the name of the member to parse
+ * @options: a bitwise combination of parsing options from #GDataParserOptions, or %P_NONE
+ * @output: the return location for the parsed string content
+ * @success: the return location for a value which is %TRUE if the string was parsed successfully, %FALSE if 
an error was encountered,
+ * and undefined if @element didn't match @element_name
+ * @error: a #GError, or %NULL
+ *
+ * Gets the string content of @element if its name is @element_name, subject to various checks specified by 
@options.
+ *
+ * If @element doesn't match @element_name, %FALSE will be returned, @error will be unset and @success will 
be unset.
+ *
+ * If @element matches @element_name but one of the checks specified by @options fails, %TRUE will be 
returned, @error will be set to a
+ * %GDATA_SERVICE_ERROR_PROTOCOL_ERROR error and @success will be set to %FALSE.
+ *
+ * If @element matches @element_name and all of the checks specified by @options pass, %TRUE will be 
returned, @error will be unset and
+ * @success will be set to %TRUE.
+ *
+ * The reason for returning the success of the parsing in @success is so that calls to 
gdata_parser_string_from_element() can be chained
+ * together in a large "or" statement based on their return values, for the purposes of determining whether 
any of the calls matched
+ * a given @element. If any of the calls to gdata_parser_string_from_element() return %TRUE, the value of 
@success can be examined.
+ *
+ * Return value: %TRUE if @element matched @element_name, %FALSE otherwise
+ *
+ * Since: UNRELEASED
+ */
+gboolean
+gdata_parser_string_from_json_member (JsonReader *reader, const gchar *member_name, GDataParserOptions 
options,
+                                      gchar **output, gboolean *success, GError **error)
+{
+       const gchar *text;
+       const GError *child_error = NULL;
+
+       /* Check if there's such element */
+       if (g_strcmp0 (json_reader_get_member_name (reader), member_name) != 0) {
+               return FALSE;
+       }
+
+       /* Check if the output string has already been set. The JSON parser guarantees this can't happen. */
+       g_assert (!(options & P_NO_DUPES) || *output == NULL);
+
+       /* Get the string and check it for NULLness or emptiness. Check for parser errors first. */
+       text = json_reader_get_string_value (reader);
+       child_error = json_reader_get_error (reader);
+       if (child_error != NULL) {
+               *success = parser_error_from_json_error (reader, child_error, error);
+               return TRUE;
+       } else if ((options & P_REQUIRED && text == NULL) || (options & P_NON_EMPTY && text != NULL && *text 
== '\0')) {
+               *success = gdata_parser_error_required_json_content_missing (reader, error);
+               return TRUE;
+       } else if (options & P_DEFAULT && (text == NULL || *text == '\0')) {
+               text = "";
+       }
+
+       /* Success! */
+       g_free (*output);
+       *output = g_strdup (text);
+       *success = TRUE;
+
+       return TRUE;
+}
+
+/*
+ * gdata_parser_int64_time_from_json_member:
+ * @reader: #JsonReader cursor object to read JSON node from
+ * @element_name: the name of the element to parse
+ * @options: a bitwise combination of parsing options from #GDataParserOptions, or %P_NONE
+ * @output: (out caller-allocates): the return location for the parsed time value
+ * @success: the return location for a value which is %TRUE if the time val was parsed successfully, %FALSE 
if an error was encountered,
+ * and undefined if @element didn't match @element_name
+ * @error: a #GError, or %NULL
+ *
+ * Gets the time value of @element if its name is @element_name, subject to various checks specified by 
@options. It expects the text content
+ * of @element to be a date or time value in ISO 8601 format. The returned time value will be a UNIX 
timestamp (seconds since the epoch).
+ *
+ * If @element doesn't match @element_name, %FALSE will be returned, @error will be unset and @success will 
be unset.
+ *
+ * If @element matches @element_name but one of the checks specified by @options fails, %TRUE will be 
returned, @error will be set to a
+ * %GDATA_SERVICE_ERROR_PROTOCOL_ERROR error and @success will be set to %FALSE.
+ *
+ * If @element matches @element_name and all of the checks specified by @options pass, %TRUE will be 
returned, @error will be unset and
+ * @success will be set to %TRUE.
+ *
+ * The reason for returning the success of the parsing in @success is so that calls to 
gdata_parser_int64_time_from_element() can be chained
+ * together in a large "or" statement based on their return values, for the purposes of determining whether 
any of the calls matched
+ * a given @element. If any of the calls to gdata_parser_int64_time_from_element() return %TRUE, the value 
of @success can be examined.
+ *
+ * Return value: %TRUE if @element matched @element_name, %FALSE otherwise
+ *
+ * Since: UNRELEASED
+ */
+gboolean
+gdata_parser_int64_time_from_json_member (JsonReader *reader, const gchar *member_name, GDataParserOptions 
options,
+                                          gint64 *output, gboolean *success, GError **error)
+{
+       const gchar *text;
+       GTimeVal time_val;
+       const GError *child_error = NULL;
+
+       /* Check if there's such element */
+       if (g_strcmp0 (json_reader_get_member_name (reader), member_name) != 0) {
+               return FALSE;
+       }
+
+       /* Check if the output time val has already been set. The JSON parser guarantees this can't happen. */
+       g_assert (!(options & P_NO_DUPES) || *output == -1);
+
+       /* Get the string and check it for NULLness. Check for errors first. */
+       text = json_reader_get_string_value (reader);
+       child_error = json_reader_get_error (reader);
+       if (child_error != NULL) {
+               *success = parser_error_from_json_error (reader, child_error, error);
+               return TRUE;
+       } else if (options & P_REQUIRED && (text == NULL || *text == '\0')) {
+               *success = gdata_parser_error_required_json_content_missing (reader, error);
+               return TRUE;
+       }
+
+       /* Attempt to parse the string as a GTimeVal */
+       if (g_time_val_from_iso8601 ((gchar*) text, &time_val) == FALSE) {
+               *success = gdata_parser_error_not_iso8601_format_json (reader, text, error);
+               return TRUE;
+       }
+
+       /* Success! */
+       *output = time_val.tv_sec;
+       *success = TRUE;
+
+       return TRUE;
+}
+
+/*
+ * gdata_parser_boolean_from_json_member:
+ * @reader: #JsonReader cursor object to read the JSON node from
+ * @member_name: the name of the JSON object member to parse
+ * @options: a bitwise combination of parsing options from #GDataParserOptions, or %P_NONE
+ * @output: (out caller-allocates): the return location for the parsed boolean value
+ * @success: (out caller-allocates): the return location for a value which is %TRUE if the boolean was 
parsed successfully, %FALSE if an error was encountered,
+ * and undefined if @member_name was not found in the current object in @reader
+ * @error: (allow-none): a #GError, or %NULL
+ *
+ * Gets the boolean value of the @member_name member of the current object in the #JsonReader, subject to 
various checks specified by @options.
+ *
+ * If no member matching @member_name can be found in the current @reader JSON object, %FALSE will be 
returned, @error will be unset and @success will be unset. @output will be undefined.
+ *
+ * If @member_name is found but one of the checks specified by @options fails, %TRUE will be returned, 
@error will be set to a
+ * %GDATA_SERVICE_ERROR_PROTOCOL_ERROR error and @success will be set to %FALSE. @output will be undefined.
+ *
+ * If @member_name is found and all of the checks specified by @options pass, %TRUE will be returned, @error 
will be unset and
+ * @success will be set to %TRUE. @output will be set to the parsed value.
+ *
+ * The reason for returning the success of the parsing in @success is so that calls to 
gdata_parser_boolean_from_json_node() can be chained
+ * together in a large "or" statement based on their return values, for the purposes of determining whether 
any of the calls matched
+ * a given JSON node. If any of the calls to gdata_parser_boolean_from_json_node() return %TRUE, the value 
of @success can be examined.
+ *
+ * Return value: %TRUE if @member_name was found, %FALSE otherwise
+ *
+ * Since: UNRELEASED
+ */
+gboolean
+gdata_parser_boolean_from_json_member (JsonReader *reader, const gchar *member_name, GDataParserOptions 
options, gboolean *output, gboolean *success, GError **error)
+{
+       gboolean val;
+       const GError *child_error = NULL;
+
+       /* Check if there's such an element. */
+       if (g_strcmp0 (json_reader_get_member_name (reader), member_name) != 0) {
+               return FALSE;
+       }
+
+       /* Get the boolean. Check for parse errors. */
+       val = json_reader_get_boolean_value (reader);
+       child_error = json_reader_get_error (reader);
+       if (child_error != NULL) {
+               *success = parser_error_from_json_error (reader, child_error, error);
+               return TRUE;
+       }
+
+       /* Success! */
+       *output = val;
+       *success = TRUE;
+
+       return TRUE;
+}
+
 void
 gdata_parser_string_append_escaped (GString *xml_string, const gchar *pre, const gchar *element_content, 
const gchar *post)
 {
diff --git a/gdata/gdata-parser.h b/gdata/gdata-parser.h
index d37b18f..2c5ad2f 100644
--- a/gdata/gdata-parser.h
+++ b/gdata/gdata-parser.h
@@ -35,6 +35,10 @@ gboolean gdata_parser_error_mutexed_properties (xmlNode *element, const gchar *p
 gboolean gdata_parser_error_required_element_missing (const gchar *element_name, const gchar 
*parent_element_name, GError **error);
 gboolean gdata_parser_error_duplicate_element (xmlNode *element, GError **error);
 
+gboolean gdata_parser_error_duplicate_json_element (JsonReader *reader, GError **error);
+gboolean gdata_parser_error_required_json_content_missing (JsonReader *reader, GError **error);
+gboolean gdata_parser_error_not_iso8601_format_json (JsonReader *reader, const gchar *actual_value, GError 
**error);
+
 gboolean gdata_parser_int64_from_date (const gchar *date, gint64 *_time);
 gchar *gdata_parser_date_from_int64 (gint64 _time) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
 gchar *gdata_parser_int64_to_iso8601 (gint64 _time) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
@@ -80,6 +84,12 @@ gboolean gdata_parser_object_from_element_setter (xmlNode *element, const gchar
                                                   gboolean *success, GError **error);
 gboolean gdata_parser_object_from_element (xmlNode *element, const gchar *element_name, GDataParserOptions 
options, GType object_type,
                                            gpointer /* GDataParsable ** */ _output, gboolean *success, 
GError **error);
+gboolean gdata_parser_string_from_json_member (JsonReader *reader, const gchar *member_name, 
GDataParserOptions options,
+                                               gchar **output, gboolean *success, GError **error);
+gboolean gdata_parser_int64_time_from_json_member (JsonReader *reader, const gchar *member_name, 
GDataParserOptions options,
+                                                   gint64 *output, gboolean *success, GError **error);
+gboolean gdata_parser_boolean_from_json_member (JsonReader *reader, const gchar *member_name, 
GDataParserOptions options,
+                                                gboolean *output, gboolean *success, GError **error);
 
 void gdata_parser_string_append_escaped (GString *xml_string, const gchar *pre, const gchar 
*element_content, const gchar *post);
 gchar *gdata_parser_utf8_trim_whitespace (const gchar *s) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
diff --git a/gdata/gdata-private.h b/gdata/gdata-private.h
index 73905f4..dd76ad5 100644
--- a/gdata/gdata-private.h
+++ b/gdata/gdata-private.h
@@ -79,7 +79,12 @@ G_GNUC_INTERNAL GDataParsable *_gdata_parsable_new_from_xml (GType parsable_type
                                                              GError **error) G_GNUC_WARN_UNUSED_RESULT 
G_GNUC_MALLOC;
 G_GNUC_INTERNAL GDataParsable *_gdata_parsable_new_from_xml_node (GType parsable_type, xmlDoc *doc, xmlNode 
*node, gpointer user_data,
                                                                   GError **error) G_GNUC_WARN_UNUSED_RESULT 
G_GNUC_MALLOC;
+G_GNUC_INTERNAL GDataParsable *_gdata_parsable_new_from_json (GType parsable_type, const gchar *json, gint 
length, gpointer user_data,
+                                                              GError **error) G_GNUC_WARN_UNUSED_RESULT 
G_GNUC_MALLOC;
+G_GNUC_INTERNAL GDataParsable *_gdata_parsable_new_from_json_node (GType parsable_type, JsonReader *reader, 
gpointer user_data,
+                                                                   GError **error) G_GNUC_WARN_UNUSED_RESULT 
G_GNUC_MALLOC;
 G_GNUC_INTERNAL void _gdata_parsable_get_xml (GDataParsable *self, GString *xml_string, gboolean 
declare_namespaces);
+G_GNUC_INTERNAL void _gdata_parsable_get_json (GDataParsable *self, JsonBuilder *builder);
 G_GNUC_INTERNAL void _gdata_parsable_string_append_escaped (GString *xml_string, const gchar *pre, const 
gchar *element_content, const gchar *post);
 G_GNUC_INTERNAL gboolean _gdata_parsable_is_constructed_from_xml (GDataParsable *self);
 
@@ -88,6 +93,9 @@ G_GNUC_INTERNAL GDataFeed *_gdata_feed_new (const gchar *title, const gchar *id,
 G_GNUC_INTERNAL GDataFeed *_gdata_feed_new_from_xml (GType feed_type, const gchar *xml, gint length, GType 
entry_type,
                                                      GDataQueryProgressCallback progress_callback, gpointer 
progress_user_data, gboolean is_async,
                                                      GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+G_GNUC_INTERNAL GDataFeed *_gdata_feed_new_from_json (GType feed_type, const gchar *json, gint length, GType 
entry_type,
+                                                     GDataQueryProgressCallback progress_callback, gpointer 
progress_user_data, gboolean is_async,
+                                                     GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
 G_GNUC_INTERNAL void _gdata_feed_add_entry (GDataFeed *self, GDataEntry *entry);
 G_GNUC_INTERNAL gpointer _gdata_feed_parse_data_new (GType entry_type, GDataQueryProgressCallback 
progress_callback, gpointer progress_user_data,
                                                      gboolean is_async);
diff --git a/gdata/gdata-service.c b/gdata/gdata-service.c
index 9c20aff..9d446c9 100644
--- a/gdata/gdata-service.c
+++ b/gdata/gdata-service.c
@@ -914,8 +914,10 @@ __gdata_service_query (GDataService *self, GDataAuthorizationDomain *domain, con
                        gboolean is_async)
 {
        GDataServiceClass *klass;
-       GDataFeed *feed;
+       GDataFeed *feed = NULL;
        SoupMessage *message;
+       SoupMessageHeaders *headers;
+       const gchar *content_type;
 
        message = _gdata_service_query (self, domain, feed_uri, query, cancellable, error);
        if (message == NULL)
@@ -923,8 +925,23 @@ __gdata_service_query (GDataService *self, GDataAuthorizationDomain *domain, con
 
        g_assert (message->response_body->data != NULL);
        klass = GDATA_SERVICE_GET_CLASS (self);
-       feed = _gdata_feed_new_from_xml (klass->feed_type, message->response_body->data, 
message->response_body->length, entry_type,
-                                        progress_callback, progress_user_data, is_async, error);
+
+       headers = message->response_headers;
+       content_type = soup_message_headers_get_content_type (headers, NULL);
+
+       if (content_type != NULL && strcmp (content_type, "application/json") == 0) {
+               /* Definitely JSON. */
+               g_debug("JSON content type detected.");
+               feed = _gdata_feed_new_from_json (klass->feed_type, message->response_body->data, 
message->response_body->length, entry_type,
+                                                 progress_callback, progress_user_data, is_async, error);
+       } else {
+               /* Potentially XML. Don't bother checking the Content-Type, since the parser
+                * will fail gracefully if the response body is not valid XML. */
+               g_debug("XML content type detected.");
+               feed = _gdata_feed_new_from_xml (klass->feed_type, message->response_body->data, 
message->response_body->length, entry_type,
+                                                progress_callback, progress_user_data, is_async, error);
+       }
+
        g_object_unref (message);
 
        if (feed == NULL)
diff --git a/gdata/gdata.symbols b/gdata/gdata.symbols
index f073ad2..bc65405 100644
--- a/gdata/gdata.symbols
+++ b/gdata/gdata.symbols
@@ -308,6 +308,8 @@ gdata_access_rule_get_edited
 gdata_parsable_get_type
 gdata_parsable_new_from_xml
 gdata_parsable_get_xml
+gdata_parsable_new_from_json
+gdata_parsable_get_json
 gdata_calendar_feed_get_type
 gdata_calendar_feed_get_timezone
 gdata_calendar_feed_get_times_cleaned
diff --git a/gdata/tests/common.c b/gdata/tests/common.c
index 382ea73..72e4417 100644
--- a/gdata/tests/common.c
+++ b/gdata/tests/common.c
@@ -630,6 +630,167 @@ gdata_test_compare_xml (GDataParsable *parsable, const gchar *expected_xml, gboo
        return success;
 }
 
+static gboolean
+compare_json_nodes (JsonNode *node1, JsonNode *node2)
+{
+       if (node1 == node2)
+               return TRUE;
+
+       if (JSON_NODE_TYPE (node1) != JSON_NODE_TYPE (node2))
+               return FALSE;
+
+       switch (JSON_NODE_TYPE (node1)) {
+               case JSON_NODE_OBJECT: {
+                       JsonObject *object1, *object2;
+                       guint size1, size2;
+                       GList *members, *i;
+
+                       object1 = json_node_get_object (node1);
+                       object2 = json_node_get_object (node2);
+
+                       size1 = json_object_get_size (object1);
+                       size2 = json_object_get_size (object2);
+
+                       if (size1 != size2)
+                               return FALSE;
+
+                       /* Iterate over the first object, checking that every member is also present in the 
second object. */
+                       members = json_object_get_members (object1);
+
+                       for (i = members; i != NULL; i = i->next) {
+                               JsonNode *child_node1, *child_node2;
+
+                               child_node1 = json_object_get_member (object1, i->data);
+                               child_node2 = json_object_get_member (object2, i->data);
+
+                               g_assert (child_node1 != NULL);
+                               if (child_node2 == NULL) {
+                                       g_list_free (members);
+                                       return FALSE;
+                               }
+
+                               if (compare_json_nodes (child_node1, child_node2) == FALSE) {
+                                       g_list_free (members);
+                                       return FALSE;
+                               }
+                       }
+
+                       g_list_free (members);
+
+                       return TRUE;
+               }
+               case JSON_NODE_ARRAY: {
+                       JsonArray *array1, *array2;
+                       guint length1, length2, i;
+
+                       array1 = json_node_get_array (node1);
+                       array2 = json_node_get_array (node2);
+
+                       length1 = json_array_get_length (array1);
+                       length2 = json_array_get_length (array2);
+
+                       if (length1 != length2)
+                               return FALSE;
+
+                       /* Iterate over both arrays, checking the elements at each index are identical. */
+                       for (i = 0; i < length1; i++) {
+                               JsonNode *child_node1, *child_node2;
+
+                               child_node1 = json_array_get_element (array1, i);
+                               child_node2 = json_array_get_element (array2, i);
+
+                               if (compare_json_nodes (child_node1, child_node2) == FALSE)
+                                       return FALSE;
+                       }
+
+                       return TRUE;
+               }
+               case JSON_NODE_VALUE: {
+                       GType type1, type2;
+
+                       type1 = json_node_get_value_type (node1);
+                       type2 = json_node_get_value_type (node2);
+
+                       if (type1 != type2)
+                               return FALSE;
+
+                       switch (type1) {
+                               case G_TYPE_BOOLEAN:
+                                       return (json_node_get_boolean (node1) == json_node_get_boolean 
(node2)) ? TRUE : FALSE;
+                               case G_TYPE_DOUBLE:
+                                       /* Note: This doesn't need an epsilon-based comparison because we 
only want to return
+                                        * true if the string representation of the two values is equal — and 
if it is, their
+                                        * parsed values should be binary identical too. */
+                                       return (json_node_get_double (node1) == json_node_get_double (node2)) 
? TRUE : FALSE;
+                               case G_TYPE_INT64:
+                                       return (json_node_get_int (node1) == json_node_get_int (node2)) ? 
TRUE : FALSE;
+                               case G_TYPE_STRING:
+                                       return (g_strcmp0 (json_node_get_string (node1), json_node_get_string 
(node2)) == 0) ? TRUE : FALSE;
+                               default:
+                                       /* JSON doesn't support any other types. */
+                                       g_assert_not_reached ();
+                       }
+
+                       return TRUE;
+               }
+               case JSON_NODE_NULL:
+                       return TRUE;
+               default:
+                       g_assert_not_reached ();
+       }
+}
+
+gboolean
+gdata_test_compare_json_strings (const gchar *parsable_json, const gchar *expected_json, gboolean 
print_error)
+{
+       gboolean success;
+       JsonParser *parsable_parser, *expected_parser;
+       GError *child_error = NULL;
+
+       /* Parse both strings. */
+       parsable_parser = json_parser_new ();
+       expected_parser = json_parser_new ();
+
+       json_parser_load_from_data (parsable_parser, parsable_json, -1, &child_error);
+       if (child_error != NULL) {
+               if (print_error == TRUE) {
+                       g_message ("\n\nParsable: %s\n\nNot valid JSON: %s", parsable_json, 
child_error->message);
+               }
+
+               g_error_free (child_error);
+               return FALSE;
+       }
+
+       json_parser_load_from_data (expected_parser, expected_json, -1, &child_error);
+       g_assert_no_error (child_error); /* this really should never fail; or the test has encoded bad JSON */
+
+       /* Recursively compare the two JSON nodes. */
+       success = compare_json_nodes (json_parser_get_root (parsable_parser), json_parser_get_root 
(expected_parser));
+       if (success == FALSE && print_error == TRUE) {
+               /* The comparison has failed, so print out the two JSON strings for ease of debugging */
+               g_message ("\n\nParsable: %s\n\nExpected: %s\n\n", parsable_json, expected_json);
+       }
+
+       g_object_unref (expected_parser);
+       g_object_unref (parsable_parser);
+
+       return success;
+}
+
+gboolean
+gdata_test_compare_json (GDataParsable *parsable, const gchar *expected_json, gboolean print_error)
+{
+       gboolean success;
+       gchar *parsable_json;
+
+       /* Get a JSON string for the GDataParsable. */
+       parsable_json = gdata_parsable_get_json (parsable);
+       success = gdata_test_compare_json_strings (parsable_json, expected_json, print_error);
+       g_free (parsable_json);
+
+       return success;
+}
+
 gboolean
 gdata_test_compare_kind (GDataEntry *entry, const gchar *expected_term, const gchar *expected_label)
 {
diff --git a/gdata/tests/common.h b/gdata/tests/common.h
index 122608b..1159ef5 100644
--- a/gdata/tests/common.h
+++ b/gdata/tests/common.h
@@ -64,15 +64,24 @@ gboolean gdata_test_batch_operation_run_finish (GDataBatchOperation *operation,
 gboolean gdata_test_compare_xml_strings (const gchar *parsable_xml, const gchar *expected_xml, gboolean 
print_error);
 gboolean gdata_test_compare_xml (GDataParsable *parsable, const gchar *expected_xml, gboolean print_error);
 
+gboolean gdata_test_compare_json_strings (const gchar *parsable_json, const gchar *expected_json, gboolean 
print_error);
+gboolean gdata_test_compare_json (GDataParsable *parsable, const gchar *expected_json, gboolean print_error);
+
 gboolean gdata_test_compare_kind (GDataEntry *entry, const gchar *expected_term, const gchar 
*expected_label);
 
-/* Convenience macro */
+/* Convenience macros. */
 #define gdata_test_assert_xml(Parsable, XML) \
        G_STMT_START { \
                gboolean _test_success = gdata_test_compare_xml (GDATA_PARSABLE (Parsable), XML, TRUE); \
                g_assert (_test_success == TRUE); \
        } G_STMT_END
 
+#define gdata_test_assert_json(Parsable, JSON) \
+       G_STMT_START { \
+               gboolean _test_success = gdata_test_compare_json (GDATA_PARSABLE (Parsable), JSON, TRUE); \
+               g_assert (_test_success == TRUE); \
+       } G_STMT_END
+
 /* Common code for tests of async query functions that have progress callbacks */
 typedef struct {
     guint progress_destroy_notify_count;
diff --git a/gdata/tests/general.c b/gdata/tests/general.c
index 7c23fc9..88831e8 100644
--- a/gdata/tests/general.c
+++ b/gdata/tests/general.c
@@ -456,6 +456,53 @@ test_entry_get_xml (void)
 }
 
 static void
+test_entry_get_json (void)
+{
+       gint64 updated, published, updated2, published2;
+       GDataEntry *entry, *entry2;
+       gchar *json;
+       GError *error = NULL;
+
+       entry = gdata_entry_new (NULL);
+
+       /* Set the properties more conventionally */
+       gdata_entry_set_title (entry, "Testing title & \"escaping\"");
+       gdata_entry_set_summary (entry, NULL);
+       gdata_entry_set_content (entry, NULL);
+       gdata_entry_set_rights (entry, NULL);
+
+       /* Check the generated JSON's OK */
+       gdata_test_assert_json (entry,
+               "{"
+                       "\"title\":\"Testing title & \\\"escaping\\\"\""
+               "}");
+
+       /* Check again by re-parsing the JSON to a GDataEntry. */
+       json = gdata_parsable_get_json (GDATA_PARSABLE (entry));
+       entry2 = GDATA_ENTRY (gdata_parsable_new_from_json (GDATA_TYPE_ENTRY, json, -1, &error));
+       g_assert_no_error (error);
+       g_assert (GDATA_IS_ENTRY (entry2));
+       g_clear_error (&error);
+       g_free (json);
+
+       g_assert_cmpstr (gdata_entry_get_title (entry), ==, gdata_entry_get_title (entry2));
+       g_assert_cmpstr (gdata_entry_get_id (entry), ==, gdata_entry_get_id (entry2)); /* should both be NULL 
*/
+       g_assert_cmpstr (gdata_entry_get_content (entry), ==, gdata_entry_get_content (entry2));
+       g_assert_cmpstr (gdata_entry_get_content_uri (entry), ==, gdata_entry_get_content_uri (entry2)); /* 
should both be NULL */
+
+       updated = gdata_entry_get_updated (entry);
+       updated2 = gdata_entry_get_updated (entry2);
+       g_assert_cmpuint (updated, ==, updated2);
+
+       published = gdata_entry_get_published (entry);
+       published2 = gdata_entry_get_published (entry2);
+       g_assert_cmpuint (published, ==, published2);
+
+       g_object_unref (entry);
+       g_object_unref (entry2);
+}
+
+static void
 test_entry_parse_xml (void)
 {
        GDataEntry *entry;
@@ -492,7 +539,76 @@ test_entry_parse_xml (void)
 }
 
 static void
-test_entry_error_handling (void)
+test_entry_parse_json (void)
+{
+       GDataEntry *entry;
+       GError *error = NULL;
+
+       /* Create an entry from JSON with unhandled nodes. */
+       entry = GDATA_ENTRY (gdata_parsable_new_from_json (GDATA_TYPE_ENTRY,
+               "{"
+                       "\"title\":\"A title\","
+                       "\"updated\":\"2009-01-25T14:07:37Z\","
+                       "\"selfLink\":\"http://example.com/\",";
+                       "\"etag\":\"some-etag\","
+                       "\"id\":\"some-id\","
+                       "\"kind\":\"kind#kind\","
+                       "\"unhandled-boolean\":false,"
+                       "\"unhandled-string\":\"this-is-a-string---sometimes\","
+                       "\"unhandled-int\":15,"
+                       "\"unhandled-double\":42.42,"
+                       "\"unhandled-object\":{"
+                               "\"a\":true,"
+                               "\"b\":true"
+                       "},"
+                       "\"unhandled-array\":["
+                               "1,"
+                               "2,"
+                               "3"
+                       "],"
+                       "\"unhandled-null\":null"
+                "}", -1, &error));
+       g_assert_no_error (error);
+       g_assert (GDATA_IS_ENTRY (entry));
+
+       /* Now check the outputted JSON from the entry still has the unhandled nodes. */
+       gdata_test_assert_json (entry,
+               "{"
+                       "\"title\":\"A title\","
+                       "\"id\":\"some-id\","
+                       "\"updated\":\"2009-01-25T14:07:37Z\","
+                       "\"etag\":\"some-etag\","
+                       "\"selfLink\":\"http://example.com/\",";
+                       "\"kind\":\"kind#kind\","
+                       "\"unhandled-boolean\":false,"
+                       "\"unhandled-string\":\"this-is-a-string---sometimes\","
+                       "\"unhandled-int\":15,"
+                       "\"unhandled-double\":42.42,"
+                       "\"unhandled-object\":{"
+                               "\"a\":true,"
+                               "\"b\":true"
+                       "},"
+                       "\"unhandled-array\":["
+                               "1,"
+                               "2,"
+                               "3"
+                       "],"
+                       "\"unhandled-null\":null"
+                "}");
+       g_object_unref (entry);
+
+       /* Test parsing of empty titles. */
+       entry = GDATA_ENTRY (gdata_parsable_new_from_json (GDATA_TYPE_ENTRY,
+               "{"
+                       "\"title\":\"\""
+                "}", -1, &error));
+       g_assert_no_error (error);
+       g_assert (GDATA_IS_ENTRY (entry));
+       g_object_unref (entry);
+}
+
+static void
+test_entry_error_handling_xml (void)
 {
        GDataEntry *entry;
        GError *error = NULL;
@@ -524,6 +640,56 @@ test_entry_error_handling (void)
 }
 
 static void
+test_entry_error_handling_json (void)
+{
+       GDataEntry *entry;
+       GError *error = NULL;
+
+#define TEST_JSON_ERROR_HANDLING(x) \
+       entry = GDATA_ENTRY (gdata_parsable_new_from_json (GDATA_TYPE_ENTRY,x, -1, &error));\
+       g_assert_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR);\
+       g_assert (entry == NULL);\
+       g_clear_error (&error)
+#define TEST_JSON_ERROR_HANDLING_PARSER(x) \
+       entry = GDATA_ENTRY (gdata_parsable_new_from_json (GDATA_TYPE_ENTRY,x, -1, &error));\
+       g_assert_error (error, GDATA_PARSER_ERROR, GDATA_PARSER_ERROR_PARSING_STRING);\
+       g_assert (entry == NULL);\
+       g_clear_error (&error)
+
+       /* General structure. */
+       TEST_JSON_ERROR_HANDLING_PARSER ("[true,false,true]"); /* root node is not an object */
+       TEST_JSON_ERROR_HANDLING_PARSER ("false"); /* root node is not an object */
+       TEST_JSON_ERROR_HANDLING_PARSER ("invalid json"); /* totally invalid JSON */
+
+       /* id */
+       TEST_JSON_ERROR_HANDLING ("{\"id\":\"\"}"); /* empty */
+       TEST_JSON_ERROR_HANDLING ("{\"id\":null}"); /* invalid type */
+
+       /* updated */
+       TEST_JSON_ERROR_HANDLING ("{\"updated\":\"not correct\"}"); /* incorrect format */
+       TEST_JSON_ERROR_HANDLING ("{\"updated\":false}"); /* invalid type */
+       TEST_JSON_ERROR_HANDLING ("{\"updated\":\"\"}"); /* empty */
+
+       /* title */
+       TEST_JSON_ERROR_HANDLING ("{\"title\":false}"); /* invalid type */
+
+       /* etag */
+       TEST_JSON_ERROR_HANDLING ("{\"etag\":\"\"}"); /* empty */
+       TEST_JSON_ERROR_HANDLING ("{\"etag\":false}"); /* invalid type */
+
+       /* selfLink */
+       TEST_JSON_ERROR_HANDLING ("{\"selfLink\":\"\"}"); /* empty */
+       TEST_JSON_ERROR_HANDLING ("{\"selfLink\":false}"); /* invalid type */
+
+       /* kind */
+       TEST_JSON_ERROR_HANDLING ("{\"kind\":\"\"}"); /* empty */
+       TEST_JSON_ERROR_HANDLING ("{\"kind\":false}"); /* invalid type */
+
+#undef TEST_JSON_ERROR_HANDLING_PARSER
+#undef TEST_JSON_ERROR_HANDLING
+}
+
+static void
 test_entry_escaping (void)
 {
        GDataEntry *entry;
@@ -3997,8 +4163,11 @@ main (int argc, char *argv[])
        g_test_add_func ("/service/locale", test_service_locale);
 
        g_test_add_func ("/entry/get_xml", test_entry_get_xml);
+       g_test_add_func ("/entry/get_json", test_entry_get_json);
        g_test_add_func ("/entry/parse_xml", test_entry_parse_xml);
-       g_test_add_func ("/entry/error_handling", test_entry_error_handling);
+       g_test_add_func ("/entry/parse_json", test_entry_parse_json);
+       g_test_add_func ("/entry/error_handling/xml", test_entry_error_handling_xml);
+       g_test_add_func ("/entry/error_handling/json", test_entry_error_handling_json);
        g_test_add_func ("/entry/escaping", test_entry_escaping);
        g_test_add_func ("/entry/links/remove", test_entry_links_remove);
 


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