[libgdata] core: Add support for JSON to GDataParsable, GDataEntry and GDataFeed
- From: Philip Withnall <pwithnall src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libgdata] core: Add support for JSON to GDataParsable, GDataEntry and GDataFeed
- Date: Fri, 30 Aug 2013 05:22:10 +0000 (UTC)
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]